# 2.0.0 — rebuilt engine (2026-10-22)

The from-scratch rebuild (branch `rebuild/engine`, analysis in `rebuild/00-REBUILD_ANALYSIS.md`,
benchmark in `rebuild/07-BENCHMARK-2026-10-21.md`). Same flags, same JSON shapes, same exit codes;
every mode was gated against the 1.50.2 golden parity set (`tools/parity/`): 65-run battery with
identical rc, every PDF identical in render and decompressed Illustrator payload, every PNG/TIF
pixel-identical; 34 compile fixtures identical. Clean on Java 8 (covariant scan 0, preflight output
byte-identical to 1.50.2 on 1.8.0_452) and Temurin 23.

## What changed inside

- One render per page feeds every plate and every analyzer (PageRaster fan-out); thin-line
  labelling is run-length union-find; the contour analyzer reuses the raster analyzer's planes.
- The Illustrator private-data payload is a streaming model (AiPayload): --list-text,
  --bind-variables, --derivatives, --derivative-preview and --compile no longer inflate and
  re-scan a 100+ MB buffer per step.
- Flow layout (--create text blocks, preview PNG) draws TrueType outlines directly; Ghostscript
  is no longer invoked anywhere in the shared pipeline.
- The AmazonS3 client is built on first S3 use, not at `--aws=` parse. Parse-time `--aws` errors
  (missing file, no [default], bad region) keep their exit codes.
- Process-wide state lives in a Job context; `--threads=N` (default: all cores) bounds every
  fan-out; stages shorter than ~400 ms of work stay serial.
- Illustrator private data is written at zstd level 9 (was 19): files ~0.45 MB larger per
  12.7 MB payload, decompressed bytes identical, decoder-neutral.

## Visible to callers (no parser impact, listed for completeness)

1. `--derivative-preview` JSON no longer emits the "Illustrator round-trip data rebuilt for N text
   objects" warning line.
2. Flow-layout glyph outlines are exact paths instead of Ghostscript's 0.1 pt grid (a few percent
   of edge pixels at 300 dpi); preview PNG colour comes from the fork's own conversion.
3. Derivative / compile / bind PDFs are slightly larger (zstd 9).

## Measured (dev VM, serial, 768 MB heap, wall seconds, 1.50.2 → 2.0.0)

| mode | 1.50.2 | 2.0.0 |
|---|---|---|
| --list-spot-colors | 2.6 | 1.1 |
| --preflight selective-eats | 28.3 | 6.7 |
| --thin-line selective-eats | 32.0 | 9.4 |
| --preflight coca-cola | 3.6 | 1.8 |
| --derivatives BR1X007 | 40.4 | 3.9 |
| --derivative-preview BR1X007 | 40.1 | 2.9 |
| --list-text BR1X007 | 4.4 | 1.1 |
| --compile BR1X007 | 10.8 | 3.7 |
| --backers 45725 | 7.3 | 3.3 |
| --create flow | 2.4 | 0.8 |
| --vectorize selective / tanimura | 6.3 / 3.8 | 3.9 / 2.5 |

Parity battery, whole set: 578 s → 98 s.

# 2.1.0 — `--serve` resident preview server (2026-10-22)

`--serve=[HOST:]PORT` keeps one JVM resident and answers `POST /preview` with the PNG
`--derivative-preview` would have written (manual chapter 20). The one-shot mode's body was
split into `derivatives/PreviewRun` (load package → pick dataset → choose engine → render →
PNG) and both the one-shot and the server call that one code path, so a server preview is the
one-shot preview by construction: gate (a) one-shot PNG md5s unchanged against the 2.0.0 jar on
preview_ai / preview_lab / preview_br / preview_br with `--set`; gate (b) server PNG md5 equal
to the one-shot for the same four cases, json round trip identical, warm repeat identical.
Existing flags, JSON shapes and exit codes untouched (full battery: the 8 known JSON diffs
only, rc identical, covariant scan 0).

- Templates are cached (`--cache=N`, LRU, default 16) keyed by canonical package path plus a
  size/mtime fingerprint; a rewritten file at the same path is rebuilt on the next request.
- Every `/preview` answer carries the one-shot `--usage` work counts (`usage` object in json,
  `X-Epic-Usage` header on PNG), counted per request on the request's own thread.
- `/health` reports `heap_used_mb`, `heap_max_mb`, `rss_mb`; `/stats` per-template counters;
  `/evict`; `/shutdown` from loopback peers only. Non-loopback bind needs `--serve-allow-remote`.
- New internal tool `usage.Usage.start/exit/producedBytes/document` scopes a usage document to a
  request without a sidecar file; the one-shot sidecar path is unchanged.
