Packages P5 (bridge: accepted renderers, diagnostics, ZplRaster, preview) and P7 (render target dpi) of docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md, as one pull request.
Why
On develop the renderer speaks qqq-renderer-0.2.0 and the host bridge accepted only qqq-renderer-0.1.0, so the bridge refused its own renderer. A label could not be previewed before printing, and a request could not say which printer resolution to render for (RenderRequest.targetDpi was validated and then ignored by the bridge). This makes the host side whole again, adds the preview, and carries the render target end to end.
The version rule
One table, in RendererVersion:
Renderer
Answers
Refusals it may name
Version 2 scene, render target, preview
0.1.0
the released fields only (5 for VALIDATE, 10 for RENDER, 6 for an error)
the 13 released codes
refused: RENDERER_TOO_OLD
0.2.0
the same, with diagnostics (and detail on an error)
those and the 15 scene codes
rendered
any other
rejected
The temporary RENDERER_0_1_REFUSALS range is gone. Each set is written out, so a code added to FailureCode later belongs to no renderer until someone decides which (a test says so).
A job is framed exactly as renderer 0.1.0 reads it unless it needs more, so version 1 printing is the same request against either image.
The bridge asks before it sends. Nothing tells the bridge which image answers, so before a job that only 0.2.0 can do it validates a fixed, empty version 1 scene on a fixed stock; every released renderer answers that with its version. An old image is refused with RENDERER_TOO_OLD and receives nothing of the version 2 scene or the record. After a verified answer of 0.2.0 the question is not repeated. An answer from 0.1.0 to such a job is refused whatever it says, and the next job asks again. Cost: one extra worker run for the first version 2 job of an adapter, and one for each version 2 job against an old image.
Diagnostics from an untrusted worker
WorkerAnswer reads the frame; SceneFacts holds the two facts the host reads from the source it sends (its schema, and the id of each element and child). The host still lays out and validates nothing.
A version 2 refusal is RenderException(code, diagnostic) instead of the opaque failure.
Every field is checked again on the host: code and severity against the closed vocabulary, numbers as whole integers in bounds, the element against the ids the scene that was sent declares, and each pointer segment against array indexes, the schema's property names (held equal to renderer/schema/zpl-scene-2.schema.json by a test) and those ids.
An answer holds at most 2048 diagnostics in 1 MiB beside its label. The VALIDATE frame was 4096 bytes; for a job that can return diagnostics it is 4096 bytes plus that megabyte, and no frame ever passes the frame of the largest document the deployment admits, which is what the transport was sized for.
A refused answer that carries any part of a label is rejected by name, as is a refused answer to a job that did not collect (the print path never does) and an accepted answer that holds an error.
Preview
preview(PreviewRequest) is the print path stopped before retention: the same job with "diagnostics": "COLLECT", the same integrity checks, the same ZplOutputPolicy, then ZplRaster decodes the page from the accepted label bytes into PreviewPage(png, widthDots, heightDots). A test holds the print and preview jobs equal but for the request id and that one field, and the preview's size and hash equal to the bytes render returns.
ZplRaster is its own strict reader of the policy's grammar. Its entry point runs the policy first, so no caller can draw bytes it did not accept. ^GFA and ^GB are painted exactly; each ^A0N field (version 1 only) is a hatched placeholder and the page says exact: false with the warning PRINTER_FONT_NOT_SHOWN. The PNG is written by the class itself (one bit per dot); no image or font is decoded on the host.
A refused scene is drawn on the host from the diagnostics' boxes, and from nothing else. The design left this open after the scene 2 core: the worker no longer returns a partial label. The two options were to draw from the boxes, or to add a separately named field holding the partial canvas. The second would show the elements that had no error, but it puts a printable label with elements missing on the host, and it makes the host draw bytes the output policy never accepted as the label. With boxes alone no such bytes exist outside the worker, so no host can print a partial label, whatever it does. The preview of a refused scene is therefore a page on which each error's box is outlined and hatched at its place, with printable: false, no output (outputBytes 0, the hash of no bytes), and no page at all when no error has a box. Recorded in sections 6.1 and B.1 of the design.
A refusal is a result, not an exception: a collected REFUSED answer, an error with its detail, TARGET_REQUIRES_SCENE_2, RENDERER_TOO_OLD and PREVIEW_TOO_LARGE (more than sixteen million dots, decided before any worker is contacted) all come back as diagnostics. An opaque failure stays the generic exception.
The bridge writes and logs nothing for a preview. The production Docker transport journals every worker run it starts (container and request identity, never source or context); a preview's run is journalled like a validation's.
PreviewRequest and RenderRequest now hold their own unmodifiable copy of the context (null values allowed) and print only the format and render target (and the request id); neither wire form changes. This was a review finding on the ports.
Render target
DocumentGenerationService.Request gains a trailing Integer targetDpi (absent, 203, 300 or 600). Every earlier constructor stays.
Stored in a new nullable column, compared on retry and lookup with destination, media and copies, and framed into the intent digest as target-dpi/v1 when stated. A request without one has exactly the digest it had (a test recomputes it from the framing, and the migration test shows an earlier row still matches).
Sent to the worker with a version 2 scene, and the label is held by the output policy to the page at that resolution, so a worker that renders at another resolution is refused in either direction.
No path is left where 300 dpi is asked for and another resolution comes back. Generation refuses a target for a PDF, and refuses any target unless the configured renderer declares the new IsolatedRenderer.supportsRenderTarget() (false by default, so an adapter written before the target cannot be handed one and ignore it); both before any row. The bridge refuses a version 1 scene or a PDF asked for another resolution than its stock's as TARGET_REQUIRES_SCENE_2 before any worker is contacted (renderer 0.1.0 does not read the field at all).
The generate process form offers the target and the request status shows it. Batches, the queue and reprints carry none.
What a host must apply
One statement, from the shipped script for its database (foundation/src/main/resources/db/migrations/):
Database
Script
Statement
H2
20261112-render-target-dpi-h2.sql
ALTER TABLE document_generation_request ADD COLUMN target_dpi INTEGER NULL;
MySQL
20261112-render-target-dpi-mysql.sql
ALTER TABLE document_generation_request ADD COLUMN target_dpi INT NULL;
PostgreSQL
20261112-render-target-dpi-postgres.sql
ALTER TABLE document_generation_request ADD COLUMN IF NOT EXISTS target_dpi INTEGER NULL;
Earlier rows stay null and read as no target. A host with its own IsolatedRenderer adapter that wants render targets overrides supportsRenderTarget(); the two bridges in this repository already do.
Also
renderer/qualification.json said qqq-renderer-0.1.0 and qqq-barcodes-1; it now says what the worker reports (qqq-renderer-0.2.0, qqq-barcodes-9), and a test holds it to the worker's answer. It still says qualified: false.
Where the design and the code did not match
Section 11.3 has the bridge refuse an old image by name but gives it no way to know the image's version before it sends. The version question above is new; the section records it.
Section 6.1 drew a refused preview from the worker's partial bitmap. Since the scene 2 core a refused answer has no label; the decision above replaces it.
Section 2.3 says only the renderer reads the scene. To refuse a version 1 target before sending, and to check diagnostics, the host now reads the schema and the element ids of the source, and nothing else.
Work plan, P5 lists RendererJson.java as edited: the scalar-only answer reader in it is removed, and WorkerAnswer reads the whole frame.
Work plan, P7 lists GenerationStore.java and DocumentGenerationSchema.java: neither needed a change (the bootstrap DDL is generated from the table metadata), and PrintingDispatch.java does not carry the value.
With renderer 0.2.0 a label source that is not JSON, or has no known schema, is refused as SCENE_INVALID with a pointer where 0.1.0 gave the opaque failure. The bridge accepts that from 0.2.0 only.
Tests
Version rule matrix (VersionRuleTest): renderer 0.1.0 and 0.2.0 against version 1 and version 2 scenes, what each is sent, the version question and that it holds nothing of the scene or record, a renderer replaced by an older one, an unknown version, and each render target rule.
Hostile answers (WorkerAnswerTest): bad pointer segments, an undeclared element (also one that appears only in a sample), oversized frames, texts and diagnostic counts, every wrong field count, a refused answer carrying each part of a label, wrong shapes, each rejected by its name.
ZplRaster (ZplRasterTest): every scene 2 golden at 203, 300 and 600 dpi decodes dot for dot to the golden picture of the canvas, and the PNG read back by the platform decoder holds the same dots; all 42 released version 1 labels decode; malformed strips and oversized pages are refused by name.
Preview (PreviewTest): the golden picture at three resolutions through the bridge, print and preview equality, no files written, the refused page placed at the golden's own corner at three resolutions, old image, too large, version 1 placeholders, and that only an accepted label becomes a picture.
Mutation campaigns extended to the page decoder and the answer frames, each with an independent reading of what was accepted (docs/security/fuzzing.md).
Render target (GenerationExpectedIntentTest, GenerationProcessesTest, GenerationPortsContractTest): carried, stored, part of the intent, digest framing recomputed independently, refusals before any row, typed refusal replayed without rendering, and the migration on H2, MySQL and PostgreSQL (GenerationQueueJdbcTest and its two real-engine subclasses).
Worker side (HostContractTest in renderer): the qualification record, the version question, and that collecting diagnostics does not change a label that prints.
The version 1 corpus is untouched and green in both modules.
Build
./mvnw --batch-mode --no-transfer-progress -s <settings> -T1C verify on the head bf546e1, from a clean tree: BUILD SUCCESS, all eleven modules, no skipped test and no skipped gate, 0 Checkstyle violations, every coverage check met.
Module
Tests
foundation
264
renderer
306
bridges/renderer
207
the other five modules with tests
76
all
853
Line coverage in bridges/renderer (JaCoCo, this build): ZplRaster 183 of 183 lines and 132 of 132 branches; WorkerAnswer 122 of 122 and 152 of 152; SceneFacts 59 of 59; RendererVersion 14 of 14. Those four are gated at 100% of lines by name. IsolatedRendererBridge, an existing class under the bundle gate, is at 182 of 198 lines. The bundle is at 90.9% of instructions and 88.4% of lines, and its gate rises from 85% and 82% to 90% and 88%, as the POM says to do when coverage rises. The numbers were measured on Windows; the gate is a whole-percent floor under them.
Deliberately left out
The preview service, action and record widget (P6b), PDF previews (P11) and the dg: asset branch (P8).
A picture of the elements that had no error in a refused preview: see the decision above.
A render target for batches, the queue and reprints.
RemoteRendererBridge.preview and supportsRenderTarget delegate to the bridge in one line each; that module has no test tree and none was added.
No container image was built or pushed, and no release branch was touched.
Packages **P5** (bridge: accepted renderers, diagnostics, `ZplRaster`, preview) and **P7** (render target dpi) of `docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md`, as one pull request.
## Why
On `develop` the renderer speaks `qqq-renderer-0.2.0` and the host bridge accepted only `qqq-renderer-0.1.0`, so the bridge refused its own renderer. A label could not be previewed before printing, and a request could not say which printer resolution to render for (`RenderRequest.targetDpi` was validated and then ignored by the bridge). This makes the host side whole again, adds the preview, and carries the render target end to end.
## The version rule
One table, in `RendererVersion`:
| Renderer | Answers | Refusals it may name | Version 2 scene, render target, preview |
|---|---|---|---|
| `0.1.0` | the released fields only (5 for VALIDATE, 10 for RENDER, 6 for an error) | the 13 released codes | refused: `RENDERER_TOO_OLD` |
| `0.2.0` | the same, with `diagnostics` (and `detail` on an error) | those and the 15 scene codes | rendered |
| any other | rejected | | |
- The temporary `RENDERER_0_1_REFUSALS` range is gone. Each set is written out, so a code added to `FailureCode` later belongs to no renderer until someone decides which (a test says so).
- A job is framed exactly as renderer 0.1.0 reads it unless it needs more, so version 1 printing is the same request against either image.
- **The bridge asks before it sends.** Nothing tells the bridge which image answers, so before a job that only 0.2.0 can do it validates a fixed, empty version 1 scene on a fixed stock; every released renderer answers that with its version. An old image is refused with `RENDERER_TOO_OLD` and receives nothing of the version 2 scene or the record. After a verified answer of 0.2.0 the question is not repeated. An answer from 0.1.0 to such a job is refused whatever it says, and the next job asks again. Cost: one extra worker run for the first version 2 job of an adapter, and one for each version 2 job against an old image.
## Diagnostics from an untrusted worker
`WorkerAnswer` reads the frame; `SceneFacts` holds the two facts the host reads from the source it sends (its `schema`, and the `id` of each element and child). The host still lays out and validates nothing.
- A version 2 refusal is `RenderException(code, diagnostic)` instead of the opaque failure.
- Every field is checked again on the host: code and severity against the closed vocabulary, numbers as whole integers in bounds, the element against the ids the scene that was sent declares, and each pointer segment against array indexes, the schema's property names (held equal to `renderer/schema/zpl-scene-2.schema.json` by a test) and those ids.
- An answer holds at most 2048 diagnostics in 1 MiB beside its label. The VALIDATE frame was 4096 bytes; for a job that can return diagnostics it is 4096 bytes plus that megabyte, and no frame ever passes the frame of the largest document the deployment admits, which is what the transport was sized for.
- A refused answer that carries any part of a label is rejected by name, as is a refused answer to a job that did not collect (the print path never does) and an accepted answer that holds an error.
## Preview
`preview(PreviewRequest)` is the print path stopped before retention: the same job with `"diagnostics": "COLLECT"`, the same integrity checks, the same `ZplOutputPolicy`, then `ZplRaster` decodes the page from the accepted label bytes into `PreviewPage(png, widthDots, heightDots)`. A test holds the print and preview jobs equal but for the request id and that one field, and the preview's size and hash equal to the bytes `render` returns.
- **`ZplRaster`** is its own strict reader of the policy's grammar. Its entry point runs the policy first, so no caller can draw bytes it did not accept. `^GFA` and `^GB` are painted exactly; each `^A0N` field (version 1 only) is a hatched placeholder and the page says `exact: false` with the warning `PRINTER_FONT_NOT_SHOWN`. The PNG is written by the class itself (one bit per dot); no image or font is decoded on the host.
- **A refused scene is drawn on the host from the diagnostics' boxes, and from nothing else.** The design left this open after the scene 2 core: the worker no longer returns a partial label. The two options were to draw from the boxes, or to add a separately named field holding the partial canvas. The second would show the elements that had no error, but it puts a printable label with elements missing on the host, and it makes the host draw bytes the output policy never accepted as the label. With boxes alone no such bytes exist outside the worker, so no host can print a partial label, whatever it does. The preview of a refused scene is therefore a page on which each error's box is outlined and hatched at its place, with `printable: false`, no output (`outputBytes` 0, the hash of no bytes), and no page at all when no error has a box. Recorded in sections 6.1 and B.1 of the design.
- **A refusal is a result, not an exception**: a collected `REFUSED` answer, an error with its `detail`, `TARGET_REQUIRES_SCENE_2`, `RENDERER_TOO_OLD` and `PREVIEW_TOO_LARGE` (more than sixteen million dots, decided before any worker is contacted) all come back as diagnostics. An opaque failure stays the generic exception.
- The bridge writes and logs nothing for a preview. The production Docker transport journals every worker run it starts (container and request identity, never source or context); a preview's run is journalled like a validation's.
- `PreviewRequest` and `RenderRequest` now hold their own unmodifiable copy of the context (null values allowed) and print only the format and render target (and the request id); neither wire form changes. This was a review finding on the ports.
## Render target
`DocumentGenerationService.Request` gains a trailing `Integer targetDpi` (absent, 203, 300 or 600). Every earlier constructor stays.
- Stored in a new nullable column, compared on retry and lookup with destination, media and copies, and framed into the intent digest as `target-dpi/v1` when stated. A request without one has exactly the digest it had (a test recomputes it from the framing, and the migration test shows an earlier row still matches).
- Sent to the worker with a version 2 scene, and the label is held by the output policy to the page at that resolution, so a worker that renders at another resolution is refused in either direction.
- **No path is left where 300 dpi is asked for and another resolution comes back.** Generation refuses a target for a PDF, and refuses any target unless the configured renderer declares the new `IsolatedRenderer.supportsRenderTarget()` (false by default, so an adapter written before the target cannot be handed one and ignore it); both before any row. The bridge refuses a version 1 scene or a PDF asked for another resolution than its stock's as `TARGET_REQUIRES_SCENE_2` before any worker is contacted (renderer 0.1.0 does not read the field at all).
- The generate process form offers the target and the request status shows it. Batches, the queue and reprints carry none.
### What a host must apply
One statement, from the shipped script for its database (`foundation/src/main/resources/db/migrations/`):
| Database | Script | Statement |
|---|---|---|
| H2 | `20261112-render-target-dpi-h2.sql` | `ALTER TABLE document_generation_request ADD COLUMN target_dpi INTEGER NULL;` |
| MySQL | `20261112-render-target-dpi-mysql.sql` | `ALTER TABLE document_generation_request ADD COLUMN target_dpi INT NULL;` |
| PostgreSQL | `20261112-render-target-dpi-postgres.sql` | `ALTER TABLE document_generation_request ADD COLUMN IF NOT EXISTS target_dpi INTEGER NULL;` |
Earlier rows stay null and read as no target. A host with its own `IsolatedRenderer` adapter that wants render targets overrides `supportsRenderTarget()`; the two bridges in this repository already do.
## Also
`renderer/qualification.json` said `qqq-renderer-0.1.0` and `qqq-barcodes-1`; it now says what the worker reports (`qqq-renderer-0.2.0`, `qqq-barcodes-9`), and a test holds it to the worker's answer. It still says `qualified: false`.
## Where the design and the code did not match
- **Section 11.3** has the bridge refuse an old image by name but gives it no way to know the image's version before it sends. The version question above is new; the section records it.
- **Section 6.1** drew a refused preview from the worker's partial bitmap. Since the scene 2 core a refused answer has no label; the decision above replaces it.
- **Section 2.3** says only the renderer reads the scene. To refuse a version 1 target before sending, and to check diagnostics, the host now reads the `schema` and the element ids of the source, and nothing else.
- **Work plan, P5** lists `RendererJson.java` as edited: the scalar-only answer reader in it is removed, and `WorkerAnswer` reads the whole frame.
- **Work plan, P7** lists `GenerationStore.java` and `DocumentGenerationSchema.java`: neither needed a change (the bootstrap DDL is generated from the table metadata), and `PrintingDispatch.java` does not carry the value.
- With renderer 0.2.0 a label source that is not JSON, or has no known schema, is refused as `SCENE_INVALID` with a pointer where 0.1.0 gave the opaque failure. The bridge accepts that from 0.2.0 only.
## Tests
- **Version rule matrix** (`VersionRuleTest`): renderer 0.1.0 and 0.2.0 against version 1 and version 2 scenes, what each is sent, the version question and that it holds nothing of the scene or record, a renderer replaced by an older one, an unknown version, and each render target rule.
- **Hostile answers** (`WorkerAnswerTest`): bad pointer segments, an undeclared element (also one that appears only in a sample), oversized frames, texts and diagnostic counts, every wrong field count, a refused answer carrying each part of a label, wrong shapes, each rejected by its name.
- **`ZplRaster`** (`ZplRasterTest`): every scene 2 golden at 203, 300 and 600 dpi decodes dot for dot to the golden picture of the canvas, and the PNG read back by the platform decoder holds the same dots; all 42 released version 1 labels decode; malformed strips and oversized pages are refused by name.
- **Preview** (`PreviewTest`): the golden picture at three resolutions through the bridge, print and preview equality, no files written, the refused page placed at the golden's own corner at three resolutions, old image, too large, version 1 placeholders, and that only an accepted label becomes a picture.
- **Mutation campaigns** extended to the page decoder and the answer frames, each with an independent reading of what was accepted (`docs/security/fuzzing.md`).
- **Render target** (`GenerationExpectedIntentTest`, `GenerationProcessesTest`, `GenerationPortsContractTest`): carried, stored, part of the intent, digest framing recomputed independently, refusals before any row, typed refusal replayed without rendering, and the migration on H2, MySQL and PostgreSQL (`GenerationQueueJdbcTest` and its two real-engine subclasses).
- **Worker side** (`HostContractTest` in `renderer`): the qualification record, the version question, and that collecting diagnostics does not change a label that prints.
- The version 1 corpus is untouched and green in both modules.
## Build
`./mvnw --batch-mode --no-transfer-progress -s <settings> -T1C verify` on the head `bf546e1`, from a clean tree: **BUILD SUCCESS**, all eleven modules, no skipped test and no skipped gate, 0 Checkstyle violations, every coverage check met.
| Module | Tests |
|---|---|
| `foundation` | 264 |
| `renderer` | 306 |
| `bridges/renderer` | 207 |
| the other five modules with tests | 76 |
| all | 853 |
Line coverage in `bridges/renderer` (JaCoCo, this build): `ZplRaster` 183 of 183 lines and 132 of 132 branches; `WorkerAnswer` 122 of 122 and 152 of 152; `SceneFacts` 59 of 59; `RendererVersion` 14 of 14. Those four are gated at 100% of lines by name. `IsolatedRendererBridge`, an existing class under the bundle gate, is at 182 of 198 lines. The bundle is at 90.9% of instructions and 88.4% of lines, and its gate rises from 85% and 82% to 90% and 88%, as the POM says to do when coverage rises. The numbers were measured on Windows; the gate is a whole-percent floor under them.
## Deliberately left out
- The preview service, action and record widget (P6b), PDF previews (P11) and the `dg:` asset branch (P8).
- A picture of the elements that had no error in a refused preview: see the decision above.
- A render target for batches, the queue and reprints.
- `RemoteRendererBridge.preview` and `supportsRenderTarget` delegate to the bridge in one line each; that module has no test tree and none was added.
- No container image was built or pushed, and no release branch was touched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A request could not say which printer resolution to render a label for:
RenderRequest.targetDpi existed and nothing set it.
DocumentGenerationService.Request gains a trailing targetDpi (absent, 203,
300 or 600; every earlier constructor stays and means none). It is stored
in a new nullable column, target_dpi, on document_generation_request,
compared on retry and lookup beside destination, media and copies, framed
into the intent digest as target-dpi/v1 when stated, and passed to the
renderer. A request without a target has exactly the digest it had, so
earlier rows and their retries are unchanged; the same key with another
target, or with none, is another intent.
Nobody who asks for 300 dpi may silently get another resolution, so a
target is refused before any row exists when it cannot be honoured: for a
PDF, and for any renderer that does not declare the new
IsolatedRenderer.supportsRenderTarget() (false by default, so an adapter
written before the target cannot be handed one and ignore it).
A host applies one statement, from the shipped script for its database:
db/migrations/20261112-render-target-dpi-{h2,mysql,postgres}.sql
(ALTER TABLE document_generation_request ADD COLUMN target_dpi INTEGER
NULL). The generate process form offers the target and the request status
shows it. Batches, the queue and reprints carry none.
Also from the review of the ports: RenderRequest and PreviewRequest hold
their own unmodifiable copy of the context (null values allowed) and
print only their format and render target (and the request id), never the
record data or the template source. Neither wire form changes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Packages P5 (bridge: accepted renderers, diagnostics,
ZplRaster, preview) and P7 (render target dpi) ofdocs/design/LABEL-LAYOUT-FONTS-PREVIEW.md, as one pull request.Why
On
developthe renderer speaksqqq-renderer-0.2.0and the host bridge accepted onlyqqq-renderer-0.1.0, so the bridge refused its own renderer. A label could not be previewed before printing, and a request could not say which printer resolution to render for (RenderRequest.targetDpiwas validated and then ignored by the bridge). This makes the host side whole again, adds the preview, and carries the render target end to end.The version rule
One table, in
RendererVersion:0.1.0RENDERER_TOO_OLD0.2.0diagnostics(anddetailon an error)RENDERER_0_1_REFUSALSrange is gone. Each set is written out, so a code added toFailureCodelater belongs to no renderer until someone decides which (a test says so).RENDERER_TOO_OLDand receives nothing of the version 2 scene or the record. After a verified answer of 0.2.0 the question is not repeated. An answer from 0.1.0 to such a job is refused whatever it says, and the next job asks again. Cost: one extra worker run for the first version 2 job of an adapter, and one for each version 2 job against an old image.Diagnostics from an untrusted worker
WorkerAnswerreads the frame;SceneFactsholds the two facts the host reads from the source it sends (itsschema, and theidof each element and child). The host still lays out and validates nothing.RenderException(code, diagnostic)instead of the opaque failure.renderer/schema/zpl-scene-2.schema.jsonby a test) and those ids.Preview
preview(PreviewRequest)is the print path stopped before retention: the same job with"diagnostics": "COLLECT", the same integrity checks, the sameZplOutputPolicy, thenZplRasterdecodes the page from the accepted label bytes intoPreviewPage(png, widthDots, heightDots). A test holds the print and preview jobs equal but for the request id and that one field, and the preview's size and hash equal to the bytesrenderreturns.ZplRasteris its own strict reader of the policy's grammar. Its entry point runs the policy first, so no caller can draw bytes it did not accept.^GFAand^GBare painted exactly; each^A0Nfield (version 1 only) is a hatched placeholder and the page saysexact: falsewith the warningPRINTER_FONT_NOT_SHOWN. The PNG is written by the class itself (one bit per dot); no image or font is decoded on the host.printable: false, no output (outputBytes0, the hash of no bytes), and no page at all when no error has a box. Recorded in sections 6.1 and B.1 of the design.REFUSEDanswer, an error with itsdetail,TARGET_REQUIRES_SCENE_2,RENDERER_TOO_OLDandPREVIEW_TOO_LARGE(more than sixteen million dots, decided before any worker is contacted) all come back as diagnostics. An opaque failure stays the generic exception.PreviewRequestandRenderRequestnow hold their own unmodifiable copy of the context (null values allowed) and print only the format and render target (and the request id); neither wire form changes. This was a review finding on the ports.Render target
DocumentGenerationService.Requestgains a trailingInteger targetDpi(absent, 203, 300 or 600). Every earlier constructor stays.target-dpi/v1when stated. A request without one has exactly the digest it had (a test recomputes it from the framing, and the migration test shows an earlier row still matches).IsolatedRenderer.supportsRenderTarget()(false by default, so an adapter written before the target cannot be handed one and ignore it); both before any row. The bridge refuses a version 1 scene or a PDF asked for another resolution than its stock's asTARGET_REQUIRES_SCENE_2before any worker is contacted (renderer 0.1.0 does not read the field at all).What a host must apply
One statement, from the shipped script for its database (
foundation/src/main/resources/db/migrations/):20261112-render-target-dpi-h2.sqlALTER TABLE document_generation_request ADD COLUMN target_dpi INTEGER NULL;20261112-render-target-dpi-mysql.sqlALTER TABLE document_generation_request ADD COLUMN target_dpi INT NULL;20261112-render-target-dpi-postgres.sqlALTER TABLE document_generation_request ADD COLUMN IF NOT EXISTS target_dpi INTEGER NULL;Earlier rows stay null and read as no target. A host with its own
IsolatedRendereradapter that wants render targets overridessupportsRenderTarget(); the two bridges in this repository already do.Also
renderer/qualification.jsonsaidqqq-renderer-0.1.0andqqq-barcodes-1; it now says what the worker reports (qqq-renderer-0.2.0,qqq-barcodes-9), and a test holds it to the worker's answer. It still saysqualified: false.Where the design and the code did not match
schemaand the element ids of the source, and nothing else.RendererJson.javaas edited: the scalar-only answer reader in it is removed, andWorkerAnswerreads the whole frame.GenerationStore.javaandDocumentGenerationSchema.java: neither needed a change (the bootstrap DDL is generated from the table metadata), andPrintingDispatch.javadoes not carry the value.SCENE_INVALIDwith a pointer where 0.1.0 gave the opaque failure. The bridge accepts that from 0.2.0 only.Tests
VersionRuleTest): renderer 0.1.0 and 0.2.0 against version 1 and version 2 scenes, what each is sent, the version question and that it holds nothing of the scene or record, a renderer replaced by an older one, an unknown version, and each render target rule.WorkerAnswerTest): bad pointer segments, an undeclared element (also one that appears only in a sample), oversized frames, texts and diagnostic counts, every wrong field count, a refused answer carrying each part of a label, wrong shapes, each rejected by its name.ZplRaster(ZplRasterTest): every scene 2 golden at 203, 300 and 600 dpi decodes dot for dot to the golden picture of the canvas, and the PNG read back by the platform decoder holds the same dots; all 42 released version 1 labels decode; malformed strips and oversized pages are refused by name.PreviewTest): the golden picture at three resolutions through the bridge, print and preview equality, no files written, the refused page placed at the golden's own corner at three resolutions, old image, too large, version 1 placeholders, and that only an accepted label becomes a picture.docs/security/fuzzing.md).GenerationExpectedIntentTest,GenerationProcessesTest,GenerationPortsContractTest): carried, stored, part of the intent, digest framing recomputed independently, refusals before any row, typed refusal replayed without rendering, and the migration on H2, MySQL and PostgreSQL (GenerationQueueJdbcTestand its two real-engine subclasses).HostContractTestinrenderer): the qualification record, the version question, and that collecting diagnostics does not change a label that prints.Build
./mvnw --batch-mode --no-transfer-progress -s <settings> -T1C verifyon the headbf546e1, from a clean tree: BUILD SUCCESS, all eleven modules, no skipped test and no skipped gate, 0 Checkstyle violations, every coverage check met.foundationrendererbridges/rendererLine coverage in
bridges/renderer(JaCoCo, this build):ZplRaster183 of 183 lines and 132 of 132 branches;WorkerAnswer122 of 122 and 152 of 152;SceneFacts59 of 59;RendererVersion14 of 14. Those four are gated at 100% of lines by name.IsolatedRendererBridge, an existing class under the bundle gate, is at 182 of 198 lines. The bundle is at 90.9% of instructions and 88.4% of lines, and its gate rises from 85% and 82% to 90% and 88%, as the POM says to do when coverage rises. The numbers were measured on Windows; the gate is a whole-percent floor under them.Deliberately left out
dg:asset branch (P8).RemoteRendererBridge.previewandsupportsRenderTargetdelegate to the bridge in one line each; that module has no test tree and none was added.🤖 Generated with Claude Code
A request could not say which printer resolution to render a label for: RenderRequest.targetDpi existed and nothing set it. DocumentGenerationService.Request gains a trailing targetDpi (absent, 203, 300 or 600; every earlier constructor stays and means none). It is stored in a new nullable column, target_dpi, on document_generation_request, compared on retry and lookup beside destination, media and copies, framed into the intent digest as target-dpi/v1 when stated, and passed to the renderer. A request without a target has exactly the digest it had, so earlier rows and their retries are unchanged; the same key with another target, or with none, is another intent. Nobody who asks for 300 dpi may silently get another resolution, so a target is refused before any row exists when it cannot be honoured: for a PDF, and for any renderer that does not declare the new IsolatedRenderer.supportsRenderTarget() (false by default, so an adapter written before the target cannot be handed one and ignore it). A host applies one statement, from the shipped script for its database: db/migrations/20261112-render-target-dpi-{h2,mysql,postgres}.sql (ALTER TABLE document_generation_request ADD COLUMN target_dpi INTEGER NULL). The generate process form offers the target and the request status shows it. Batches, the queue and reprints carry none. Also from the review of the ports: RenderRequest and PreviewRequest hold their own unmodifiable copy of the context (null values allowed) and print only their format and render target (and the request id), never the record data or the template source. Neither wire form changes. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>