5 Commits
Author SHA1 Message Date
james.maesandClaude Fable 5.2 f5acf93e3d feat(renderer): scene 2 text: bundled faces, measurement before drawing, stated fit rules (P2)
maven / build (push) Successful in 10m16s
build / maven (push) Successful in 10m16s
A version 2 `text` element is now drawn. It replaces the placeholder that
parsed and placed a text and refused it as RENDERER_TOO_OLD.

What a scene can say
- `font` names one of the five bundled faces (or comes from `defaults`); any
  other name is FONT_UNKNOWN. There is no printer font and no installed font.
- `size` is the em as a physical length (1 pt to 1000 pt), not rounded to dots.
- `value` or `valuePath`; `align`/`valign` place the block in the box and each
  line in the block; `lineHeight`, `rotation`, `reverse`, `threshold`.
- `fit`: one line by default, shrunk in quarter-point steps to 70% of the size
  (not below 5 pt), then refused. Opt in to more lines (`maxLines`) and to
  cutting with an ellipsis (`overflow: truncate`).
- `maxLength`: validation proves the longest value fits, so a length refusal
  happens when a scene is published; printing refuses a longer value first.

How it is measured and drawn
- Measurement is integer arithmetic on the face's own numbers (thousandths of
  the em, read once at a reference size), so it is the same on every machine.
  Every character sits at a whole-dot pen position and is drawn there by the
  Java rasteriser that version 1 raster text uses, then reduced to ink at the
  threshold. A line's box is its advance widened to its ink plus two dots a
  side; a render whose ink would leave it fails instead of clipping.
- At each size a line is broken between words while that fits and at any
  character when it does not, which is also what makes a length provable.
- One glyph for each character, left to right: no kerning, ligatures, mark
  placement or bidirectional text. A character that cannot be drawn that way
  is FONT_MISSING_GLYPH with a count.

Diagnostics (ids and numbers only; no value ever enters one)
TEXT_DOES_NOT_FIT, VALUE_MISSING, VALUE_TOO_LONG, FONT_UNKNOWN,
FONT_MISSING_GLYPH, RESERVED_FONT_NAME; TEXT_TRUNCATED, TEXT_SMALL_FOR_DPI,
TEXT_LIGHT_FACE, FIT_NOT_PROVEN; TEXT_SHRUNK, TEXT_WRAPPED.

Core seams added: `ElementSource.inherit` gives an element the scene's
`defaults` for the properties its type reads, and `ElementSource.refuse`
raises a coded refusal at a property (FONT_UNKNOWN at the place that names
the face, in the element, the defaults or an override).

Tests: goldens for the five faces and for fit, rotation and reverse at 203,
300 and 600 dpi, generated in the CI image (Linux, Temurin 21.0.9) and the
same bytes on Windows (21.0.12) and on Linux Temurin 21.0.13; every character
of every face drawn inside its measured line; seeded property tests of fit
(never shrunk when it fits, the largest size that fits, never below the floor,
never more lines than allowed, cut only when asked and always with the mark,
a proven length never refused); licence, provenance and hash of every face;
the version 2 mutation campaign extended to text. The version 1 corpus is
untouched. 100% line coverage of the scene 2 packages.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:37:13 -06:00
james.maesandClaude Fable 5.2 bf546e16c5 feat(bridge): accept renderer 0.2.0, read its diagnostics, preview a label (P5)
maven / build (push) Successful in 12m32s
build / maven (push) Successful in 12m32s
On develop the renderer speaks qqq-renderer-0.2.0 and the bridge accepted
only 0.1.0, so the bridge refused its own renderer, a label could not be
previewed, and a render target was accepted and then ignored.

The version rule (RendererVersion). The bridge accepts 0.1.0 (the released
fields, the 13 released refusals) and 0.2.0 (the same with diagnostics and
detail, those refusals and the 15 scene refusals); any other version is
rejected. Each set is written out, replacing the temporary enum range. A
job is framed as 0.1.0 reads it unless it needs more. Before a job only
0.2.0 can do (a version 2 scene, a preview) the bridge asks the worker its
version by validating a fixed, empty version 1 scene, so an old image is
refused as RENDERER_TOO_OLD and receives nothing of the scene or the
record; an answer from 0.1.0 to such a job is refused whatever it says.

