# Epic CLI — Agent Guide

A field manual for AI agents (and humans) driving `java-epic-cli.jar`. It covers every mode,
the stdout/exit-code contracts you must parse, and the landmines that are not obvious from
`--help`. The jar's built-in help is authoritative for flag details — this guide tells you
how to *use* it safely and what it's *for*.

```
java -jar java-epic-cli.jar --help            # full mode list
java -jar java-epic-cli.jar --help=<topic>    # preflight | colors | thin-line | split |
                                              # transforms | imposition | s3 | spooler |
                                              # compile | vectorize | backers | derivatives
```

## What this tool is

Epic CLI is the PDF preflight and print-production engine for EpicERP (`php-heatapplied`) and
DrivenARC (`php-driven-arc`). Both shell out to the jar at the repo root
(`/datahandler/repos/java-epic-cli/java-epic-cli.jar`) and parse its stdout JSON directly.
It is infrastructure, not a product: **its flags, JSON shapes, and exit codes are contracts.**

Domains it covers: preflight reports (fonts, colorspaces, spot/process ink usage, raster DPI,
thin lines), spot-color inspection/renaming, page transforms, imposition + flow layout,
raster→vector tracing, vector object extraction, path simplification, design compilation from
layer JSON, master derivatives generation, production-backer generation, and print spooling.
It vendors its own PDFBox fork (`org.apache.pdfbox` sources in-repo) tuned for low RAM.

## Rules of engagement (read before invoking)

1. **stdout is machine output.** Report/standalone modes print exactly one JSON document on
   stdout; diagnostics go to stderr. **Never merge streams (`2>&1`) into what you parse.**
2. **Exit codes:** `0` success · `1` command syntax / usage error · `2` file, S3, or
   environment fault. `--imposition` recolor adds `3` = a recolor entry named a separation
   the source PDF doesn't have (message lists the actual separations). `--derivatives` uses
   partial-batch semantics: exit `0` even when individual datasets fail (per-dataset
   `status` in the JSON); `2` only for an unreadable package/sidecar.
3. **Flag syntax matters.** Leading-mode flags are case-insensitive and prefix-matched.
   Standalone modes take their argument with `=` (`--backers=<file>`, `--compile=<req>`,
   `--derivatives=<pkg>`); the positional form (`--backers file.pdf`) fails with a normal
   JSON error object on stdout (`{"status":"error","reason":"unknown option: ..."}`) and
   **exit 1**. If you parse stdout without checking the exit code, key on `"status"`.
4. **Omitting `[output]` on a transform overwrites the source in place.** Always pass an
   output path unless in-place is truly intended.
5. **Transforms combine and run in the order given** (`--rotate90 --mirror in.pdf out.pdf`).
   If a listing mode is present, every other mode is skipped.
6. **S3 everywhere:** any path may be `s3://bucket/key` (alias `aws://`). Needs `--aws=FILE`
   or `./aws.ini` (auto-detected). No credentials + s3 path → exit 2.
7. **`--quiet`** suppresses output, exit code is the result — except `--compile`, which always
   prints its one JSON result object.
8. **Java version:** built for Java 8 bytecode; runs on JDK 8/11/17/23. **Never run on the
   JDK 21 line** — a JDK 21 `java.awt.geom.Area` regression fragments `--extract-objects`
   output ~55×.
9. **Memory:** the jar has bounded-scratch handling and runs real production files at modest
   heaps (most modes fine at `-Xmx768m`; backers ~96m, preflight of a 76MB AI master 256m).
   Don't hand it a huge `-Xmx` reflexively.

## Mode catalog