- 2.1.1: `/preview` accepts `recolor` (the `--recolor` spec string verbatim). Recolor previews,
  one-shot and server alike, now swap the in-memory working page right before the render
  instead of generating, saving, recoloring and reopening the derivative PDF pair: a warm
  BR1X007 recolor preview in `--serve` measured 14–22 s on that path and ~0.55 s on this one,
  PNG byte-identical to 2.0.0 (envelope and live-text fixtures, two specs each), error
  reasons identical. Visible deltas: a recolor preview no longer emits the "Illustrator
  round-trip data rebuilt" warning (plain previews dropped it in 2.0.0), and a spec only the
  production derivative would reject no longer fails a preview. The swap resets the
  document's resource cache: colorspace objects built before it keep the old tint transform,
  and without the reset the recolored PNG was identical to the unrecolored one.
- 2.1.2: `--fonts-dir` and `--strict-fonts` given to `--serve` now apply to every request.
  Through 2.1.1 each request bound a fresh job that dropped both, so a server started with an
  empty `--fonts-dir` still answered 200 for a compile-engine package whose font was named
  without a file path, pulling the face from `/datahandler/fonts`; the one-shot with the same
  flags fails (rc 2). Measured on a lab package with a name-only `Bungee-Regular` ref: lax,
  empty font directory, and strict with an empty directory all now give the one-shot's status
  and reason, and with the face in `--fonts-dir` the server PNG equals the one-shot PNG. BR1X007
  PNG unchanged under strict and lax servers. `/health` and the ready line report
  `strict_fonts` and `fonts_dir`. Font policy stays server-wide by design (manual chapter 20).

# 2.2.0 — image variables (2026-10-22)

Compile-engine derivatives swap images the way they swap text (manual chapter 12): a variable
whose `ha:placeholder` is an artwork layer id (or whose `ha:bind` names one) takes a
package-relative PDF/PNG/JPEG/TIFF, fitted uniformly and centered in the layer's box. Bad
values fail only their dataset; paths outside the package are refused. New `--list-slots`
reports text and image slots with boxes in inches; `--usage` gains `work.image_swaps`.
Text binding is unchanged for every existing package (the name fallback never binds artwork;
text wins a text/artwork collision). Measured on a hand-built fixture (11 datasets: PDF, PNG
with alpha, RGB/CMYK JPEG, TIFF, a PNG named .jpg, empty, missing, `..`, absolute, symlink
escape, text file, corrupt PNG, second-image failure): all ok/failed as specified,
`image_swaps` 7; `--serve` PNG byte-identical to the one-shot; `--recolor` leaves raster
pixels identical.

# 2.3.0 — fit, anchor, fixed artboard (2026-10-22)

Compile-engine derivatives (manual chapter 12): `ha:fit` (contain | shrink | cover) and
`ha:anchor` (left | center | right) on a `<variable>` or one `<ha:bind>`; `cover` fills an
image frame and clips to it. `design.derivative_artboard: "fixed"` keeps the design artboard
for both outputs instead of refitting to ink. `--list-slots` gains `artboard` and per-slot
`fit` / `anchor`. Packages without the new attributes derive as in 2.2.0: the 2.2.0 image
fixture's previews are pixel-identical (contain/center now draws through a content box inside
the unchanged layer box). Ink fitting (`BoundingBoxFinder`) gained an opt-in clip-aware mode,
used only on pages carrying a cover image; every other caller measures as before.

- 2.3.1: `--usage` on `--imposition` reports what it wrote: `work.sheets` (one per plan, one
  per flow-layout block), `trim_sq_in` summed over the sheets, `trim_in` the first sheet, new
  `work.placed_sq_in` (placed art area), and the plan's `destination` in `files_out` /
  `pages_out` / `bytes_out` (through 2.3.0 all were 0 and the plan itself was counted when it
  was written just before the call). Every mode: an input given by a relative path is no
  longer counted twice for an in-place edit (2.3.0 reported 2 files and double the bytes for
  `--swap-spot-colors x.pdf`), and an input counts as an output only when the run changed its
  mtime. `mode` skips every global flag: through 2.3.0 a command that began `--usage=u.json`
  (or `--threads=`, `--strict-fonts`, `--fonts-dir=`) recorded mode `usage` / `threads` / ….

## Measured (dev VM, 6 cores, BR1X007 Illustrator master, 72 ppi, 553×934 RGBA, 2 GB heap)

Client wall time, 3 warm-ups then 20 requests; "burst" is 4 concurrent clients.

| run | p50 ms | p95 ms | note |
|---|---|---|---|
| one-shot `--derivative-preview` (new jar) | 3530–6170 wall | — | user 5.0–8.9 s, RSS ~640 MB |
| resident, threads=6, serial | 525 | 869 | first 20 after warm-up (max 1744, JIT) |
| resident, threads=6, serial, repeat | 486 | 558 | |
| resident, threads=6, burst 4×10 | 1319 | 1994 | 2.85 previews/s, 40 identical PNGs, 0 errors |
| resident, threads=1, serial | 493 | 727 | |
| resident, threads=1, burst 4×5 | 2043 | 2446 | 1.90 previews/s (queued on one worker) |
| resident, threads=1, 144 ppi, serial | 640 | 684 | 474 KB PNG |