Diagnostics (WorkerAnswer, SceneFacts). A version 2 refusal is
RenderException(code, diagnostic). The worker is untrusted, so every field
is checked again: the closed vocabulary, whole numbers in bounds, an
element only if the scene that was sent declares it, and a pointer only of
array indexes, schema property names and declared ids. An answer holds at
most 2048 diagnostics in one megabyte beside its label, and no frame
passes the frame of the largest document the deployment admits. A refused
answer that carries any part of a label is rejected.

Render target. targetDpi is sent with a version 2 scene and the label is
held by the output policy to the page at that resolution. A version 1
scene or a PDF asked for another resolution than its stock's is refused as
TARGET_REQUIRES_SCENE_2 before any worker is contacted, because renderer
0.1.0 does not read the field. supportsRenderTarget() is true.

Preview. preview() is the print path stopped before retention: the same
job with collected diagnostics, the same integrity checks and output
policy, and a page decoded by ZplRaster from the accepted label bytes.
ZplRaster is its own strict reader of the policy's grammar and runs the
policy first; it paints ^GFA and ^GB exactly, hatches printer font text
(the page is then not exact) and writes a one-bit PNG itself. Refusals
come back as diagnostics, never as a label. A refused scene is drawn on
the host from the diagnostics' boxes alone: the worker sends no label for
it and the wire gains no field for the partial canvas, so no host can
print a label with elements missing. The design records the decision in
sections 6.1 and B.1, the version question in 11.3 and what the host now
reads from a scene in 8.1.

renderer/qualification.json said qqq-renderer-0.1.0 and qqq-barcodes-1;
it now says what the worker reports, and a test holds it to the answer.

The mutation campaign covers the page decoder and the answer frames. The
new classes are gated at 100% line coverage and the bundle gate rises to
the measured 90% of instructions and 88% of lines. The version 1 corpus is
untouched.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:02:54 -06:00
james.maesandClaude Fable 5.2 58051e7b99 fix(renderer): scene 2 core: the findings of the independent review
maven / build (push) Successful in 11m29s
build / maven (push) Successful in 11m29s
1. A length too long for any stock is a named refusal. Length.parse
   narrowed the micrometres to an int before it applied the 2 m limit, so
   "84600in" or "99999in" threw ArithmeticException: the opaque
   RENDER_FAILED, and an error instead of a REFUSED answer when collecting.
   The limit is now compared in long before narrowing, so it is
   SCENE_INVALID at the pointer of the length.

2. The printable area holds a box of its own size at every resolution. It
   was floor(far edge) less ceil(near margin), which can be one dot smaller
   than a box of the stated printable width: 100 mm stock with 3 mm margins
   and a 94 mm box fitted at 203 and 600 dpi and was refused at 300. It is
   now floor((W - L - R) x dpi) by floor((H - T - B) x dpi), the rounding of
   a box size, starting at ceil(L x dpi), ceil(T x dpi), moved back one dot
   where it would otherwise pass the page edge. New golden case `margins`
   with fractional-dot margins; the three existing goldens at 600 dpi are
   regenerated (the area grew by one dot each way; 203 and 300 unchanged).

3. The version 2 mutation campaign accepts only a Refusal (the package's
   own invariants are IllegalArgumentExceptions too) and renders about one
   input in four, reading the label back and comparing with validation. It
   found nothing at 20000 or 100000 iterations for either seed.

4. A refused answer carries no label bytes: with COLLECT and an error the
   answer had base64 of a label with elements missing. Scene2.render answers
   no label for a refused scene and the answer has no document fields. This
   changes the wire contract of section B.1 of the design, which records it.

5. One scene may paint sixteen times its printable area, an overlap counted
   each time, counted before any ink and in validation too. The element
   that would pass it is SCENE_INVALID with measured.paintedPercent against
   limit.paintedPercent 1600. 1000 fills of a 64 million dot canvas were
   8 GB of byte writes; the worst scene is now 128 MB.