| Mode | Kind | One-liner |
|---|---|---|
| `--preflight` | report | Master intake report: metadata, geometry, fonts, colorspaces, spot/process usage, raster DPI, AI template variables |
| `--list-spot-colors` | report | Spot (Separation/DeviceN) colors + CMYK equivalents + `utilized` flag (+ `hiddenOnly` when all ink is optional-content-off) |
| `--list-process-colors` | report | CMYK channel usage (vector + raster) |
| `--thin-line=P:N[:MODE]` | report+PNG | Strokes < P pt / gaps < N pt, violation overlay PNG per page; `raster` (default) or `contour` |
| `--split` | transform | One PDF per page (`%d` or `_N` naming), JSON file list |
| `--rotate90/180/270`, `--mirror`, `--resize=WxH`, `--create=WxH` | transform | Page transforms; `--create` starts from a blank sheet |
| `--swap-spot-colors=SPEC` | transform | Rename spot colors + replace CMYK fallbacks everywhere (resources, XMP, AI private data) |
| `--imposition=PLAN` | transform | JSON-plan sheet layout: placements, flow layouts (arc-warp text), registration marks, per-placement recolor |
| `--spool` | action | Magic-byte PDF/RAW print routing (Windows native spooler / CUPS) |
| `--compile=REQ` | standalone | Artwork Designer layer-JSON → print-ready spot-separated PDF (shapes, artwork placement+recolor, outlined or live text, arc warp) |
| `--vectorize` / `--rvp` | standalone | Raster image → flat spot-color vector PDF (palette discovery/snapping, seam welding, curve fitting, simplify); PDF input = stencil plate lifting |
| `--extract-objects` | standalone | Exact lift of a vector PDF's filled paths into editable objects (never traced); `--with-text` reports live text |
| `--simplify-curves SRC OUT` | standalone | Illustrator-style path simplification (default tolerance 0.35pt); also inline via `--simplify-curves` on vectorize/extract |
| `--backers=ART` | standalone | Art + PRODUCTION_ONLY layer of spot backer plates from `--plates=` expressions (spread/choke from painted ink), grafted natively into AI private data |
| `--derivatives=PKG` | standalone | Master + sidecar XML batch → per-dataset working and `_production` PDFs (lab + real-AI masters) |

Options accepted alongside modes: `--aws=FILE`, `--mysql=FILE`, `--quiet`, `--help[=T]`,
`--version`.

## The contracts you'll parse

### `--preflight` (the master intake report)
JSON on stdout; source never modified. Document level: size/name/creator/author/title/
subject/keywords + `variables` (Illustrator template variables parsed out of AI private
data). Per page: `rotationInDegrees`, all page boxes in points, colorspace-family booleans
(**including inside raster images and nested form XObjects** — if spot detection ever looks
emptier than expected, suspect a stale/regressed jar before a data problem), `processColors`,
`spotColors` (with `utilized`), `liveTextExists`/`fonts` (embedding detail),
`vectorElementsExist`, `strictlyVector`, `rasterImagesExist`/`rasterImages`/
`minimumEffectiveDpi`, and `thinLine`/`thinLineContour` when combined with `--thin-line`.

### Spot colors
`utilized` means ink actually appears: vector fill/stroke, usage nested in form XObjects,
or raster images whose colorspace is that spot (pixels are sampled — a present-but-blank
plate is `utilized: false`). A spot whose every use sits in optional content switched OFF by
the document's default configuration (OCProperties `/D /OFF` — e.g. a hidden `!` backer layer
in the no-AI-payload fallback, or any AI-saved file with a hidden layer)
additionally reports `hiddenOnly: true` in both preflight and `--list-spot-colors`; the field
is absent otherwise, and `utilized` stays true for such ink. Swap spec (quote it): `"Old=New:C,M,Y,K"` with `;` chaining,
CMYK 0–1. Swaps propagate to XMP and Illustrator private data so the file survives reopening
in AI.

### `--imposition=PLAN`
Plan JSON: `width`/`height` (inches, blank sheet) or `file` (base sheet), `destination`,
`rotation`, `mirror`, `force_width`/`force_height`/`preview`, `pdf_documents` placements,
`flow_layouts` (names/numbers runs; text may carry `"warp": {"style":"arc","bend":-100..100}`
→ outlined arched text; destination becomes a `.zip`), `registration_marks`,
`print_rotation`, `metadata`. A `pdf_documents` entry may carry a `recolor` map
(source separation name → new name/CMYK) — an entry naming a missing separation exits `3`
listing the file's actual separations. Flow-layout teams that don't fit are reported in a
skip JSON on stdout; silence = nothing skipped.

