Status (2026-02-14): Foundation complete. v0.1.13 published to crates.io, npm, and PyPI. VS Code extension MVP is implemented in-repo (syntax highlighting, diagnostics, format-on-save, hover docs, diagnostic explain actions) with Marketplace/Open VSX publish automation ready. This document captures the long-term vision, organized by phase, with honest assessments of complexity and approach.
For tactical work items, see BACKLOG.md. For the original (pre-implementation) vision document, see docs/research/archive/original-implementation-plan.md.
Everything below is done and shipped:
- Spec-first pipeline — 216 JSONC spec files, 223/223 ZPL II commands (100%), spec-compiler generates parser tables, docs bundle, constraints bundle, coverage report
- Parser — hand-written tokenizer + recursive-descent parser, opcode trie (O(k) longest-match), field data mode (
^FD/^FV), raw data mode (^GF/~DG), prefix/delimiter mutation tracking (^CC/^CD), lossless round-trip via trivia preservation - Validator — table-driven: type/range/enum/length checking, typed cross-command value state tracking (
defaultFrom+defaultFromStateKey), constraint DSL (requires/incompatible/order/emptyData/note), profile-aware bounds, printer gates, media validation, barcode field data validation (29 symbologies), 46 diagnostic codes with structured context - Formatter — spec-driven, configurable indentation, trailing-arg trimming, round-trip fidelity
- Profiles — 11 printer profiles, hardware feature gates, DPI-dependent defaults, media capabilities
- CLI —
parse,syntax-check(checkalias),lint(validatealias),format,print,explain,doctorwith--output pretty|json|sarif - Bindings — unified 5-function core API across WASM (TypeScript), Python (PyO3), C FFI, Go (cgo), .NET (P/Invoke). Native (non-WASM) targets additionally expose print/status/info APIs with typed and
_with_optionsvariants for hardware printing - CI/CD — release-plz automation, publishing to crates.io, npm, PyPI, Go module tagging, cross-platform builds
- DX distribution —
@zpl-toolchain/clinpm wrapper enablesnpx @zpl-toolchain/cliwithout requiring Rust toolchains - Hundreds of passing tests — parser, validator, emitter round-trips, golden snapshots, fuzz smoke tests, print client integration tests, preflight diagnostics, and TypeScript package/runtime coverage
These items can all start now, in parallel, with no dependencies on each other:
| Priority | Item | Phase | Effort | Value |
|---|---|---|---|---|
| 1 | Test corpus expansion | 1a | 2–4 weeks | Confidence, regression safety |
| 2 | Web playground (diagnostics only) | 2a | 1–2 weeks | Highest visibility, no-install access |
| 3 | 2b | Done (implemented in repo; first extension marketplace release pending) | Daily developer value | |
| 4 | 5a | Done (v0.1.0) | End-to-end: lint → print | |
| 5 | Performance benchmarks | 1b | 1 week | Baseline before renderer (needs corpus) |
After these, the next major effort is the native renderer (3–6 months, incremental).
Goal: Strengthen the foundation before building upward. Low risk, high value. Start immediately.
Expand from 5 samples (141 lines) to 30–50 curated labels. See CORPUS_EXPANSION_PLAN.md for the detailed plan.
- Real-world labels (15–20): curated from open-source repos (zebrash test fixtures, Neodynamic samples, GitHub gists), PII-scrubbed, license-documented
- Synthetic labels (15–30): generated from parser tables to exercise specific command families, edge cases, stress scenarios, and prefix/delimiter mutations
- Benchmarks: criterion for local perf, iai-callgrind for deterministic CI benchmarks
Effort: 2–4 weeks | Risk: Low | Depends on: Nothing
- Add criterion benchmarks for parse/validate/format across corpus
- Establish per-label timing thresholds for CI
- Profile before optimizing (the backlog lists several deferred optimizations)
Current implementation status:
crates/core/examples/pipeline_benchmark.rsincludes baseline sample labels plus synthetic scenarios (trivial,effect-heavy,field-heavy,mixed).- CI runs
scripts/check-validator-benchmark.mjsin thevalidator-benchmarkmatrix job (ubuntu/macos/windows) and fails on validate-phase per-iter regressions with OS-specific tolerance ratios. - Guardrail thresholds are intentionally conservative for CI stability and should be tightened over time (ratchet-only policy).
Threshold maintenance policy (ratchet-only):
- Run benchmark locally in release mode with stable iterations (
ZPL_BENCH_ITERS=100or higher). - Only decrease thresholds when results are consistently lower across multiple runs.
- Never increase thresholds without a documented root cause and explicit acceptance in PR notes.
- Keep the guardrail focused on
validateper-iter timing, since validator throughput is the primary contract in this phase.
Effort: 1 week | Risk: Low | Depends on: Corpus (1a)
- Added CI jobs that execute wrapper/runtime regression tests for Go and .NET bindings
- Added Python binding runtime-test coverage across Python 3.9-3.13 by building/installing the wheel and running
crates/python/testsagainst the installed module (runtime API confidence, not only compile/link checks). Optimized: PRs run a reduced subset (3.9, 3.12, 3.13); push to main runs the full matrix. - Added a dedicated TypeScript core CI job (
ts-core) that runs type-check/build/test and verifies the package-level init guard behavior - Added TS print CI integration guards: artifact assertions (
dist/test/mock-tcp-server.js,dist/test/network-availability.js,dist/test/print.test.js) plus a local TCP bind precheck so integration suites cannot be silently skipped - Tightened CI reproducibility and policy gates (
npm ci, locked FFI release builds, strict spec-table generation, constrainedmaturininstall range) - Keeps confidence coverage focused on preventing wrapper/core behavior drift
Effort: 0.5–1 week | Risk: Low | Depends on: Nothing
Goal: Make the toolchain visible and useful to developers working with ZPL daily.
The lowest-barrier, highest-visibility feature. No install, shareable URLs.
- Monaco or CodeMirror editor with ZPL TextMate grammar
- Live diagnostics using
@zpl-toolchain/core(WASM) - Format button
- Hover docs from
docs_bundle.json
Ships initially as a diagnostics-only playground (parse, validate, format, hover docs — all working today via WASM). Label preview is added later when the native renderer (Phase 3) is WASM-ready — this is Phase 3c, which upgrades the same playground with rendered output. No Labelary or third-party renderer dependency.
Effort: 1–2 weeks | Risk: Low | Depends on: Nothing (WASM bindings ready)
Skip full LSP — use WASM directly in the extension host. ZPL files are small (usually <50KB); full reparse on every keystroke is fine.
- TextMate grammar for syntax highlighting (ZPL is trivial:
^/~leaders) - Diagnostics on document change via
validate(), byte spans → VS Code ranges viaLineIndex - Format on save via
format() - Hover docs from
docs_bundle.json(command name, signature, args, description) - Explain diagnostic code action linking to
DIAGNOSTIC_CODES.md
Why not LSP: ZPL is a simple language. The LSP protocol adds JSON-RPC overhead, a separate server process, and maintenance burden — all unnecessary when WASM runs directly in the extension host with sub-millisecond latency.
Renderer integration seam (already in place):
- Extension includes a
RendererBridgestub service boundary inpackages/vscode-extension. - MVP diagnostics/format/hover pipeline remains renderer-agnostic and reusable.
- Live preview can be added additively (Webview + strict CSP + typed messages) once renderer WASM entrypoints are ready in Phase 3.
Effort: 1–2 weeks (reuses playground code) | Risk: Low | Depends on: Nothing
Add after MVP proves useful:
- Quick fixes: insert missing
^XZ, add^PW/^LL, fix barcode data - Completions: command names + signatures from spec tables
- Profile switcher: change active printer profile for validation
- Live preview: integrate renderer when available (Phase 3)
Effort: 2–4 weeks | Risk: Medium | Depends on: 2b
Goal: Produce visual output from ZPL — the feature that makes the toolchain dramatically more useful.
The original plan considered wrapping existing renderers (zebrash, BinaryKits.Zpl, Labelary). After critical evaluation, we build our own because:
- No double parsing — every existing renderer has its own parser. We'd parse ZPL twice. Our parser is more complete (223/223 commands, spec-driven). The renderer consumes our AST directly.
- Portability — a Rust renderer compiles to native AND WASM from the same codebase. Same renderer on a website, in a VS Code extension, in a CLI, in a Python script.
- Profile-aware — our renderer uses our printer profiles for DPI, page dimensions, and feature gates. No other renderer does this.
- Correctness foundation — our validator already tracks all the state the renderer needs (
^BYdefaults,^CFfont,^FWorientation,^LHlabel home,^PW/^LLbounds). The renderer is a natural extension.
ULIR: Skip. The AST + state tracking (LabelState, DeviceState, FieldTracker) is sufficient. The renderer walks the AST the same way the validator does. Add a ResolvedLayout type only if visual editing (Phase 6) creates a concrete need.
Study BinaryKits.Zpl (.NET, MIT, 388 stars, 561 commits) — the most complete open-source ZPL renderer — for its CommandAnalyzer, ElementDrawer, and font mapping patterns. Build better: spec-driven, profile-aware, portable, complete.
All WASM-compatible, actively maintained, permissively licensed:
| Crate | Purpose | License | Notes |
|---|---|---|---|
| tiny-skia (0.12) | 2D bitmap rasterization | MIT/Apache-2.0 | CPU-only Skia subset, ~200KB binary, 1.6M downloads/month, WASM-compatible |
| resvg (0.46) | SVG rendering to PNG | MIT/Apache-2.0 | Built on tiny-skia, pixel-perfect cross-platform, 3.6k stars, WASM via @resvg/resvg-wasm |
| rxing (0.7) | Barcode encode/decode | Apache-2.0 | Port of ZXing: Code 128, QR, DataMatrix, PDF417, Aztec, EAN, UPC, Code 39, ITF. SVG output via svg_write feature. WASM via rxing-wasm |
| fontdb + rustybuzz | Font loading/shaping | MIT | Part of the resvg ecosystem. For ^A@ downloadable fonts (later) |
| image (0.25) | Image I/O (PNG, JPEG) | MIT/Apache-2.0 | Reading/writing rendered output |
Typed state tracking is now implemented in crates/core/src/state/ and shared by validator and future renderer work.
Implemented producer value tracking:
^BY→BarcodeDefaults { module_width, ratio, height }^CF→FontDefaults { font, height, width }^FW→FieldOrientationDefaults { orientation, justification }^LH→LabelHome { x, y }^PW/^LL/^PO/^PM/^LR/^LT/^LS→LayoutDefaults
Consumer defaults resolve explicitly through spec metadata (defaultFrom + defaultFromStateKey) and are validated with the same type/range/profile rules as explicit args.
Two rendering backends sharing the same AST-walking + state-tracking core:
Primary: Direct bitmap via tiny-skia — render to a pixel buffer (Pixmap), closest to what a real printer does (rasterization to dots). Profile DPI determines resolution. Produces PNG output for golden tests and previews. WASM-compatible for browser/extension use.
Secondary: SVG output — generate SVG for browser-renderable, human-debuggable output. Useful for the web playground (embed directly in DOM). rxing's svg_write feature handles barcode SVG natively.
Build incrementally by command family:
- Geometry:
^GB,^GC,^GD,^GE— simple shapes via tiny-skia. 1 week - Text:
^Adevice fonts — embed font metrics per printer family (A–Z, 0–9),^CIcode pages, 0/90/180/270 rotation. 2–3 weeks - Barcodes: Code 128 (
^BC), QR (^BQ) via rxing — most common symbologies first. 2–3 weeks - Graphics:
^GFdecode — ASCII hex, binary, ACS compression, Z64. 2–3 weeks - Layout:
^FO/^FTpositioning,^FWrotation,^FBtext blocks,^LHlabel home. 2–3 weeks - Remaining barcodes: DataMatrix, PDF417, Aztec, EAN, etc. via rxing. 4–6 weeks
- Advanced:
^FR(field reverse),^A@(downloadable fonts),^GS(graphic symbols). Ongoing
crates/
renderer/ # New workspace crate (publish = false initially)
Cargo.toml # deps: tiny-skia, rxing, image
src/
lib.rs # Public API: render_bitmap(), render_svg()
bitmap.rs # tiny-skia bitmap backend
svg.rs # SVG string generation backend
text.rs # Device font metrics and glyph rendering
barcode.rs # Barcode rendering (wraps rxing)
graphics.rs # ^GF decode (ASCII hex, binary, ACS, Z64)
geometry.rs # ^GB, ^GC, ^GD, ^GE
layout.rs # AST walker + state tracking (shared with validator)
fonts/ # Embedded device font metric data
The layout.rs module shares state-tracking patterns with the validator. When mature, factor the shared code into a common module both consume.
A dedicated renderer planning doc (docs/RENDERER_PLAN.md) will be created before implementation begins, and its prerequisite research checklist is tracked in docs/BACKLOG.md under "Renderer Prerequisite: Research & Decision Gate".
Effort: 3–6 months (incremental) | Risk: Medium | Depends on: Nothing (can start anytime)
- Render all corpus labels to PNG via our renderer
- Store as golden fixtures in CI (visual regression testing)
- Compare our output against Labelary and/or BinaryKits.Zpl for validation
- Barcode verification: decode rendered barcodes via
rxing, validate data integrity
Effort: 1–2 weeks | Risk: Low | Depends on: 3a
This is NOT a separate app — it upgrades the existing Phase 2a playground with rendering capabilities. Inspired by XaViewer (Fabrizz/zpl-renderer-js):
- Profile/DPI/label-size selector (uses our existing 11 printer profiles)
- Live rendered preview (powered by our WASM renderer, replacing the diagnostics-only view)
- Multi-label support
The playground architecture (Phase 2a) is designed so the renderer slots in with zero rearchitecting — just add the WASM renderer as a dependency and wire up the preview pane.
Effort: 1 week | Risk: Low | Depends on: 2a (playground), 3a (renderer)
flowchart TD
subgraph foundation [Foundation - DONE]
Spec["216 JSONC Specs\n223/223 commands"]
Parser["Parser - AST"]
Validator["Validator\n46 diagnostics"]
Formatter["Formatter\nround-trip"]
Profiles["11 Printer Profiles"]
Bindings["Bindings\nWASM/Py/Go/.NET/C"]
end
subgraph confidence [Phase 1 - Confidence]
Corpus["Test Corpus\n30-50 labels"]
Bench["Benchmarks"]
end
subgraph devtools [Phase 2 - Developer Tooling]
Playground["Web Playground\ndiagnostics + format"]
VSCode["VS Code Extension"]
end
subgraph rendering [Phase 3 - Rendering]
NativeRender["Native Renderer\ntiny-skia + rxing\nbitmap + SVG"]
Goldens["Golden PNG Tests"]
end
subgraph generation [Phase 4 - Generation]
Builder["Label Builder API"]
Templates["Template System"]
ImageEncode["Image-to-GF Encoding"]
end
subgraph hardware [Phase 5 - Hardware]
PrintClient["Print Client ✅\nTCP/USB/Serial\nv0.1.0"]
Preflight["Preflight\nextends validator"]
end
subgraph design [Phase 6 - Visual Design]
Designer["Label Designer\ncanvas editor"]
end
Spec --> Parser --> Validator
Validator --> Formatter
Parser --> Bindings
Profiles --> Validator
Profiles --> NativeRender
foundation --> Corpus --> Bench
Bindings --> Playground --> VSCode
NativeRender -.->|"preview upgrade (3c)"| Playground
NativeRender --> Goldens
Corpus --> Goldens
NativeRender --> Designer
Builder --> Designer
Validator --> Preflight
foundation --> Playground
foundation --> PrintClient
foundation --> NativeRender
foundation --> Builder --> Templates
Builder --> ImageEncode
Goal: Produce ZPL programmatically — the other direction from parsing.
A Rust API for constructing valid ZPL labels programmatically:
let label = LabelBuilder::new()
.page(width_mm: 100, height_mm: 50, dpi: 203)
.text("Sample ID: ABC-123", x: 10, y: 10, font: 'A', height: 30)
.barcode(Symbology::Code128, "ABC123", x: 10, y: 50, height: 80)
.qr_code("https://example.com", x: 200, y: 10, magnification: 5)
.build(); // → String (valid ZPL)Uses spec tables to ensure generated ZPL is valid. Exposed through all bindings.
Effort: 3–4 weeks | Risk: Low | Depends on: Nothing (uses existing emitter)
Variable substitution in ZPL files (.zplt template format):
^XA
^FO50,50^A0N,30,30^FD${patient_name}^FS
^FO50,100^BQN,2,5^FDMA,${sample_id}^FS
^XZ
- JSON Schema for template inputs (type checking, required fields)
- Deterministic transforms (date formatting, uppercase, checksums)
- Batch rendering with data arrays
Effort: 2–3 weeks | Risk: Low | Depends on: Nothing
Convert images (PNG, JPEG, BMP) and PDFs into ZPL ^GF graphic fields for embedding logos, photos, and complex graphics in labels. This is a core utility — nearly every label with a logo needs it.
let gf = encode_image("logo.png", &EncodingOptions {
dpi: 203,
compression: Compression::Z64, // or ACS
threshold: 128, // blackness threshold
dither: Dither::FloydSteinberg, // or Ordered, Threshold
trim_whitespace: true,
rotation: Rotation::None,
})?; // → String ("^GFA,...")- Grayscale conversion with configurable blackness threshold
- Dithering: Floyd-Steinberg, ordered, simple threshold
- Compression: ACS (run-length, universal printer support) and Z64 (zlib, better compression, newer printers)
- Auto-trim whitespace around edges
- Orthogonal rotation (0/90/180/270) — ZPL can't rotate
^GFat format time - CLI command:
zpl-toolchain encode-image logo.png --dpi 203 --compression z64 - Rasterize-to-
^GFfallback: accept any bitmap buffer, enabling a "whole-label-as-image" escape hatch for content that can't be natively expressed in ZPL
Exposed through all bindings. The algorithms are well-documented in the ZPL Programming Guide and multiple open-source implementations exist to study (zpl-image, zplgfa, zebrafy).
Effort: 2–3 weeks | Risk: Low | Depends on: Nothing
Goal: Connect the toolchain to physical printers.
Note: Phase 5a (print client) is low effort and has zero dependencies on rendering or generation. It completes the end-to-end workflow: parse → validate → print. Consider starting it alongside Phases 1–2 rather than waiting for the renderer.
Both ZPL and SGD travel over TCP port 9100:
- ZPL (
^XA...^XZ): 223 commands for label content. This is what we've built. - SGD (
! U1 setvar/getvar/do): Hundreds of key-value settings for device configuration (device.languages,media.type,print.tone, etc.). Model-dependent, scattered documentation.
We build a ZPL print client. SGD is deferred (see Deferred section), but the transport layer is designed with SGD in mind — a simple printer.send_zpl() vs a future printer.send_sgd() abstraction costs nothing now and avoids a rewrite later. Note: send_sgd() does not exist today; it is listed here as a future aspiration to show that the transport design accommodates it.
Implemented 2026-02-08. TCP, USB, and serial/Bluetooth SPP transports with split
Printer/StatusQuerytrait design. See research/ZPL-PRINT-CLIENT-PLAN.md for the design plan and PRINT_CLIENT.md for the user-facing guide.
The crates/print-client/ crate provides:
- Three transports: TCP (port 9100, default), USB (
nusb, feature-gated), Serial/BT SPP (serialport, feature-gated) - Split trait design:
Printer(send-only) +StatusQuery(bidirectional) — all transports implement both - Status parsing:
~HS→HostStatus(24 fields),~HI→PrinterInfo(model, firmware, DPI, memory) - STX/ETX frame parser: byte-level state machine, transport-agnostic (
impl Read) - Retry with exponential backoff:
RetryPrinter<P>wrapper, jitter,is_retryable()classification;ReconnectRetryPrinter<P>with automatic reconnection viaReconnectabletrait - CLI integration:
zpl print <file.zpl> --printer <addr>with--no-lint,--strict,--dry-run,--status,--wait - Bindings:
print_zpl[_with_options]()plusquery_printer_status[_with_options]()andquery_printer_info[_with_options]()in bindings-common (cfg-gated, not WASM); Go/.NET wrappers expose raw + typed query helpers - TypeScript: separate
@zpl-toolchain/printpackage (pure TS,node:net, browser proxy with WebSocket support); batch API (printBatch(labels, opts?, onProgress?),waitForCompletion()) withBatchOptions/BatchProgress/BatchResulttypes
Effort: Done | Risk: — | Depends on: Nothing
Analyze a label before sending to printer. Initial preflight diagnostics are implemented:
- ZPL2308 — Graphics bounds (
^GFexceeds printable area based on^PW/^LLor profile page bounds) - ZPL2309 — Graphics memory estimation (
^GFtotal memory exceeds printer RAM from profile) - ZPL2310 — Missing explicit label dimensions (
^PW/^LLcommands) - ZPL2311 — Object bounds checking (estimated text/barcode overflow beyond effective label bounds)
- Media mode sanity (
^MN/^MT/^MMvs profile) viaZPL1403validator checks - Missing required commands via spec-driven
requiresconstraints (ZPL2101)
This is an extension of the validator, not a new system. Preflight now covers graphics bounds/memory, explicit dimension portability hints, object bounds, media sanity, and required-command checks.
For a future precision pass on heuristic object-bounds estimation (ZPL2311), see the deferred backlog item in BACKLOG.md ("Preflight ZPL2311 precision upgrade (much later)").
Effort: 1–2 weeks (remaining items) | Risk: Low | Depends on: Nothing (extends validator)
- Discovery: mDNS/Bonjour for network printers
- Emulator: virtual printer on port 9100, renders incoming ZPL (requires renderer)
Effort: 4–8 weeks | Risk: Medium | Depends on: 5a, 3a (renderer)
Goal: WYSIWYG label designer — the capstone product.
Split view: ZPL code editor ↔ visual canvas. Edit either side, round-trip via generator + renderer.
- Drag-and-drop primitives (text, barcode, graphic, box)
- Snap-to-grid, alignment guides
- Profile-aware bounds visualization
- Template data preview
Depends on: Renderer (3a), Generator (4a) | Risk: High | Effort: 3–6 months
Browser-based label designer using WASM bindings. Same engine as VS Code panel but standalone.
Depends on: 6a, Playground (2a) | Risk: High | Effort: 3–6 months
Ideas from the original plan that we've consciously decided to defer or drop, with rationale:
| Idea | Rationale | Revisit When |
|---|---|---|
| ULIR (Unified Label IR) | The AST + state tracking is sufficient for rendering. Add a ResolvedLayout type only if visual editing or cross-backend support creates a concrete need. |
If building a visual designer (Phase 6) |
| Multi-backend IR (EPL/TSPL/ESC-POS) | Transpiling between fundamentally different printer languages is unproven and premature. Each language has unique semantics. | If there's real demand, build separate adapters (AST → EPL), not a universal IR |
| Schema v2 | Current v1.1.1 schema works well. The proposal adds complexity (opcode-keyed maps, paramGroups, familyRegistry) without clear benefit yet. | When the current schema becomes a bottleneck for new features |
| Label ABI | Versioned template metadata is useful, but premature before templates exist. | After template system (4b) is proven |
| BLAKE3 structural hashing | Nothing to hash yet. Add content-addressed caching when the renderer produces artifacts and caching becomes a measurable need. | After renderer is production-quality |
| Combo matrices | The existing constraint DSL (requires/incompatible) handles cross-command validation well. A separate matrix system adds complexity without clear benefit. |
If constraint DSL proves insufficient |
| Streaming parser/token iterator | Current parser architecture is simpler and stable; streaming introduces substantial state/diagnostic complexity without current evidence of need. | If memory/throughput benchmarks show parser bottlenecks on target workloads |
| Incremental editor parsing | Full reparse remains acceptable for current file sizes; incremental parsing adds invalidation complexity and correctness risk. | If measured editor latency exceeds thresholds on representative documents |
| Streaming corpus processing mode | Existing parse+validate flow is adequate for present workloads; dedicated streaming mode should be evidence-driven. | If real batch workloads exceed current memory/throughput envelope |
| Spooler/backpressure subsystem | Current batch/retry semantics cover present needs; spooler adds queue-state and delivery-contract complexity. | If production use requires queue fairness, backpressure, and richer delivery-state observability |
| NuGet publishing (.NET) | .NET bindings exist but aren't published to NuGet. Low demand signal so far. | If .NET users request it |
| VS Code extension publisher namespace migration | Current extension identity is trevordcampbell.zpl-toolchain. Migration to a project namespace (e.g., zpl-toolchain.*) would change extension IDs/install/defaultFormatter references and requires marketplace/Open VSX namespace ownership + token updates. |
Revisit before first public extension marketplace/Open VSX release if org branding is preferred |
| Pin GitHub Actions to commit SHAs | Current CI/release workflows intentionally use major tag refs (@v4, @v5, etc.) for maintainability while release infrastructure is stabilizing. Advisory zizmor reports unpinned-uses; hardening is desirable but operationally sensitive and should be done as a coordinated, verified sweep. |
Revisit after current release-flow stabilization window; execute as a dedicated hardening pass |
| Tighten workflow security lint policy | zizmor is intentionally advisory/non-blocking to avoid release friction during foundational CI/release work. Next step is optimizing install/runtime and then moving findings from informational signal to enforceable policy. |
Revisit after initial advisory period and findings triage |
| SGD support | SGD (! U1 setvar/getvar/do) is a massive, model-dependent key-value store for printer device configuration. Our core value is label correctness, not printer fleet management. The print client's transport layer is designed to be SGD-aware (Phase 5), so adding SGD later doesn't require rearchitecting. Could become a separate crate (zpl_toolchain_sgd) sharing the transport layer. |
If printer fleet management features are requested |
| HTML/CSS label authoring | Design labels using HTML/CSS/Tailwind and convert to ZPL. Three possible approaches: (A) rasterize HTML to bitmap → ^GF (simple but entire label is one image), (B) semantic mapping of HTML elements to ZPL commands (extremely complex — CSS layout is massive), (C) constrained label DSL using HTML syntax with ZPL-specific components. Approach A is enabled by image encoding (4c); approaches B/C need the visual designer infrastructure. Inspired by zplbox and Universal-ZPL-Generator. |
After Phase 6 visual designer proves the "high-level → ZPL" concept |
| PDF-to-ZPL conversion | Convert PDF pages to ZPL labels (page → bitmap → ^GF). Useful for legacy label designs that exist only as PDFs. Straightforward once image encoding (4c) exists — just need a PDF rasterizer (Rust crates: pdf-render, pdfium-render). Inspired by PDFtoZPL and zebrafy. |
After image encoding (4c) is complete |
| Idea | Rationale |
|---|---|
| WASM Rulelets | Enterprise fantasy. Sandboxed WASM modules with ABI versioning, quotas, and allowlists — for custom lint rules that nobody has asked for. If custom rules are needed, config files or Rust plugins are simpler. ESLint plugins aren't WASM modules. |
Query/Response schemas (~HM, ~HS, ~HQES) |
Niche. Parsing printer status responses belongs in a printer management tool, not a ZPL parser/validator. The print client (5a) can send ~HS and parse the response without a formal schema system. |
| Full LSP server | Unnecessary for ZPL. WASM runs directly in the VS Code extension host with sub-millisecond latency. The LSP protocol adds JSON-RPC overhead, a separate process, and maintenance burden. Direct WASM integration is simpler and faster. |
| "Micro-kernel" plugin architecture | The current module structure (parser, validator, emitter, bindings) is already well-factored. Don't add plugin interfaces until there are multiple implementations to swap. |
| Deterministic clock / security theater | Input size limits and log redaction are good practice. "Deterministic clock for templates" and enterprise security features are premature. Add them if a real security audit demands them. |
Done:
Phase 5a: Print Client ✅ (TCP/USB/serial, CLI, bindings, TypeScript)
Immediate (can all start now):
Phase 1: Corpus ──── Benchmarks
Phase 2: Playground (diagnostics) ──── VS Code MVP ──── Enhanced
Medium-term:
Phase 3: Native Renderer ──── Golden PNGs
│
└──── Playground Preview Upgrade (3c)
Phase 4: Label Builder ──── Template System ──── Image Encoding
Later:
Phase 5b: Preflight ──── 5c: Emulator (needs renderer)
Phase 6: VS Code Designer ──── Web Designer (needs renderer + builder)
Phase 5a is complete. Phases 1, 2, and 4 are fully independent and can proceed in parallel. Phase 3c upgrades the Phase 2a playground (same app, not separate). Phase 5c and Phase 6 depend on the renderer.
- Spec-first, data-driven — parser tables, validator rules, docs, and completions all derive from the same JSONC spec files
- Ship incrementally — every phase delivers standalone value; no "big bang" releases
- The AST is the universal interface — the renderer, generator, preflight, and print client all consume the same AST. No ULIR, no separate IR. The AST + state tracking is the shared foundation
- Own the renderer — build it in Rust, portable to WASM. Study BinaryKits.Zpl for inspiration, but build better (spec-driven, profile-aware, complete command coverage). No third-party renderer dependencies
- Profile-aware everywhere — the renderer uses printer profiles for DPI, page dimensions, and feature gates. The web playground exposes a profile switcher. The validator and renderer share the same profile data
- Shared state tracking — the renderer walks the AST the same way the validator does. Both need
^BYdefaults,^CFfont,^FWorientation,^LHlabel home,^PW/^LLbounds. Factor this into a shared module - SGD-aware transport — the print client uses TCP 9100 for ZPL delivery. The transport abstraction is kept clean enough that SGD commands could plug in later without rearchitecting. SGD itself is deferred, not dropped
- Proven crates for hard problems —
rxingfor barcodes,tiny-skiafor rasterization,resvgfor SVG-to-PNG. Don't reinvent what's already solved well in Rust - Offline by default — every feature works without network; remote services are opt-in
- Deterministic outputs — same input → same output, regardless of platform or concurrency
- Small sharp tools — each sub-tool (parse, validate, format, render, print) is useful alone and composes cleanly
| Date | Change |
|---|---|
| 2026-02-20 | Expanded validator benchmark guardrail to an OS matrix (ubuntu/macos/windows) with OS-specific tolerance ratios and promoted conformance checks into explicit runtime lanes (rust, ts-print). |
| 2026-02-20 | Added validator benchmark guardrail to CI (validator-benchmark job) and a ratchet-only threshold maintenance policy under Phase 1b. |
| 2026-02-20 | Added COMPATIBILITY_POLICY.md: compatibility/deprecation governance, conformance CI gates, drift-prevention checklist. |
| 2026-02-20 | Cross-surface conformance hardening pass: TypeScript ~HS parsing defaults to strict mode (with explicit strict/lenient exports), TS tcpQuery uses strict STX/ETX frame-count semantics for known framed commands, spec-compiler now hard-fails on mixed/unexpected schema versions, JSONC comment stripping unified into shared crates/jsonc-strip, and a shared contracts fixture baseline (contracts/fixtures/bindings-parity.v1.json) now drives bindings parity tests. |
| 2026-02-16 | Official quality/refinement execution pass: CRLF/CR lexer correctness landed; semicolon comment semantics removed in favor of official ^FX comments; VS Code syntax/comment UX aligned to ^FX ... ^FS; TS print tcpQuery now handles framed bursty ~HS/~HI responses robustly; validator internals split into focused modules without behavior drift; strict parser-table policy applied across CLI/bindings (explicit > embedded > actionable hard error); fuzz coverage expanded and core benchmark harness added. |
| 2026-02-14 | VS Code extension polish + release hardening: richer completions with category/scope metadata, extension integration coverage expanded (including non-default formatter settings), and linux/arm64 extension-test runner auto-detects local editor CLIs for better local architecture coverage. |
| 2026-02-08 | Implemented Phase 5a: print client with TCP/USB/serial transports, split Printer/StatusQuery trait design, STX/ETX frame parser, ~HS/~HI status parsing, retry with exponential backoff, CLI integration (zpl print), Python/FFI bindings, TypeScript package. Design doc and user guide added. |
| 2026-02-08 | Council review: clarified Phase 2a/3c relationship (same app, not separate). Removed Labelary dependency. Added state tracking refactor as Phase 3 prerequisite. Elevated Phase 5a (print client) to immediate priorities. Fixed dependency graph and mermaid diagram. Cleaned up backlog conflicts. |
| 2026-02-08 | Added Phase 4c: image-to-^GF encoding. Deferred HTML/CSS label authoring and PDF-to-ZPL conversion with clear revisit triggers. |
| 2026-02-08 | Revised Phase 3: native-first renderer (tiny-skia + rxing), no bridge. Added SGD-aware transport to Phase 5. SGD moved from Dropped to Deferred. Expanded architectural principles. |
| 2026-02-08 | Initial roadmap, synthesized from original plan + critical review |