Repository navigation
typeset.us 4.4.0: adoption fixes (defaults unchanged, expanded coverage opt-in) - #14
Merged
Merged
Conversation
The existing golden corpus has none of the constructions the 4.4 adoption fixes change, so a golden run against 4.3.1 would report "0 cells changed" whatever those fixes did (PLAN decision 3). tests/v4-corpus-4.4.json holds 33 cases: long unbreakable runs (499 and 501 letters, 600 closing quotes, 600 letters and a hyphen, 600 spaced opening brackets, 600 no-break-joined characters, and a 600-character URL that breaks at its slashes), declared languages (en_US, english, en-GB, pt, pt-BR, it, nl, vi, sr-Latn, a Spanish phrase in English), Greek letters in Latin text (5 μg/mL, α-synuclein, a Greek sentence), inline elements 4.3 declines (sup, sub, time, kbd, dfn, ins, visually hidden link text, an aria-hidden icon) and quote cases (width="100", French spaced quotes, dialogue, primes, elisions, untagged German). Each carries a group and the class the plan gives it; E1 adds the long-run expectations and E2 appends its cases. verify-golden composes every case at the corpus widths and fonts (264 cells per engine) as the "coverage" variant, against --coverage-baseline (default: the published 4.3.1). A coverage cell must be identical to that release unless its case names an expected outcome or a change this build makes; it may not gain overflow the baseline lacked, a new orphan or more line-end problems. The report counts, per engine and per group, how many cells each build composed, and the summary gives the existing corpus (3,932 cells per engine) and the coverage corpus apart. The published 4.3.1 composes 55, 54 and 63 of the 264 coverage cells in Chromium, WebKit and Firefox (run of 2026-09-29, output/golden-E1-vs-4.3.1.json). The "recorded trade-off still occurs" check now runs only when --baseline is a 4.2 build: the trade-offs are changes against 4.2.0, and against 4.3.1 (the plan's golden command) they are identical cells, so the check failed for a reason that says nothing about the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tsconfig.json includes every .ts file, so the site's type check (next build
runs it) also read the global.d.ts files that packages/typeset-v4/dist and
public/releases/4.3.0 and 4.3.1 declare: each types window.Typeset as its
own build's API. src/lib/v4/typeset.go.ts types it as the API in src, so any
change to that API before a release cut makes the declarations disagree.
Adding one outcome to OUTCOMES (the next commit) gave TS2717 and TS2322 in
typeset.go.ts, typeset.release.standalone.ts, typeset.website-go.ts and
src/components/TypesetV4.tsx ("Source has 43 element(s) but target allows
only 42"), which would fail npm run build on release/4.4.0 until the cut
rewrites dist. The site imports the engine from src, never from dist or the
release archive, so both are excluded; tsc -p tsconfig.json passes with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e measuring it WebKit's Range.getClientRects() takes time in proportion to the length of the line a range is on, and measuring a paragraph reads a word the browser splits across lines one character at a time, so a long run costs the square of its length. SUPPORT.md documents about 37 s in typeset() for 11,000 closing quotes and a letter and 15 s for 11,000 letters, a hyphen and a letter, both under the 12,000-character budget, and the default script tag composes every p, li and td, comments and profiles included: one hostile comment froze Safari for every visitor (adoption review, serious item 5). exceedsRunBudget() (break-opportunities.ts) finds whether more than 500 code points, collapsible white space aside, lie between two line-break opportunities. Prose costs almost nothing: the text is first split where a break is certain (a letter or digit, spaces, a letter: UAX #14 LB18 with no earlier rule applying; all 2,868 such breaks in the 118 corpus paragraphs are breaks the Unicode rules give), and only a piece longer than 500 characters runs the Unicode rules. typeset() checks it before the first measureLayout and declines as native:run-budget (named after native:budget, native:line-budget and native:tracking-budget) with no line boxes read; planRichText() checks it too, so TypesetRichText declines the same way, and its onResult reads no boxes for it either. No opt-out: it is a safety limit, and the browser wraps such runs itself (overflow-wrap: break-word wraps them at the measure). Measured with typeset() at 600 px, 18px Georgia, 4.3.1 against this build (output/e1/run-budget-timing.json, 2026-09-29): WebKit 26.5: 11,000 closing quotes 24,842 ms -> 18 ms; 11,000 letters, a hyphen and a letter 9,904 ms -> 8 ms Chromium 149: 29 ms -> 19 ms; 1,328 ms -> 7 ms Firefox 151: 33 ms -> 24 ms; 156 ms -> 10 ms verify-promise now checks five such inputs (quotes, letters and a hyphen, letters in a sentence, 5,000 spaced opening brackets, 3,000 no-break-joined words) in all three engines: native:run-budget in under 50 ms from typeset() and from planRichText(), nothing measured, text unchanged; and that runs of 499 and 500 characters are not declined and 501 is. Rendering: golden run against 4.3.1 (output/golden-E1-vs-4.3.1.json), the existing 3,932 cells per engine are unchanged by this commit; in the coverage corpus the 5 long-run cases over the limit (40 cells per engine) go from native:no-candidate to native:run-budget, and no cell that 4.3.1 composed changes (55, 54 and 63 composed in Chromium, WebKit and Firefox, before and after). The 499-letter run and the 600-character URL that breaks at its slashes are identical to 4.3.1. The coverage cases now carry these expected outcomes. OUTCOMES.md, docs/outcomes.md (npm run docs:outcomes), the README's outcome count (43) and capabilities.json name the new code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every spacing and hanging marker carried 18 inline declarations, and every
tracking wrapper an inline all: unset, which Chromium, WebKit and Firefox
serialize as every longhand: 6,473, 7,938 and 5,224 bytes of style
attribute per wrapper (output/e1/cascade-probe, 2026-09-29). The adoption
review counted "about 18 inline style rules" per marker and every added
element styled inline, which bloats pages and session-replay recordings
(serious item 4).
The shared declarations now live in the engine's constructable stylesheet
(LIFECYCLE_CSS, mirrored in dist/styles.css). Each marker keeps only its
own margin-left, each wrapper its letter-spacing and word-spacing:
- Spacing and hanging markers: !important in the typeset-markers cascade
layer. An unlayered !important page rule, which beat the inline
declarations, loses to it, so page CSS reaches them no more easily than
before. margin-left stays inline and outside the layer, so the stale and
print rules still zero it.
- Tracking wrappers: all: unset in a rule that is not important, weighted
with three :not(#_) so ordinary page rules do not reach it, as they did
not reach the inline all: unset. An !important all: unset (tried first,
with letter-spacing: revert-layer) hid the inline spacing: revert-layer in
an important layer rolls back to "normal" in all three engines, and
tracking rolled back everywhere. Normal importance keeps the wrapper's
spacing reachable by the stale and print rules and by page !important
rules such as the WCAG 1.4.12 text-spacing override, as in 4.3.
- Breaks keep their one inline !important declaration, which no rule can
override; a layer would let an earlier layer's !important rule hide them.
- markerRules() installs the sheet before markers are written and checks
it is adopted by the element's document or shadow root. Where it is not
(no constructable stylesheets), markers carry the 4.3 declarations inline.
The inline margin uses longhands: React 19 warned about a shorthand
beside its longhand when a style object changed between renders.
TypesetRichText keeps its last answer while React 19's strict mode renders
with the host ref detached.
Measured on the 85 tests/v4-corpus.json paragraphs at 375 px in 18px
Georgia, default options, per composed paragraph (output/e1/marker-metrics.json):
engine elements style attribute bytes outerHTML bytes
Chromium 20.6 21,991 -> 690 23,659 -> 2,357
WebKit 20.7 26,192 -> 700 27,865 -> 2,373
Firefox 20.5 18,389 -> 672 20,055 -> 2,338
Element counts do not change.
Rendering: golden run against 4.3.1 (output/golden-E1-vs-4.3.1.json), 0 of
the 3,932 existing cells per engine change; 2,117 (Chromium), 2,104
(WebKit) and 2,119 (Firefox) differ only in marker style attributes and are
counted apart as styleMoved, a new neutral category that requires the same
per-marker values and identical line boxes and marker boxes; in the
coverage corpus 52, 51 and 59 cells are styleMoved and none otherwise
changes. (WebKit's computed margin-left reports the used 1/64 px value for
4.3's inline declarations and the specified one here; the line boxes are
identical, so the geometry check compares boxes, not that string.)
verify-spacing gains checks in all three engines: markers carry only their
own values inline and compute as 4.3 wrote them; a shadow root adopts the
sheet; with adoptedStyleSheets removed they fall back to the 4.3
declarations with identical outcomes, finishes and geometry; and with
span{display:block!important;padding:3px!important;margin:9px} on the page
every paragraph gets 4.3.1's outcome or a native one, text intact, one line
per composed line. React markers carry only their own values. The
/pairing-cards PNG export (html2canvas-pro clones the page without adopted
sheets) renders identically with the sheet and with every marker's 4.3
declarations inline: byte-identical PNGs in Chromium and WebKit, 0 differing
pixels in Firefox, both card sizes (next build of this branch, 2026-09-29).
verify-promise: a page's [data-ts-hang]{position:relative!important} no
longer reaches the marker, so the hang applies, static, where 4.3 rolled it
back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composed text installs a document copy handler that puts the source text on the clipboard without the generated line breaks. It yields to a handler the page registered first, but there was no way to turn it off, which the adoption review's security lens flagged for publishers whose attribution or data-loss-prevention scripts run later and read the native copy (smaller enterprise-review snags, item a). copy (default true) is now an option of typeset(), mount() and both React adapters, and data-typeset-copy="false" on the auto and go loaders. An element composed with copy: false is never registered with the handler, and the handler is added only when some composed element has copy on, so a page composed entirely with copy: false has none. The browser's own copy of such an element has a line break at every composed line end; the option's JSDoc and the README row say so. A copy that reaches an enabled host is still serialized, as before. The option is part of the options key, a TypesetRichText copy prop change registers or releases the host, and development builds validate it as a boolean. Tests, all three engines: verify-copy-privacy copies from a page composed with copy: false (no copy handled; a copy inside a paragraph is native, with line breaks, and has the uncomposed page's words) and from a page with one host off and the rest on (the off host's copy is native; copies that reach an enabled host equal native copy of the uncomposed page). verify-loaders runs every loader with data-typeset-copy="false" (copy left to the browser, composition and finishes unchanged) and checks that by default a copy puts the source text on the clipboard. verify-options checks that copy: true and false log nothing and copy: "no" warns once. The warning text is not yet in SUPPORT.md's list, which the DOCS step edits. Rendering: none. The golden run against 4.3.1 is unchanged by this commit (the default is true). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
auditJSON() and typeset-audit reported an overflow error, and pass: false,
for a paragraph that cuts its own text off with white-space: nowrap,
overflow: hidden and text-overflow: ellipsis (the adoption review's #ellip
probe: "1270.66px outside content box"), so a page using ordinary
truncation failed the CI gate the README sells.
When an element overflows and its overflow is hidden or clip with
text-overflow: ellipsis or a -webkit-line-clamp, the audit now adds the
review item clipped ("Npx clipped on purpose (overflow: hidden,
text-overflow: ellipsis)") instead of the overflow error. Review items
never fail the gate, so pass follows the existing rule. Overflow without
clipping is still an error. The CLI's help says review items, text clipped
on purpose among them, never fail it; capabilities.json says the same.
Tests, all three engines: verify-audit checks that the ellipsis case passes
with exactly one clipped review item, that a line clamp clipping a long
word is a clipped review and not an overflow error, and that unclipped
overflow is still an error. verify-cli audits a page with such a paragraph
through the CLI: exit 0, pass true, a clipped review targeting #ellip, no
overflow issue (4.3: exit 1).
Rendering: none (the audit only reads).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured per commit with the release recipe (Node 24 zlib, gzip level 9), from the 4.3.1 cut to this branch, on typeset.global.js (react.js+shared in parentheses): run budget (f33340c) +227 B (+200) marker stylesheet (5ec07a7) +359 B (+489) copy: false (e7e7734) +57 B (+72) clipped audit item (19ff84a) +95 B (+0) total +738 B (+761), 1.3% (1.4%) Every entry: mount-only 50,361 -> 51,005, TypesetText-only 50,489 -> 51,157, TypesetRichText-only 46,820 -> 47,399, smartQuotes-only 1,544 unchanged, react.js+shared 54,083 -> 54,844, TypesetText-no-tree-shaking 63,169 -> 64,027, go.js 56,467 -> 57,213, typeset.global.js 56,122 -> 56,860; dist/auto.js 56,479 -> 57,228. Measured against the 4.3.0 budgets, seven entries exceeded the 2% tolerance, so the budgets now hold the measured sizes, with this raise recorded in budgets.json. The marker move costs bytes in the bundle and saves about 21 KB of style attributes per composed paragraph at 375 px in Chromium (25 KB WebKit, 18 KB Firefox). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full test:v4 on 4533257 (M2 Pro, 2026-09-29) failed one check out of 13,895: verify-package-contents' "unpacked package is under 1.91 MB", at 1,936,451 bytes (87 files), against 4.3.1's 1,902,907. Per file, against public/releases/4.3.1: react.cjs +3,438 B, index.cjs +3,150, the shared ESM chunk +3,142, typeset.global.js.map +2,242, go.js.map +2,220, auto.js +2,090, go.js +2,080, typeset.global.js +2,042, lifecycle.d.ts and .d.cts +1,770 each, capabilities.json +1,367, break-opportunities.d.ts and .d.cts +991 each, styles.css +958, react.js +927, OUTCOMES.md +589, the other declarations and README.md the rest: the run budget, the markers' stylesheet with its inline fallback, the copy option and the clipped audit item in every build, and their docs. The limit moves to 1.94 MB rather than any fix being dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct at the script tag The adoption review (2026-09-28) found three first-hour traps the README left to SUPPORT.md or to nothing: - Language. English line-end preferences apply only to text declared English. On 42 paragraphs at 375 px in Chromium, stranded short words at line ends went from 59 to 7 with lang="en" and only to 36 untagged; the install section never said to set lang. "Do I need it?" gains a row for languages, scripts and text styles Typeset leaves native, noting the script still downloads. - User-generated text. The script tag's default selector covers comments and profiles, and in 4.3 one long unbroken run froze a Safari tab. The warning under the script tag says to exclude those areas, and that from 4.4 such paragraphs are declined as native:run-budget (E1, f33340c). SUPPORT.md's limitation now gives E1's timings (WebKit 24,842 ms and 9,904 ms against 18 ms and 8 ms) instead of "4.3 keeps the exact measurement". - Server-rendered React. Composing before hydration made React 19 report #418 and React 18 #425, #418 and #423, and client-render the root; a 300 ms hydration delay was enough. The warning says so, says 4.4 waits for hydration where it finds a framework's markers, and points to the adapters or mount() in useEffect. The React section also stops saying server-rendered text rewraps "without adding a line" (its own FAQ says 1 in 6 paragraphs at 320 px take one), points MDX, next/link, router-link and <Trans> content to the SetArticle mount() recipe on /install/frameworks, and says the adapters are React DOM only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion The README's SEO answer was a bare "No", and "What it won't do" left out the side effects of real <br> line breaks that SUPPORT.md and the adoption review measured: - Find-in-page and Text Fragment links (#:~:text=, which Google uses to highlight a quoted passage) miss a phrase across a generated break: window.find found 43 of 60 five-word phrases at 375 px and 52 of 60 at 768 px, in each of Chromium, WebKit and Firefox, against 60 of 60 uncomposed. - Firefox Reader View (Readability) double-wrapped 35 of 40 paragraphs composed at 375 px (4.2.0: 0). Safari Reader, Chrome Reading mode and Edge Immersive Reader are unmeasured, and the README now says so. - A live English to Spanish Google Translate check put a stray space before punctuation in 3 of 9 paragraphs in all three engines. Safari's and Firefox's translators are not detected and untested; README and SUPPORT.md now say that, and SUPPORT.md notes that WebKit's text manipulation API ends an item at each <br>. The SEO answer keeps "not for indexing or ranking" and adds the effect on readers arriving from a search, with data-no-typeset recommended for documentation and reference pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le recipe The adoption review found that the defaults differ by entry point and were spread over two tables and prose: the script tag curls quotes and hangs punctuation, typeset.us/go, mount() and the adapters do not, and spacing and tracking are on everywhere. A table under Options now gives, per install method (script tag or typeset.us/auto, typeset.us/go, mount(), typeset(), TypesetText, TypesetRichText), what it sets, smart quotes, hanging punctuation, spacing and tracking, headings and the copy option, as the 4.3.1 loaders and adapters behave. "What it costs" gains a recipe that imports the package after requestIdleCallback (with a timer fallback) and says it moves the download, not the main-thread work, plus the gzip size of each entry. Sizes are 4.3.1's, like the rest of that section: esbuild bundles of packages/typeset-v4/dist, minified, gzip -9, measured 2026-09-29: typeset() 44,294 B, mount 50,361, TypesetText 50,489, TypesetRichText 46,820, analyzeBreaks 14,522, smartQuotes 1,544, auto.js 56,476. On the E1 candidate (7e20d13) they are 44,785, 51,005, 51,157, 47,399, 14,522, 1,544 and 57,228; the 4.4.0 docs pass restates them for the final build. The FAQ's "can I defer it?" answer links the recipe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
STABILITY.md said only that the previous minor gets security fixes, and SECURITY.md's table had no dates, so nobody could plan an upgrade (PLAN decision 19; the dates await the owner as O3, and these are the defaults): - The 4.x line gets bug and security fixes until at least 2027-09-30, whenever 5.0 ships. - When a minor ships, the minor before it gets security fixes for 60 days. 4.2.x: until 2026-11-26, 60 days after 4.3.0 reached npm (npm view typeset.us time: 2026-09-27T05:09Z). The row also says 4.2 has the screen-reader defect 4.3 fixed. The package README's Stability section says the same in one sentence. SECURITY.md lists the email first: GitHub private vulnerability reporting is switched off for the repository (S10, gh api 2026-09-29), so the old "Preferred" link led nowhere. "How releases are made" now says 4.3.1 was published by release.yml with provenance (npm view typeset.us@4.3.1 dist.attestations, 2026-09-29: an SLSA v1 provenance attestation) and 4.0.0 to 4.3.0 have none. verify-release-trust requires the dated rows, the until-at-least date, the 60-day rule and the email before the private-reporting link, instead of the literal "| 4.2.x | Security fixes only |". packages/typeset-v4/SECURITY.md is kept byte-identical to SECURITY.md, as that suite checks and as release-cut would copy it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The description and the README's first line said "verified in Chrome, Safari and Firefox". The suites run Playwright's Chromium, WebKit and Firefox on macOS; WebKit there is not Safari, and no shipping Safari, physical phone or Windows machine is in the matrix (adoption review, project-health and stranger-first-hour lenses). The words now name what is tested. verify-package-contents checks the new description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify-package-contents failed on the adoption docs: "unpacked package is under 1.94 MB" at 1,944,532 bytes, against E1's 1,936,451 (M2 Pro, 2026-09-29). After two small wording cuts the tree packs 1,944,497 B, +8,046 B over 7e20d13, all in packaged docs: README.md +6,919 (install warnings for user-generated text, server-rendered React and lang, the defaults table, the idle-load recipe with per-entry sizes, and the find-in-page, reader-view and translation limits), SUPPORT.md +672 (run-budget timings, translator notes) and SECURITY.md +455 (the dated support window). No build output changed. The limit moves to 1.95 MB rather than any of those docs being dropped; the suite passed at 1.95 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only declared English in the auto loader A double quote with white space or the text's edge on both sides now stays straight: French spaced quotes (Il a dit : " Bonjour ") became two closing quotes. So does a double quote with no open quotation to close: width="100" became width=”100", and aria-live="polite" and said:"hello" got a closing quote first. "Hello," she said., primes (5'10", 27") and elisions (Rock 'n' roll in the '90s) are as in 4.3.1, and the million-text equivalence check holds against the updated reference. These apply wherever quotes are educated (class B). The auto loader curled quotes in untagged text, so untagged German and French pages got English quotes (Er sagte: "Hallo Welt" -> “Hallo Welt”). Its default is now smartQuotes: 'en-declared', a new value: English quotes only where the element or an ancestor declares English. data-typeset-smart-quotes="en" restores the 4.3 behaviour, and an explicit npm smartQuotes: 'en' keeps its meaning. go.js accepts "en-declared" too. The React adapters see only their own lang prop during render, so 'en-declared' needs lang on the component there. A lang is read through the new language.ts: en_US and en_US.UTF-8 are en-US and english is en, so such pages count as English for quotes. SUPPORT.md lists the development warning for smartQuotes and is edited only by the DOCS step, so verify-options' check of that list is recorded in known-failures.json until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.3 said nothing when a page's blocks all declined (a lang it could not read, such as en_US before 4.4, left every block native) or when composed text declared no language, so English line-end preferences were off. - Both loaders log at most two console.info lines after their first pass, never a warning or an error: "no lang attribute, so English line-end preferences are off; add lang="en" to <html> if the page is in English", only when composed blocks are untagged; and "none of the N matched blocks was composed; most common: <outcome> ×n; see https://typeset.us/docs". Outcomes that mean nothing needed improving (fits, empty, sentence-aligned, paragraph-rhythm) do not count. - auditJSON() gains the review items untagged (one for the scope) and uncomposed (nothing in scope composed for another reason, including native:environment under jsdom and happy-dom), with the most common outcome. pass does not change. - typeset-audit prints one "typeset-audit: warning:" line on stderr for uncomposed, naming the widths; the exit code does not change. Tests: verify-audit (untagged, lang="en", the legacy path, a declined scope, a scope that fits, native:environment), verify-cli (the warning line and exit 0; no line for a composed page) and verify-react-node (the jsdom lane's uncomposed item). The loader lines are checked in verify-loaders with the other 4.4 loader changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A composed heading that wraps has a generated line break inside it, which iOS VoiceOver may read as two items. No one has checked that by ear yet, and STABILITY.md changes a default only for a verified accessibility failure, so the script tag keeps composing h1-h6 by default (PLAN decision 5) and organisations that need an accessibility sign-off can turn it off: - headings: false for mount() and typesetAll(), and data-typeset-headings="false" on both loaders, leave h1-h6, [role=heading] and anything inside them untouched, with no outcome written (no data-ts-outcome, not even native:environment under jsdom). - typeset(el) still composes the element it is given; the JSDoc and the README say so. - Development builds check that headings is a boolean. Without the option output is identical to 4.3.1 (class A). The checks (typesetAll(), mount() and typeset() in three engines, the auto loader with the attribute, the option warnings) land with the coverage suite in the next commit, which exercises all of the 4.4 loader options together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arkup, with coverage: 'core' to keep 4.3.1's coverage
Verified defects, on under both coverages (class B):
- A lang in another spelling is read as the language it names: en_US,
en_US.UTF-8, EN_us and en_GB (CMS and PHP templates), and the
unambiguous names english, Deutsch, francais, español, italiano,
nederlands, português. 4.3 read them as unknown languages and left
every block native:language; they now compose exactly as en-US, en,
de, fr and es do (the golden run's reference check, all 24 cells per
engine, outcome and markup).
- Up to three Greek or Cyrillic letters in a row in Latin text: "5 μg/mL",
"α-synuclein", "ΔG" and "ПЦР" compose, which makes the μg entries in
phrase-boundaries reachable ("5 μg" is bound like "5 mg"). A Greek
sentence, a Cyrillic word or four such letters still decline.
Coverage additions (class C), behind coverage: 'extended' | 'core'
(data-typeset-coverage on both loaders, the coverage prop on both React
adapters), whose default is the one constant COVERAGE_DEFAULT in
coverage.ts: 'extended' for now, owner decision O1 at the cut.
- Any lang whose likely script is Latin (pt, it, nl, pl, sv, tr, vi,
fil, sw, ht, sr-Latn and the rest) composes with neutral preferences,
as untagged text does, and an unreadable tag counts as untagged. ar,
he, ja, zh, th, hi, ko, el and Cyrillic sr still decline.
- A descendant in another Latin-script language is part of its
paragraph, set with the block's preferences; a Japanese phrase still
leaves it native:mixed-language.
- time, dfn, kbd and ins; sup, sub, vertical-align super and sub, and sup
raised with position: relative and a top offset (normalize.css),
measured in place with the rendered lines as the check.
- Visually hidden text (the sr-only pattern) and aria-hidden elements
with no width are zero-width atoms: no break inside them, left out of
measurement, spacing and tracking, and a space beside them keeps its
natural width (a spacing marker there left a white-space-only text node
that Chromium dropped from its tree: "hours on" read "hourson").
- ::after link icons, visible aria-hidden icons, br, img, svg, q and bdi
stay native.
Golden, once on coverage: 'core' against the published 4.3.1, with the
coverage corpus composed again with the defaults (the new
coverage-default pass): existing corpus 0 of 3,932 cells changed per
engine; coverage corpus (47 cases, 376 cells) under core 104 changed, all
class B or E1's run budget, and every class C cell identical; with the
defaults 280 changed, 0 unexplained; composed 55/54/63 (4.3.1) ->
103/102/111 (core) -> 278/276/286 (default) in Chromium/WebKit/Firefox.
Two cells that newly compose as English end with more weak line ends
than native are allowed trade-offs with their reasons (ΔG at 320 px, the
footnote sup at 240 px, whose lines are those 4.3.1 gives the paragraph
without it).
verify-coverage (new, in suites.mjs) checks spellings, 23 declared
languages, descendants, scripts, a 30-case inline matrix, quotes and
headings against 4.3.1 and under core in three engines;
verify-native-ax composes the new markup and compares its words with
Chromium's tree before composition, which joins words at sr-only text
natively too; verify-loaders and verify-options cover the 4.4 loader
attributes and options (quotes, coverage, headings, the notes). OUTCOMES
meanings regenerated with docs:outcomes; no new outcome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on the E2 candidate against E1's head (7e20d13), release recipe (Node 24 zlib, gzip level 9): typeset.global.js 56,860 -> 58,482 +1,622 B (2.9%) go.js 57,213 -> 59,037 +1,824 B (3.2%) react.js+shared 54,844 -> 56,150 +1,306 B (2.4%) mount-only 51,005 -> 52,262 +1,257 B (2.5%) TypesetText-only 51,157 -> 52,416 +1,259 B (2.5%) TypesetRichText-only 47,399 -> 48,562 +1,163 B (2.5%) TypesetText-no-tree-shaking 64,027 -> 65,697 +1,670 B (2.6%) smartQuotes-only 1,544 -> 1,556 +12 B (0.8%) dist/auto.js 57,228 -> 59,035 +1,807 B (3.2%) Seven entries exceeded the 2% tolerance, so budgets.json holds the measured sizes, with the raise and what it pays for recorded. verify-package-contents failed at 2,001,720 B against the 1.94 MB cap; with the last fix the package is 2,003,354 B (95 files, +66,903 B over E1): each engine bundle +4.0 to +7.1 KB, the two source maps +6.5 and +7.4 KB, eight new declaration files (7.0 KB), and the options' docs. The cap moves to 2.01 MB (4.2.0: 2.57 MB) rather than any fix being dropped. Re-run: verify-budgets 8 checks and verify-package-contents 29 checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Visual tests and screenshot tools had no way to know when the engine had
finished: mount()'s ready resolves once, before offscreen and later work,
and the React adapters expose nothing. whenSettled(options?: { timeout?:
number }) resolves { settled: true } once every mount() controller and
every React adapter host has its outcome, no composition work is queued or
timed (a resize settle, records waiting a task, a reveal waiting a frame),
no web font is loading, at two checks 50 ms apart. It resolves
{ settled: false } if work remains at the timeout (10 s by default), and
{ settled: true } at once where nothing can be composed (jsdom, happy-dom).
It is exported from typeset.us, typeset.us/react and window.Typeset.
Each controller and each document's adapter registry registers a busy test
in a list kept on globalThis under Symbol.for('typeset.us/work'), so copies
of the engine on one page share it (the CommonJS index and react bundles,
or a script-tag loader beside an npm import). It polls with a timer, not
an animation frame: a frame requested every frame left no idle period long
enough for the adapters' offscreen work, which then composed one host a
second (the first verify-react run caught that; 10 s timeouts).
Tests: verify-controller (settles after the first composition; waits out a
width change's settle and recomposition, then nothing changes for 300 ms;
{ timeout: 40 } during the settle gives false), verify-react (60 adapter
hosts, on screen and off, beside a mount() paragraph from dist/index.js,
with a web font served 400 ms late: both entries resolve settled after the
font, every host has an outcome, and two screenshots and the DOM 500 ms
apart are identical, React 18 and 19 in Chromium, WebKit and Firefox),
verify-react-node (resolves at once under jsdom and happy-dom, both
entries, ESM and CJS; the types accept it and reject a string timeout).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The auto and go loaders composed at DOMContentLoaded. On a server-rendered React page whose hydration came later, the composed paragraphs no longer matched the server HTML: React 18 and 19 reported recoverable hydration errors (#418, #425, #423), threw the server markup away and rendered the root again on the client, and the page was composed twice. The adoption review's probe showed it for every hydration after composition, and the typeset.us site tells Framer and Wix users about it. Now, when the page carries a server-rendering marker (#__next, #__NEXT_DATA__, self.__next_f, #___gatsby, [data-framer-hydrate-v2], astro-island, [data-server-rendered], or a React root already on the document, the body or a child of the body), the first composition waits for the load event, then for the framework's own signal, then for one idle callback (a 50 ms timer without requestIdleCallback), capped at 10 s after load. React's signal is the keys it sets: a __reactContainer$ root, and a __reactFiber$ key on each server-rendered target or on an ancestor below the body (which covers dangerouslySetInnerHTML content React never hydrates node by node). Astro's is no astro-island[ssr] left (client:visible and client:media islands are not waited for); Vue 2's is no [data-server-rendered] left. data-typeset-defer="hydration" waits without a marker; data-typeset-defer="none" keeps 4.3's timing. Without a marker the timing is 4.3's. whenSettled() counts the wait as work. Probe (adoption-fixes/tools/hydration-probe, Chromium, candidate auto.js, hydrateRoot 300 ms after the client script runs), SSR article under <div id="__next">: React 18.2.0: 0 recoverable errors, server node kept, 47 breaks, first composition at 358 ms after hydrateRoot at 325 ms; React 19.3.0: 0 errors, kept, 47 breaks, 384 ms after 348 ms. The same page under <div id=root> (no marker), and under #__next with data-typeset- defer="none", still gives 4.3's errors (12 recoverable errors under React 18, 1 under React 19). verify-loaders checks both loaders in three engines with React's keys simulated: nothing composed before the root or the fiber keys, composed after, whenSettled() waiting for both; the defer="none" and unmarked defer="hydration" pages compose within 400 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package's jsdelivr and unpkg fields pointed at dist/typeset.global.js, which only defines window.Typeset: a newcomer who pasted the bare https://cdn.jsdelivr.net/npm/typeset.us URL got a script that composed nothing. Both fields now point at dist/auto.js, the automatic loader, the same bytes as typeset.us/go@<version>.js. typeset.us/go is the opt-in loader (it composes only [data-typeset]), while the website's go.js is the automatic one, so the name misled. A new export, typeset.us/opt-in, points at the same file and types (dist/go.js, go.d.ts, with a typesVersions entry). typeset.us/go stays as a deprecated alias of it, removed no sooner than 5.0; nothing is removed now. capabilities.json gains whenSettled, hydration and entryPoints entries. Its websiteLoader.cdnDefault still names typeset.global.js: the E2 branch edits that line too, so it is left for the merge (changelog-E3.md says so). Tests: verify-package-contents (the CDN fields; ./opt-in and ./go resolve to the same file and types, packed), verify-react-node (import 'typeset.us/opt-in' types window.TypesetReady and window.Typeset, and the globals type whenSettled()), and verify-test-runners' attw run now names ./opt-in (CI). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify-package-contents measured 1,956,832 bytes against the 1.94 MB cap on the first E3 run, and 1,956,329 bytes (89 files) after whenSettled() moved to a timer: +19,878 over E1's 1,936,451. Per file, against the E1 candidate (7e20d13): go.js.map +3,766 B, auto.js and go.js +2,248 each (the hydration wait and whenSettled()), the shared ESM chunk +1,718, index.cjs +1,724, react.cjs +1,678, typeset.global.js.map +1,523, typeset.global.js +808, settled.d.ts and .d.cts +810 each, the four release declarations +420 together, manifest.json +285, react.js +203, and capabilities.json's whenSettled, hydration and entryPoints entries (+2.1 KB). The limit moves to 1.96 MB rather than any fix being dropped. Merged with E2 the package will need measuring again. Gzip, level 9, against E1: dist/auto.js 57,228 -> 58,115, go.js 57,213 -> 58,107, typeset.global.js 56,860 -> 57,140, react.js+shared 54,844 -> 55,215, mount-only 51,005 -> 51,088, TypesetText-only 51,157 -> 51,230, TypesetRichText-only 47,399 -> 47,487, TypesetText-no-tree-shaking 64,027 -> 64,366, smartQuotes-only 1,544 unchanged; all within verify-budgets' 2% tolerance, so budgets.json is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng idle periods After a stepped 1280 to 900 px window resize on the BWA site in Chromium, off-screen TypesetText and TypesetRichText blocks sometimes caught up late (last host mutation about 4.4 s after the resize, against about 1.6 s) and some still showed native lines 8 s on. A first A/B sample (3 of 7 route-runs with 4.3.1, 0 of 6 with 4.3.0) made it look like a 4.3.1 regression, with db22d8d's on-screen page-time hold-off as the suspect. It is not one: - the same probe in one bisect flagged 3 of 16 route-runs with the 4.3.0 build, 1 of 24 with 4.3.1's, 4 of 16 with cea3a10 and 1 of 16 with db22d8d; - instrumented builds (db22d8d, and cea3a10 before it) held no block in any of 68 route-runs, flagged or not (0 HOLD and 0 held events), so the hold-off and its settle never acted; - in all 10 flagged instrumented runs Chromium had stopped giving the page idle periods once the probe's frames stopped, and every one of the registry's idle passes fired on its timeout (clean runs: 2, both while the probe's frames ran); with an idle monitor, those runs got 0 or 1 idle callbacks between 1.6 and 5 s after the resize, the clean ones 65 to 73. The registry waits up to IDLE_TIMEOUT_MS (1 s) for a long idle period, and a callback that fires on its timeout composes one block, so a page given no idle time composed its off-screen blocks one a second. Now, once an idle period has come, the callback also times out STARVED_IDLE_MS (50 ms) of page time after the last one. With none since, or none at all by the 1 s timeout, the browser has stopped giving them: the remaining blocks compose in frames within FRAME_BUDGET_MS, as nearby blocks do, until an idle period comes again. A timeout that fires more than 20 ms late (a long task held the thread) waits on instead, and while short idle periods keep coming a block still composes only on the 1 s timeout, so a screen push still spends no animation frame on far text. The continuous-change hold-off (dbd30a3, db22d8d) is unchanged, so the storm fix (D1) and the separate-change fix (R1) stand. verify-scheduler gains a check: 48 TypesetText blocks, their container narrowed from 420 to 380 px with frames running for 600 ms, then frames and idle periods stop (every idle callback fires on its timeout). 4.3.1: the 23 off-screen blocks left native took 44 s in Chromium, 19 still native at 5 s; this change: 199 ms in Chromium, 173 ms in Firefox (WebKit has no requestIdleCallback; the registry uses a timer there). Tested once each, M2 Pro: tsc (tsconfig.json, tsconfig.scripts.json); verify-release --only recompose-storms,settle,scheduler,reflow-triggers,react 6 of 6 suites pass, 910 checks (storms 53, reflow-triggers 46, scheduler 99, settle 420, react 231, react-node 61). BWA probe (candidate packed into a copy of the 4.3.0 site build, Chromium, routes / and /team, 1280 to 900): 3 runs, 0 of 6 route-runs flagged, last resize mutation 1.52 to 1.66 s. The WebKit verify-reflow-triggers text-spacing outlier (538 ms against a 400 ms limit, one hosted PR run of 244308d) does not share this cause: that check drives mount(), not the React registry, and WebKit has no requestIdleCallback. Locally 4.3.0 and 4.3.1 both measure 1 to 2 ms (16 runs, 6 under 8 CPU burners). Evidence: ~/claude-handoff/2026-09-29/typeset-4.3.2-bisect/runs/ (the instrumented timelines) and ~/claude-handoff/2026-09-29/typeset-4.3.2-fix/ (suite run, the check's scenario on 4.3.1 and on this change, probe runs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ult file E3 branched from E1's 7e20d13, beside E2. The only conflict was the unpacked-package cap in verify-package-contents; both agents' notes are kept and the cap is set from the merged package once it is measured. capabilities.json's websiteLoader.cdnDefault now says bare CDN URLs serve dist/auto.js, as E3's entryPoints.cdnDefault already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…port window The docs branched from E1's 7e20d13, beside E2 and E3. The only conflict was the unpacked-package cap in verify-package-contents again; the docs' note joins E2's and E3's and the cap is set from the merged package once it is measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e22ce25 composes off-screen React blocks in frames once the browser stops giving idle periods. It merged cleanly beside E3's whenSettled() registration in adapter-registry.ts: the registry's busy check reads pending hosts, which the starved path drains, so whenSettled() waits for that work too. The "4.3.2 - Unreleased" section comes along and stays below 4.4.0's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d install lines, demo resizes and the CI trim origin/adoption/site at 27b9d95, which master merged as PR #12, including the CI trim (pull requests and master only, weekly release-artifact runs, monthly Dependabot). It merged without conflicts: the site owns verify-smart-quotes' prose file list and verify-docs' CURRENT list (both drop docs/show-hn.md), E2 owns the rest of verify-smart-quotes, and the site's ci.yml check in verify-release-trust sits apart from the docs' support-window rows. The site copy still describes 4.3.1; its 4.4 wording (Framer and Wix under the hydration wait, the defaults under each snippet) is left for the docs pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section is assembled from the E1, E2 and E3 notes, the docs and site
branches and the 4.3.2 fix it carries. It states the open coverage-default
decision (O1): the build defaults to coverage: 'extended', and the cut
either keeps that with the STABILITY.md sentence that allows it or sets
COVERAGE_DEFAULT to 'core'.
- Rendering changes: E2's golden run against the published 4.3.1 on
coverage: 'core', with the coverage corpus composed again on the default.
Existing corpus 0 of 3,932 cells changed per engine (style-moved-only
2,117 / 2,104 / 2,119); coverage corpus 104 of 376 under 'core', all
defect fixes, and 280 of 376 on the default, 0 unexplained; coverage
cells composed 55/54/63 (4.3.1), 103/102/111 ('core'), 278/276/286
(default) in Chromium, WebKit and Firefox.
- Sizes: the published 4.3.1 dist against this candidate, gzip -9 (go.js
56,467 -> 59,887, auto.js 56,476 -> 59,880, typeset.global.js 56,122 ->
58,767, mount-only 50,361 -> 52,356, TypesetText-only 50,489 -> 52,629).
- The package README, STABILITY.md and llms.txt name 4.4.0 with the
integrity placeholder release-cut fills, and the README's download sizes
are 4.4.0's. The README also stops saying what 4.4 changed: the script
tag curls quotes only in declared English, typeset.us/opt-in is the
opt-in loader's name, data-typeset-defer has a row, and other
Latin-script languages compose with neutral preferences.
- SUPPORT.md names the new development warnings (copy, headings, coverage
and the three smartQuotes values), so verify-options' known failure is
removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.4.0 composes other Latin-script languages and more inline markup by default (coverage: 'extended'). STABILITY.md as 4.3 wrote it allows a change to default output only for a verified defect or behind an option that is off by default, so this sentence is what makes that default allowed: each addition is listed with its count, never changes a paragraph 4.3.1 composed (the golden run on coverage: 'core' shows 0 changed cells there), and coverage: 'core' turns it off. This is the maintainer's decision O1, open until the cut. It is its own commit so that declining it is a revert of this commit plus setting COVERAGE_DEFAULT in src/lib/v4/coverage.ts to 'core'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify-package-contents on the merged candidate (b913c19) measured 2,034,092 B in 97 files, under the 2.04 MB cap set at the merges (E2 2.01 MB, plus E3's 19,878 B and the docs' 8,046 B, each measured on E1). The cap comment and the CHANGELOG's size note now give the measured figure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CHANGELOG's 4.3.2 section makes verify-docs require the shipped docs to name the release they describe: install lines, pins (with the integrity placeholder the cut fills), release links and llms.txt. verify-docs: 43 of 43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kdown /privacy says the fetch proxy keeps a client's IP address briefly to limit abuse; the map only pruned itself past 10,000 keys, so addresses stayed until the instance restarted. Expired keys are now swept at most once a minute, and /privacy says one to two minutes. OWNER-ACTIONS records that npm publishing allows no tokens and the publish tokens are revoked (2026-09-29), and RELEASING says CI now runs on the pull request, not on release-branch pushes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.yml's registry check ran 2 s after 4.3.1's publish, while npm was still processing it, and failed the job before the GitHub Release. It now waits up to 10 minutes for the version and 5 for its attestation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the default MIGRATION.md is now "Moving to 4.4.0": from 4.3 it covers the loader's en-declared quote default and data-typeset-smart-quotes="en", coverage 'extended' and coverage: 'core', the run budget and its outcome, the hydration wait and data-typeset-defer, whenSettled(), ./opt-in with the ./go alias, the jsDelivr and unpkg default file, the audit's new review items, marker styles and sizes; the 4.2.0 steps stay below it. SUPPORT.md and for-agents.md describe 4.4.0 throughout, with every new option, attribute, outcome and review item, and their language claims qualified by coverage (4.3.1's "only en, fr, de, es" and "mixed-language blocks stay native" are false under the default). The three limitations 4.3.1 said were planned for 4.4 are marked as not addressed. The package README no longer says a minor release changes default rendering only for a verified defect, which the O1 sentence in STABILITY.md contradicts. It gains a Visual tests section (whenSettled() in Playwright and a Storybook play function), an audit --apply preview before installing, the overflow-x: clip and priority="sync" workarounds, spacing: false as the low-element mode, and the hosted loader's lack of an uptime guarantee. OUTCOMES.md names extended coverage through COVERAGE_DEFAULT, so it follows the default if O1 is declined, and the React adapters' coverage JSDoc defers to Options.coverage. The CHANGELOG's O1 note lists every file to change if it is declined. Site copy for 4.4: a defaults line under each install snippet, separate Framer and Wix hydration notes (Wix: add data-typeset-defer="hydration"), FAQ answers on page language, hydration errors, user-generated text, find-in-page, translation and reader views, and a languages row in the homepage's "Do I need it?" list. The docs add 32,975 B of package text; the unpacked-package cap goes from 2.04 MB to 2.08 MB (measured 2,067,743 B). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…load once hydrated The loaders' hydration wait (hydration.ts) began only at the load event and capped itself at 10 s after it, so a marked page whose load came late (an image, an iframe or a stalled request) waited for load however long it took, and one whose framework never hydrated composed 10 s after that. Now the cap runs from afterHydration()'s call (DOMContentLoaded), the load event included, and the framework's own signal ends the wait before load: a marked page, or one with a React root, whose server-rendered targets are hydrated goes on to its idle callback at once. Only data-typeset-defer="hydration" with no React root yet still waits for the load event, where a framework's scripts may not have run. The poll stops at the deadline rather than up to 50 ms past it. Found on the way: mount() waits for document.fonts.ready, which Chromium and WebKit resolve only at the load event (Firefox before it), so in those two the first composition still comes no earlier than load, as in 4.3. That wait is mount()'s and unchanged; the CHANGELOG's known limitation and capabilities.json say so. verify-loaders gains a variant whose load event is held (the page served from a route, an image held until 1.5 s; a page set with setContent() reports load in Chromium with the image pending) and a page that never hydrates, its load at 11 s: it must compose by 12 s in every engine (capped from load it composed at 21 s), and Firefox must compose the hydrated page before load. Tested once each, M2 Pro: tsc (tsconfig.json, tsconfig.scripts.json); verify-release --only loaders 441 checks pass; --only docs 43 pass. React 19 SSR probe (Chromium, candidate auto.js, <div id="__next">, hydrateRoot 300 ms after the client script): hydrated 334 ms, composed 403 ms, 0 recoverable errors, server node kept, 47 breaks; with a 6 s image holding load, the hydration wait ended at 350 ms (its idle callback ran then, its timeout 9,663 ms: the cap counted from the loader's start), fonts.ready came at load (6,011 ms) and the composition at 6,060 ms, 0 errors. Evidence: ~/claude-handoff/2026-09-29/review-engine-fixes/ (loaders-44, hydration-probe/probe-out.json, fonts-ready-vs-load.mjs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-cut --version 4.3.2 on the calibration machine (Apple M2 Pro, official node 24.8.0, npm 11.6.0): the staged run passed 45 of 45 suites and 13,641 checks with every speed-calibrated check enforced, and the runtime budgets. Tarball public/releases/4.3.2/typeset.us-4.3.2.tgz, sha1 d0c06dc51909b0bb38b0a9c880252c6493901cb6. No earlier release's files changed. The CHANGELOG gains the measured frame-time trade-off of the catch-up as a known limitation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision O1 is declined. STABILITY.md keeps 4.3's rule that a minor release changes default rendering only to fix a verified defect, so its O1 sentence (b913c19) is reverted and COVERAGE_DEFAULT is 'core'. Existing sites keep 4.3's output apart from the defect fixes; other Latin-script languages and more inline markup compose with coverage: 'extended', data-typeset-coverage="extended" or the adapters' coverage prop. The option's JSDoc, the package README (languages row, the install-line and Stability sentences back to the plain rule, the options and loader rows, line-end preferences), capabilities.json, SUPPORT.md, MIGRATION.md step 4, for-agents.md and llms.txt now describe 'core' as the default. OUTCOMES.md's native:language and native:mixed-language rows follow COVERAGE_DEFAULT like the rich-text rows do, and npm run docs:outcomes regenerated both copies. The CHANGELOG states the decision at the top of 4.4.0, lists the coverage additions under Added as opt-in and takes them out of Rendering changes, whose table is now the default's: against the published 4.3.1, 0 of 3,932 existing cells change in each engine, and on the coverage corpus 104 of 376 do, all class B (long runs 40, tag spellings 24, Greek letters 24, quotes 16); 55/103, 54/102 and 63/111 composed. verify-coverage and verify-native-ax's coverage fixture pass coverage: 'extended' where they test it, and verify-coverage checks that the language and inline cases compose with no coverage option exactly as under 'core'. verify-loaders expects a lang="pt" page native by default. verify-golden reads the default from coverage.ts, so a plain run holds the class C cells to 4.3.1's output and skips the footnote-sup trade-off, which occurs only under 'extended'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and what 4.4 shipped Known limitation, in the 4.4.0 and 4.3.2 CHANGELOG sections and in SUPPORT.md: while a script fills every frame (Chromium, 4x CPU slowdown, 120 Hz), the React adapters' off-screen catch-up takes up to 12 ms of each frame for about 1.3 s: 92 fps falls to 66 to 75, frames over 20 ms go from 2 or 3 to 31 to 39, with no new long tasks. 4.3.1 kept those frames but left 21 of 23 off-screen blocks stale at 3 s. (Measured by the engine agent.) SUPPORT.md's hydration-wait limitation now says that mount() waits for document.fonts.ready, which Chromium and WebKit resolve only at the load event, so the first composition comes no earlier than load there; this dates from 4.3. Sizes re-measured from a fresh candidate build (gzip -9, the published 4.3.1 column reproduced exactly by the same method): go.js 59,941, auto.js 59,935, typeset.global.js 58,770, mount only 52,357, typeset() only 45,961, analyzeBreaks() only 14,916; whenSettled() alone 1,429. The staged package packs to 2,068,719 B unpacked, 97 files (cap 2.08 MB). ROADMAP.md: the 4.4 section says what shipped (the adoption fixes, with coverage: 'extended' opt-in), and what it planned but did not ship moves to "Later in 4.x"; whether 'extended' becomes the default joins 5.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes the 4.3.2 release files, version and dated CHANGELOG heading; keeps 4.4.0's shipped docs (which name 4.4.0 with the integrity placeholder) and one Known limitations note in the 4.3.2 section. verify-docs 43 of 43, verify-ledger and tsc pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-cut --version 4.4.0 on the calibration machine (Apple M2 Pro, official node 24.8.0, npm 11.6.0): the staged run passed 46 of 46 suites and 14,167 checks with every speed-calibrated check enforced, and the runtime budgets. No earlier release's files changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…romptly again This reverts commit a5ba256. Since 4.3.2's idle-period change (e22ce25), hosted runs of verify-react Chromium "a real ancestor style change still recomposes every block" left the one block more than a viewport from the screen (r5) unwritten through a 300 ms quiet window: 2 of 3 release verify runs of 4.3.2 and the 4.4.0 PR run (CI 36643568020), after about 36 hosted runs without a failure. The same PR run failed verify-settle Firefox "TypesetRichText at 375px resized to 335px" (217 mutations 2 to 5 s after the resize: an off-screen grid-auto-pair block still stale at 2 s). The cut's dist, public/releases/4.4.0, pins, aliases, sri.json, release.json and ledger entry were built before the fix. 4.4.0 was never tagged or published, so nothing immutable is withdrawn; release-cut cuts 4.4.0 again from the fixed tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he 1 s timeout Since 4.3.2 (e22ce25) the React registry also timed each idle callback out 50 ms of page time after the last idle period, and a timeout that fired more than 20 ms late waited on instead of composing. On hosted runners verify-react Chromium "a real ancestor style change still recomposes every block" then left r5, the one block more than a viewport from the screen, unwritten through a 300 ms quiet window: 2 of 3 release verify runs of 4.3.2 and the 4.4.0 PR run (CI 36643568020), against none in about 36 hosted runs before. Not reproduced on the M2 Pro: 0 misses in 70 throttled restyles (CDP CPU throttling 4x, 6x and 10x; viewports 900, 700 and 500 px high; React 18 and 19; 14 CPU burners; 4 pages in parallel) with the 4.4.0 build, and none with 4.3.1's. An instrumented 4.4.0 build shows the 4.3.2 paths the restyle takes: the first callback after a quiet spell asks for a 1 ms timeout (the last idle period's page time was never cleared), fires 89 to 130 ms late behind the on-screen frame and waits on, then 50 ms timeouts; locally r5 still composes in the first long idle period once frames stop, as with 4.3.1. The late-timeout wait-on is the path that can hold a block where 4.3.1 composed it: with no idle periods and every timeout 30 ms late (an emulated busy runner), 4.4.0 waited on every 31 ms and r5 was still unwritten 8 s on; 4.3.1 and this change wrote it at 1.05 s. Starved-mode frames cannot hold r5 (they compose it). Now requestIdle() asks exactly what 4.3.1 asked (the rest of IDLE_TIMEOUT_MS from the first request), a long idle period composes and a short one waits, and a callback that fires on its timeout composes one block, as in 4.3.1. Only when that timeout comes with no idle period of any length in the last STARVED_IDLE_MS of page time is the browser marked starved, and frames then compose the remaining off-screen blocks after on-screen and nearby ones, within FRAME_BUDGET_MS, until a long idle period comes or nothing is left; short idle periods (the ends of those frames) no longer end it. The idle-period record and the flag reset when new work arrives after none, so a stale value no longer shortens the next wait. verify-settle Firefox "TypesetRichText at 375px resized to 335px" in the same PR run has a different cause, not changed here: the page samples every animation frame, so no long idle period comes, and the off-screen grid-auto and grid-auto-pair blocks the scroll-through leaves compose one per 1 s timeout, the second 1.94 s after the scroll-through with 4.3.1 too (Chromium 1940 to 1952 ms, Firefox 1941 ms), 60 ms inside the check's 2 s wait. CHANGELOG 4.4.0 Fixed describes the correction; its known limitation and SUPPORT.md say the frames are spent only once a callback has waited 1 s. Tested once each, M2 Pro: tsc (tsconfig.json, tsconfig.scripts.json); verify-release --only react,settle,scheduler,recompose-storms,reflow-triggers 6 of 6 suites pass, 939 checks (storms 53, reflow-triggers 46, scheduler 99, settle 420, react 243, react-node 78); verify-docs 43. Starvation scenario (verify-scheduler's check alone): 0.51 s Chromium, 0.54 s Firefox (4.4.0 build 0.17 and 0.19 s, 4.3.1 44 s). Throttled restyle, 6x, React 18, 8 rounds: 4.4.0 build 0 misses, last host 279 to 330 ms; this change 0 misses, 283 to 330 ms. Evidence: ~/claude-handoff/2026-09-29/typeset-4.4.0-idle-fix/ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It measured 51.1 ms against 50 ms once, in the v4.3.2 release run 36644238293 (attempt 2), with the three browsers sharing the runner. The linear-time bound stays enforced by the WebKit and Firefox browser checks and on the calibration machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-cut --version 4.4.0 on the calibration machine (Apple M2 Pro, official node 24.8.0, npm 11.6.0), with off-screen React blocks following a restyle as 4.3.1 did: the staged run passed 46 of 46 suites and 14,167 checks with every speed-calibrated check enforced, and the runtime budgets. Tarball sha1 2be4af91dc1497a682bd9f1fb4965e01091b4d1a. No earlier release's files changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.3.2 was cut and tagged (v4.3.2, fc7f84e) but its release verification failed in 4 of 5 hosted runs on verify-react's "a real ancestor style change still recomposes every block": the idle-starvation change let one off-screen React block recompose after the check's 300 ms quiet window. 4.4.0 carries the corrected change, so 4.3.2 is not published. scripts/v4/withdrawn.json records it; verify-ledger --network skips its registry cross-check (the ledger stays append-only and every archived file is still checked), and the CHANGELOG says so at the top of the 4.3.2 section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Not a release yet: this is the change set for 4.4.0, before the cut. It carries the adoption fixes and merges the 4.3.2 cut.
Defaults stay as 4.3. Expanded coverage (more Latin-script languages, footnote superscripts, some inline tags) is opt-in via
coverage: 'extended'. On the existing corpus, 0 of 3,932 golden cells change against 4.3.1 in every engine.What's in it
native:run-budget), which prevents the Safari freeze.copy: falseoption.langmisspellings such asen_US.headings: false.data-typeset-defer.whenSettled()../opt-inexport name, with./gokept as an alias.dist/auto.js.Checks were run locally once each under the owner's lean-testing rule; see the CHANGELOG's Development section. This PR's one CI run is the full-suite check.
🤖 Generated with Claude Code