### `--compile=REQ` (Artwork Designer)
Request: `schema_version: 1`, `destination`, `design{artboard, colors[], fonts[], layers[]}`.
`colors[].pms` is the **literal output separation name**; CMYK 0–100. Fonts resolve to
`/datahandler/fonts/{Family}-{Style}.ttf` (TrueType only). Layers draw in array order
(0 = bottom); each needs a unique `id`. Layer types: `artwork` (box-fit + `recolor` map —
incomplete recolor fails the layer), `shape` (rect/ellipse/star/polygon), `text` (single
line; outlined by default; `live_text` emits real editable text with the face fully
embedded — warped layers always stay outlined and get a `.mapping.json` sidecar;
live_text + stroke fails). Result: exactly one JSON object —
`{"status":"ok","destination":...,"colors_used":[...]}` (+`"mapping"` when a sidecar was
written) exit 0, or `{"status":"error","layer_id":...,"reason":...}` exit 1. No silent skips.

### `--vectorize` / `--extract-objects` (RVP)
Rich option surface — see `--help=vectorize`. Key semantics agents get wrong:
- `--dpi` is an **assertion about the input**, not resampling. Low-res sources auto-upscale
  (bicubic on coverage) unless `--dpi` ≥ 300 or `--no-upscale`.
- `--max-colors=N` set to the job's known ink count is the single best artifact-killer.
- `--paper=none` is the **default** (white may be a real plate); pass `--paper=auto` to drop
  near-white.