Cold first request per template (unzip + sidecar + engine): ~2.6 s for BR1X007. Process after
100 requests on three templates: heap used 228 MB, RSS 847 MB.

With the master page's content tokens parsed once per engine (`AiEnvelopeEngine.pageTokens`,
same commit series; every PNG above byte-identical before and after):

| run | before p50 / p95 | after p50 / p95 |
|---|---|---|
| threads=6, serial | 525 / 869 | 355 / 507 |
| threads=6, serial, repeat | 486 / 558 | 401 / 429 |
| threads=6, burst 4×10 | 1319 / 1994 (2.85/s) | 1274 / 1865 (2.95/s) |
| threads=1, serial | 493 / 727 | 383 / 534 |
| threads=1, burst 4×5 | 2043 / 2446 (1.90/s) | 1536 / 1700 (2.60/s) |
| threads=1, 144 ppi | 640 / 684 | 540 / 818 |

(The two tables above measure `--serve`, 2.1.0.)

# 2.4.0 — `--pack` auto-imposition planning (2026-10-22)

New standalone mode (manual chapter 21), asked for by web2print (`PACK-SPEC.md` + 8 amendments):
a request of items and sheets in, ganged runs out (sheet, press sheets, inks/plates, every
placement, a guillotine cut plan on trim lines, per-component cost, price and margin) under
`fewest_sheets`, `least_waste`, `fewest_overs` and `best_margin`, plus `unplaced[]` with reasons.
Exit 0/1/2/3 with stdout empty on non-zero and one brace-free stderr line naming the bad JSON
path. Deterministic for a request and seed when the budget is not hit; the budget is split
across strategies and an interrupted search keeps its best plan (`budget_hit: true`). Usage
sidecar: mode `pack` plus `work.items` / `work.candidates` / `work.runs`. Layout only: nothing
is rendered, and no existing mode's code path changed. Validated by `tools/pack/check.py`
(positive control: three corrupted results all flagged) on the basic, edge, 60-item and
290,000-sticker fixtures.

# 2.5.0 — stacked cut count for `--pack` (2026-10-22)

Asked for by web2print. Each run gains `stacked_cuts`, `cost.stack_strips` and
`totals.stacked_cuts`: the same cut plan priced as a shop that piles identical regions and cuts
them together (manual chapter 21, *Stacked cuts*). These fields are additive and
informational: `cost.total`, margins and every strategy's choices are unchanged.
- Measured on the basic fixture: 41 lines become 33.
- On the 60-item fixture: 511–1,129 lines become 411–717.
- 50,000 stickers: 1,282 lines become 152, but the price only drops from 641.00 to 560.00,
  because a per-lift price charges the taller pile.
- `tools/pack/check.py` bounds both fields (positive control: three corruptions flagged).
- The 60-item search time is unchanged within noise, at a 128 MB heap.

# 2.8.0 — web2print batch: merge usage, thin-line marks, vector trap (2026-10-23)

Three web2print requests. Every existing default output is unchanged.
- **`--merge` usage fix.** The `--usage` sidecar now counts every source of a merge and the real
  output: a 1+1+1-page merge reports `pages_in` 3, `pages_out` 3 (through 2.7.0 it reported 1 / 1).
  Hidden bug fixed with it: the comma form `--merge=a,b,c` merged in REVERSE order (c,b,a)
  through 2.7.0; it now keeps the order given, like the positional form web2print uses.
- **`--thin-line-mark=blend|fill|ring`** (manual 4.4). `blend` stays the default and its overlay
  PNG is byte-identical to 2.7.0 (10 overlays, raster and contour). `fill` and `ring` make the
  marks readable at a ~1000 px preview. The JSON report does not depend on the style.
- **`--trap=W[:spread|choke|centre]`** (manual 17.4b): a new standalone prepress mode for vector
  trapping on the composite PDF, with `--trap-pairs` and `--trap-join`. `--separate --trap` (the
  raster plate option) is untouched and still routed to `--separate`.

# 2.8.1 — `--edge` / `--backers` offset fits a small heap (2026-10-24)

The offset band (Offset Path) used to be one `new Area()` over the stroked outline of the whole
boundary. On dense art that one sweep ran out of memory: on BR1X019 (21.8k boundary segments)
`--edge` hit OOM at 768m after 156 s, every version back to at least 2.5.2. The band is now built
one subpath at a time and combined in a balanced union: the same point set.
- BR1X019 at -Xmx2g: `--edge` 182 s / 2.2 GB RSS becomes 31 s / 557 MB, and `--backers` 25 s
  becomes 15 s.
- Both pass at -Xmx256m.
- Pixel gate at 300 dpi, edge on five samples and backers: 0–10 anti-aliased edge pixels differ,
  never a whole pixel (worst channel 44/255). Band areas agree to 4e-7 relative.
- Splitting a subpath further into open runs is NOT equivalent (it drops band pockets where the
  path turns tighter than the pen) and is not done.
