docs/gaps.md now lists the same cases as docs/architecture.md and
reuse::plain. A doc comment on rejoin says that a run style an edit uses
for the first time stops the reuse until the old composition reaches the
same style at the same index.
Every edit of a story composed it from its first paragraph, so a typed
character in a long threaded story cost as much as laying out the whole
story: 456 ms in a story threaded over 300 pages.
A composition that lays every paragraph once keeps its state before each
paragraph. After an edit, composing starts one paragraph above the first
changed one, in the kept state and with the lines above copied. It stops
at the first unchanged paragraph it reaches in the state kept for that
paragraph, and copies the lines from there on with their story offsets
moved by the length of the edit.
- The cache does this when only the story differs from its entry.
- Composed whole as before: stories with footnotes, endnotes, tables,
anchored objects, named lists, tracked deletions, balanced columns,
vertical justification other than Top, column rules, type on a path,
spanning or split paragraphs, paragraph borders or shading, and a
layout that goes back over a paragraph (keep options).
- An edit that moves lines into other frames is composed to the end of
the story.
In a 240-paragraph story a typed character composes 3 paragraphs
(240 before).
Docs: docs/architecture.md, docs/gaps.md (gap 20), ROADMAP.
Refs #2298
* fix(engine): index lookups that were quadratic in a document's styles, tags, swatches and runs
* address review: first swatch per name in book sync; bound spelling input before collecting
* address review: keep the last source style per name in book sync; bound GREP replacement bytes while generating them
* address review: write GREP replacements into a byte-bounded buffer; gate keep_last test off wasm
* merge main into fix/sec-engine-linear-lookups
* address review: make the suggestion length-cap test deterministic
* fix(export): leave out text hidden by inherited conditions or deletion in every exporter
* address review: skip hidden frames in page text and hidden article items; first visible paragraph as nav label; read table anchor's own format
* address review: skip nonprinting frames and non-printable layers in fixed-layout page text
* Apply review fixes
- crates/epub/src/html.rs
- crates/engine/src/cmd/export/formats.rs
* Apply review fixes
- crates/epub/src/html.rs
- crates/engine/src/cmd/export/formats.rs
* address review: fixed-layout page text skips frames in inactive object states
* address review: place a threaded story at its first shown frame; skip it only when every frame is hidden
---------
Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
* format: bytes a failed entry read inflated count against the archive budget
* address review: mutate the payload bytes explicitly in the checksum test
* address review: charge a failed read to the budget it belongs to (stored vs deflated)
* idml: story runs and marker offsets found once, object-style overlay by position, embedded image budget
* address review: saturating embedded-bytes counter
* address review: IDML export past the embed limit succeeds and warns for each image written as a link
* address review: an unlinked image is always embedded, only linked ones fall back to their link
* xtask: refuse a corpus archive that expands past the cap before extracting it
* address review: a within-cap answer needs tar to exit successfully
* Apply review fixes
- xtask/src/main.rs
---------
Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
Follows #2645. Its test said a misspelled key used to apply Align Left.
Only empty params and a bare string did; the parameter check already
refused a misspelled key, and the value parser refused an unknown
value. Each case now asserts the refusal message it gets.
type.align with no `align` key applied Align Left, so a call that
misspelled the key, or passed a bare string, changed the paragraphs to
left alignment instead of failing. The command now returns an error,
leaves the text unchanged and records no undo step. A named value still
applies, and `align: null` still removes the override.
* Render: a blur size is clamped to 1,000,000 pt before the filter planner squares it
* address review: bound effect extents and sigma before spread, padding and reach arithmetic
release.yml also runs nightly on main with craft-common-actions' dry-run: every release build
except macOS, with nothing signed, pushed or drafted. Breakage that only the release build hits
(riscv64, Flatpak, FreeBSD, packaging) then shows up the next morning instead of on release
day, as it did on 11 Nov 2026.
* fix: index style, font, class and layer name lookups in exporters and importers
* Apply review fixes
- crates/epub/src/css.rs
---------
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
* fix(engine): whole-file reads take regular files only, up to the format limit
* Apply review fixes
- crates/engine/src/io.rs
* Apply review fixes
- crates/engine/src/io.rs
* Fix failing CI check
- crates/engine/src/io.rs
* address review: refuse to save an encoded document longer than the open limit
* address review: open files for reading without blocking (FIFOs, stream-backed pseudo-files)
---------
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
* fix(engine): package fonts from their loaded bytes under the read roots
* address review: a font's own file is neither overwritten nor packaged when it changed since loading
* Apply review fixes
- crates/engine/src/cmd/package.rs
* address review: compare packaged font file in fixed-size chunks
---------
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
* Strokes: cap the points a wavy or hashed stroke places along a path
* address review: zero-length quadratic contributes no length in stroke samples
* Fix failing CI check
- crates/geom/src/stroke_style.rs
* Fix failing CI check
- crates/geom/src/stroke_style.rs
---------
Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
* engine: Erase/Smooth look at each distinct drag sample once, thinned past 4096
* Apply review fixes
- crates/engine/src/cmd/anchors.rs
* address review: decide whether a drag touched the path on every distinct sample, then thin
---------
Co-authored-by: autocraft-bot[bot] <354215646+autocraft-bot[bot]@users.noreply.github.com>
* doc: appending to a story extends the last run, no scans
* Apply review fixes
- crates/doc/src/story.rs
---------
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
* Tables: commands given a table id run with nothing in a table selected
Table commands whose params list `story?` and `table?` (table.insertRow,
table.deleteColumn, table.options, the Convert Rows commands and the
others) were refused with "place the insertion point in a table" when the
call named the table but the selection was not in one. A call that passes
`table` now acts on that table whatever is selected. A table that is not
in the document, or not in the given `story`, is refused, and a `story`
that is not a non-negative integer is an error.
The menus are unchanged: with nothing in a table selected, the table
items stay disabled, and calls that name no table are still refused.
* Tables: a bad story is refused whether or not a table is selected
A call that names a table with a story that is not a non-negative integer (a string, a negative number, a fraction or null) was refused only when nothing in a table was selected. With a table selected, the command ignored the story and acted on the named table. Both paths now read story with one helper and refuse it the same way.
* Tables: an edit to a named table keeps a caret in footnote text
With the caret in footnote text, a table command that names a table
runs, and the edit cleared the caret's footnote address, so the next
typing went into the story. clamp_selection now leaves footnote
addresses alone, as in_cell does.
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.