- Seam welding is on by default (kissing fit, trapping is the printer's job); `--no-weld`
  reverts.
- `objects[]` in the report carry absolute SVG path data, inches, y-down, nonzero fill —
  `M/L/Z` forever by default; cubic `C` appears only when `--fit=curves`/`--simplify-curves`
  are opted into.
- `--extract-objects` is exact geometry, never traced; raster/gradient/live-text input
  refuses with `not liftable:` on stderr (`--with-text` tolerates text and reports it in
  `texts[]`, never outlines it).

### `--backers=ART.PDF`
`--plates="<expr>[,<expr>...]"` is **always explicit** (per-order policy, no default) and is
an open expression DSL — the jar executes geometry and swatching, the caller carries the
plate rules: `[!]spot('<Name>',C,M,Y,K)[±offset][@spot|@area]`. Name/CMYK define the
Separation colorant and its display alternate (either quote style); `±offset` is per-side
dilation in pt (positive spreads, negative chokes, omitted = exact; reference press numbers
adhesive +0.5, underbase −1.0, clear +0.25); geometry defaults to the union of all painted
ink at 300dpi (alpha-derived: SMasks, clips, CTMs honored), `@spot` covers only where that
same colorant is painted in the art, `@area` the full artboard. Plate outlines are
**smoothed by default**: the traced pixel staircase is replaced with cubic Beziers within
0.2pt of the traced coverage, corners pinned (same Schneider stage as `--vectorize
--fit=curves`); when the art's raster content sits below the render dpi the tolerance scales
with the source's effective resolution (renderDpi/imageDpi, capped 8x) so coarse pixel
blocks smooth instead of reading as geometry; `--no-smooth` keeps the raw pixel-step
polygons. `!` hides the plate — hidden
until an operator un-hides the layer in Illustrator and re-saves; takes effect only when ALL
plates are hidden. When the AI payload graft succeeds the hidden layer exists ONLY in that
payload (the PDF stream carries no paint — no PDF viewer can show it; AI regenerates the
paint on re-save after un-hiding). Without a graftable payload it falls back to a PDF
OCG-`/OFF` span — gs/Acrobat/poppler-splash hide that, but poppler-cairo viewers (evince)
paint it, so the fallback is viewer-dependent and warned. Plates stack in listed order bottom-to-top (list underbase first for the
reference press stack). The 2026-09-04 keyword forms (`--plates=adhesive,...`,
`--adhesive=`/`--underbase=`/`--clear=`) are retired and produce usage errors. Output
defaults to `<art>_Production.pdf`. One JSON result: `status`, `source`, `production_pdf`,
per-plate `plate/colorant/offset_pt/geometry/hidden/contours/vertices`, `layer_hidden`,
`coverage_bbox_pt`, `warnings`.
When the source is an AI-exported PDF, the PRODUCTION_ONLY layer is **grafted natively into
the AI private data** (PieceInfo → Illustrator → Private), so Illustrator's Layers panel
shows `PRODUCTION_ONLY` with one child object per plate above the art layer, with PieceInfo
preserved and palette/`%%CMYKCustomColor` swatch entries inserted for any colorant the
payload doesn't already define. Fallback (no/foreign/already-grafted private data): PieceInfo is
stripped and a warning says so. Warnings are additive — parse `status`, not warning count.

### `--derivatives=PKG`
Package = master PDF + `Fonts/` + sidecar XML (same basename, Adobe variable-data dialect,
`ha:out` per dataset), as ZIP or directory. Per dataset: `<ha:out>.pdf` (working —
PRODUCTION_ONLY hidden, artboard refit to remaining ink) and `<ha:out>_production.pdf`.
Binding is exact-placeholder-match (zero or multiple matches skip with a warning, never a
guess); empty value hides the frame; all text outlined. Two flavors auto-detected: `lab`
(synthesized live-text master) and `ai` (real Illustrator master — envelope meshes recovered
from private data, replacement text warped through them). Partial-batch: failed datasets
report `"failed"` + reason, batch still exits 0.

## Landmines (hard-won, believe them)

- **JDK 21 is forbidden** (Area regression; see rule 8).
- **`--create=4x6` is rejected** — an arg-length guard fires before the regex; use
  `--create=4.25x6.5`-style specs (>12 chars). `--create` is not a mode by itself; combine
  it with a transform or `--imposition`.
- **Never edit `%AI5_NumLayers`** if you touch AI private data by hand — Illustrator counts
  layers itself; editing the header hard-crashes it on open.
- **AIPDFPrivateData blocks may be individually Flate-filtered** — always read them decoded,
  never raw, before concatenating and zstd-decompressing (`%AI24_ZStandard_Data` header +
  65536-byte chunks).
- **Illustrator ignores PDF-content layer marking** (`/OC` OCGs, `/Layer` BDC,
  `OCProperties`) when building its Layers panel — named layers exist only in the PieceInfo
  private data. Don't "fix" a file by adding OCGs and expect AI to show them.
- **A jar that runs but reports emptier output than expected** (missing spot colors, zeroed
  thin-line data) is the signature of a stale/dirty-tree build, not a data problem. See
  CLAUDE.md ("Critical Build Rule") and commit `ded7016`'s history.
- `--spool` has deliberately **no retry**; it needs a real print subsystem (Windows spooler
  or CUPS) — untestable in most sandboxes.

## For agents modifying this repo

- **Build with `mvn verify`, never `mvn package`** — only `verify` runs `post-build.sh`,
  which refreshes the root `java-epic-cli.jar` the PHP consumers invoke. Stopping at
  `package` ships a stale jar silently.
- **Commit source before building.** Never build from a dirty tree.
- **Gate every jar change on the parity battery** (23 runs; byte-identical preferred,
  raster+JSON-identical modulo `elapsedMs`/path-echo acceptable). See `ENGINE_REWRITE.md`
  for the harness and the vendored-engine history.
- **Contract stability:** flag names/behavior, JSON field names/shapes, and exit codes never
  change without coordinating — `php-heatapplied` (heatapplied-dev) and `php-driven-arc`
  (arc-dev) parse them directly. Additive fields/warnings are safe.
- **Deploys** are cli-dev + devops-admin, pending Jesse's explicit approval. No exceptions.
- MySQL schema questions → db-admin. The CLI only holds a connector (`mysql.ini`).

## Quick recipes

```bash
# Intake a customer file
java -jar java-epic-cli.jar --preflight artwork.pdf > report.json

# Which inks are really used?
java -jar java-epic-cli.jar --list-spot-colors artwork.pdf

# Rename a plate everywhere (survives reopening in Illustrator)
java -jar java-epic-cli.jar --swap-spot-colors="Spot Gloss=HA Clear:0,0,0,0.3" in.pdf out.pdf

# Trace a logo raster into a 2-ink vector PDF with smooth curves
java -jar java-epic-cli.jar --vectorize --input=logo.png --output=logo.pdf \
    --max-colors=2 --fit=curves --simplify-curves --report=logo.json

# Production file with the reference three-plate press stack (AI-navigable layers)
java -jar java-epic-cli.jar --backers=art.pdf \
    --plates="spot('HA Underbase',0,0.5,0.25,0)-1.0,spot('HA Clear',0.5,0,0.25,0)+0.25,spot('HA Adhesive',0.25,0.5,0,0)+0.5" \
    --output=art_Production.pdf

# Generate derivatives from an ERP batch package
java -jar java-epic-cli.jar --derivatives=W2302N-DerivativeBatch.zip --output=out/
```