6. The README states what changed in the version 1 answer envelope in
   0.2.0 (diagnostics on every answer; SCENE_INVALID with detail where
   unreadable JSON and a missing or unknown schema were RENDER_FAILED) and
   what the bridge must handle. The version 1 ZPL bytes are unchanged.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 10:59:20 -06:00
james.maesandClaude Fable 5.2 ad6b0ffa88 feat(renderer): scene 2 core: layout in physical units, one canvas, diagnostics (P1)
A source with schema qqq-zpl-scene/2 is a label layout in millimetres,
inches, points, percentages and dots that renders at 203, 300 or 600 dpi.
The worker routes by the schema alone: version 1 takes exactly the path it
always took (the compatibility corpus is unchanged and green), version 2
takes the new package com.kingsrook.qbits.renderer.scene2, and anything
else is refused by name.

- Length: parse, whole micrometres, dots by the rule of each use
  (position nearest, box size down, thickness nearest and at least one,
  module up, font size not rounded).
- Layout: nine-point anchors, offsets inward from the anchored edge,
  omitted sizes, content alignment, groups four deep, percentages of the
  container. Coordinates start at the top left of the printable area.
- Canvas and ZplEmitter: the whole label on one 1-bit canvas, emitted as
  trimmed ^GFA bands that pass the unchanged ZplOutputPolicy.
- Diagnostics: every refusal is one diagnostic with a code, the element
  id, the box and named integers; SCENE_INVALID carries a JSON pointer.
  A diagnostic names only an id the scene declares and points only
  through schema names, indexes and declared ids.
- box and group elements; overrides by resolution, checked for all three
  resolutions; samples accepted and ignored. Text, barcodes and images
  are parsed and placed and then refused as RENDERER_TOO_OLD by
  placeholder factories in their own sub-packages.
- Renderer 0.2.0: optional targetDpi and "diagnostics": "COLLECT" on the
  request, diagnostics on the answer, detail on a version 2 error, the
  REFUSED outcome, and INSPECT_ASSET and UPGRADE_SCENE routed and refused
  as UNSUPPORTED_OPERATION. A version 1 scene with a target other than
  its stock's dpi is refused as TARGET_REQUIRES_SCENE_2.

Tests: every length and rounding rule; all nine anchors; seeded property
tests (mirrored anchors, boxes never leave the printable area, the bitmap
rebuilt from the emitted fields equals the canvas); goldens at three
resolutions as pictures and as ZPL, the ZPL also checked against the host
gate in bridges/renderer; every refusal asserted as the whole diagnostic;
a mutation campaign over the version 2 parser. The new package is at 100%
line coverage.

Also, from the review of the foundations: the mutation campaigns accept
only declared refusals (a NullPointerException in a parser is now a
finding), git no longer treats the stored corpus as text, and both
compatibility tests pin the digest of the corpus manifest.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 10:01:21 -06:00
james.maesandClaude Fable 5.2 3ba676106f build(coverage): gate renderer and bridge coverage at baseline; seeded mutation campaigns (P13)
Coverage gates:
- renderer and bridges/renderer gain JaCoCo (prepare-agent, report, check at
  verify) with a bundle rule at the measured baseline, rounded down: renderer
  94% instructions and 91% lines, bridge 85% instructions and 82% lines.
- New code is held to 100% line coverage from its first commit: packages
  com.kingsrook.qbits.renderer.scene2 and below, AssetInspector, ZplRaster,
  and the new foundation classes the label layout design names. The rules
  match nothing until those exist, so later packages do not edit these poms.
- The foundation's 85% instruction and 90% class rule is unchanged.

Fuzz scaffolding:
- MutationCampaign in each module's test tree: a seeded, bounded mutation
  harness with no clock and no unseeded randomness; a finding replays from
  its seed and iteration.
- One campaign per module in the normal test phase: what the version 1 scene
  parser accepts is a version 1 scene (checked by a second parser), and what
  the host printer gate accepts holds only the renderer's command set.
- docs/security/fuzzing.md: targets, properties, limits, seeds and replay.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 17:28:42 -06:00