# Comic Json Compiler

> Phase-1 (S9, the FINAL comic-author step) — assemble the LOCKED storyboard + locked per-panel blueprints into ONE schema-valid `comic.json` (the comic-ir/1.0 contract boundary handed to comic-director / run_comic.py). Project the page_order + each panel's condition{} + render fields into `pages[]` + keyed `panels{}` per schemas/comic.schema.json; author ONLY authored fields and leave image_path/a…

- **Type:** Skill
- **Install:** `agentstack add skill-wanshuiyin-aris-movie-director-comic-json-compiler`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [wanshuiyin](https://agentstack.voostack.com/s/wanshuiyin)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [wanshuiyin](https://github.com/wanshuiyin)
- **Source:** https://github.com/wanshuiyin/ARIS-Movie-Director/tree/main/skills/comic-json-compiler
- **Website:** https://wanshuiyin.github.io/ARIS-Movie-Director/comic/

## Install

```sh
agentstack add skill-wanshuiyin-aris-movie-director-comic-json-compiler
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# comic-json-compiler — locked storyboard + blueprints → one schema-valid `comic.json` (S9)

The **last node of the left third of Figure 1**: take the locked `storyboard_spec` (the authoritative page
order + the MOTIF STATE TABLE) and the locked per-panel `blueprint` nodes and **compile** them into the single
`comic.json` (comic-ir/1.0) that is the contract boundary to [`comic-director`](../comic-director/SKILL.md)
(Phase 2/3). You don't hand-write this file panel-by-panel — that already happened upstream; this step is the
deterministic *projection* + *reconciliation* that turns the authoring nodes into the IR and **catches the
integrity bugs that only show up once everything is assembled** — chiefly the page-count / orphan-panel
mismatch (the **orphan-panel class**). The two deterministic scripts the compile gate runs are
**per-page** (`run_comic.py` needs `--page`/`--panels`) and the JSON schema leaves `condition`/`content_svg`
OPTIONAL — so **neither of them sees the whole `pages[]`/`panels{}` set and neither catches an orphan.** That
whole-comic reconciliation against the storyboard authority is **THIS skill's own inline job** (the ~7-line set
diff below), run *before* the gate; the gate then certifies the per-page + wiki-conformance floor.

```text
  locked storyboard_spec (page_order = AUTHORITY)            ┐
  locked motif_ledger    (per-panel ground-truth)           │
  locked panel_spec × N  (the 9-field specs)                 ├─▶ ① ASSEMBLE  pages[] + panels{}  per comic.schema.json
  locked blueprint × N   (content_svg + expected_literals)   │        (author ONLY authored fields; engine fields EMPTY)
  locked prompt_bundle × N (optional, identity refs)         ┘        ▼
                                                                ② RECONCILE page-count integrity  ── INLINE python (this skill; no phantom script)
                                                                   (orphans · dangling refs · page_count vs page_order · grid2x2==4 · recap legitimacy)
                                                                      ▼
                                                                ③ VALIDATE  jsonschema(comic.schema.json) — SHAPE only (it does NOT enforce content_svg/literals)
                                                                      ▼
                                                                ④ FAIL-CLOSED contracts  (every panel a content_svg · baked figure ⇒ expected_literals)  ← THIS skill asserts; schema won't
                                                                      ▼
                                                                ⑤ comic-cross-layer-gate decision:compile_ --gate compile  (DETERMINISTIC: run_comic.py --dry-run per page AND cli/validate_wiki.py both exit 0)
                                                                      ▼
                                                            approve? ─ revise ─▶ fix the OFFENDING node upstream, recompile  (you don't invent content)
                                                                      │ approve
                                                                      ▼
                                                                ⑥ EMIT comic.json (output-versioning) + movie.project.json pointer-hub + asset-copy manifest
```

## Constants
- **CONTRACT BOUNDARY** = `comic.json` (`comic-ir/1.0`) + the assets it references. Everything downstream
  (`comic-director`, `run_comic.py`, the Phase-3 viewer build `packages/viewer/build_comic.py`) reads ONLY this
  file + its assets — so a bug that survives this step survives to production.
- **AUTHORITY = the storyboard's `page_order`.** When the assembled `comic.json` page set disagrees with the
  locked `storyboard_spec.page_order`, the **storyboard wins** — the compiler conforms, it does NOT silently
  ship its own page count. (This is the converse of the storyboard gate's page-order veto.)
- **GATE** = `comic-cross-layer-gate decision:compile_ --gate compile` (the target is the **real wiki
  `decision` node** this skill writes — there is NO `comic` node type and `comic.json` is not a wiki node, so it
  can never be a gate target or an edge endpoint). Per [`comic-cross-layer-gate`](../comic-cross-layer-gate/SKILL.md)
  the `compile` gate is **purely DETERMINISTIC**: NO `review:*` fan-out, NO Codex adjudication (so unlike every
  other gate, the "create review nodes first" pre-step does NOT apply here). It PASSES (`approve`) **iff BOTH
  on-disk scripts exit 0**: `python3 skills/comic-director/scripts/run_comic.py --project  --page 
  --panels  --dry-run` (per page) AND `python3 cli/validate_wiki.py `; else `revise` with their
  stderr as the blocker list. (The whole-comic orphan/page-count catch is NOT in either script — it is this
  skill's inline step ② below, run before the gate.)
- **VERDICT enum** (the `compile` gate's legal set, per the gate skill): `approve` / `revise`. On `approve` flip
  the compile `decision` to `final` and emit; on `revise` fix the **offending upstream node** and recompile (the
  compiler never patches content into `comic.json` by hand). A *verdict* is never a node `status`: the status the
  FLIP writes is `locked` on advance, `under_review` on needs-work, `rejected` on a terminal fail
  (schema-enum statuses only — `revise`/`regenerate` are verdicts, not statuses).
- **NO INVENTION** — the compiler is a *projection + reconciliation* step. It must NOT invent a panel, a
  number, an `expected_literal`, a page, or a `content_svg`. A missing piece is a HALT with the offending node
  id, not a fabrication ([`artifact-integrity`](../../protocols/artifact-integrity.md)).
- **ENGINE FIELDS STAY EMPTY** — `image_path` / `active_attempt_id` / `wiki_node_id` are written by the spiral
  engine on KEEP. Authoring them is a veto (you'd be claiming a bake that never ran).
- **ASCII node-id discipline** — every wiki touch goes through the one `node_id_to_filename` helper (`:` → `_`),
  per [`wiki-helper-resolution`](../../protocols/wiki-helper-resolution.md).

## Input contract — what must be LOCKED before you compile
This is a *compile* step: it consumes locked upstream nodes, it does not author them.
- One **`storyboard_spec`** (`status: "locked"`) — its `page_order` (the authority), `page_ids`, `panel_ids`,
  `global_policies` (page-order authority, text-mode rules, mirror-lock policy), `motif_ledger_id`,
  `consolidated_asset_requests`.
- The **`motif_ledger`** it points at — the per-panel ground-truth the gate uses to confirm `condition` prose
  agrees with each panel's continuity row.
- One **`panel_spec`** per panel (`status: "locked"`) — `page_type`, `world`, `text_mode`, `expected_literals`,
  `content_blueprint`, `bubbles`, `side_narration`, `motifs`, (cover/finale: `safe_zones` + endcard fields).
- One **`blueprint`** per panel (`status: "locked"`) — `content_svg`, `expected_literals` (verbatim),
  `safe_zones`, `html_bubbles`, `crop`, `negative_space_policy`, `generator_script`, `file_sha256`.
- *(optional)* the **`prompt_bundle`** per panel — for `identity_ref_paths` (else fall back to the project
  canonical from `identity_refs`).
- **HALT if any of these is not `locked`** (or its `content_svg`/`output_ref` file is missing) — name the
  offending node id. Convergence is the upstream gate's job, not yours.

## Workflow (the inline reconcile + the two deterministic gate scripts — all runnable, no phantom tool)

### ① ASSEMBLE `pages[]` + `panels{}`
Project the locked nodes into the IR. Copy `examples/comic_m3_audit/comic.json` as the shape.
1. **Top level** — `schema_version: "comic-ir/1.0"`, `comic_id`, `defaults` (`text_mode`, `default_locale`,
   `panels_per_page_cap`, `nav_default`, `pixel_rendering`), `identity_refs` (the locked cast hex table from
   the style bible — pure pointer/hex hub, never duplicated art), `ui_tokens` (viewer theming, palette pinned).
2. **`pages[]` — reading order, ONE entry per `storyboard_spec.page_order` page, in that exact order.** Each:
   `id` (the stable storyboard page id, e.g. `P00_cover`/`P02_b08`/`P03_end`), `type`
   (`cover|single|grid|grid2x2|feature|finale`), `panel_ids` (the panel ids that page shows), plus optional
   `beat`/`beat_title`/`narration`/`skill`/`title`/`links` (cover/endcard buttons — `https://` only).
3. **`panels{}` — ONE keyed entry per panel id** that appears in any page. Per panel project:
   - **`condition{}`** (what to GENERATE): `content_svg` (the locked blueprint's deterministic SVG = the
     content authority), `expected_literals` (verbatim from the blueprint), `world`, `identity_ref`
     (from the prompt_bundle or `null` → project canonical), `identity_desc`, `characters`, `scene`.
   - **render fields** (for the viewer): `text_mode`, `crop` (`shape∈hero|wide|square`, `position [x,y]` in
     0..1, `zoom ≥ 1`), `safe_zones` (each with an `id`), `bubbles` (`speaker∈researcher|executor|reviewer`,
     `style∈say|shout|terse|thought|whisper`, `text{zh,en}`, optional `anchor` = one of this panel's
     safe-zone ids), `overlays`, `caption{zh,en}`.
   - **leave EMPTY**: `image_path`, `active_attempt_id`, `wiki_node_id`.
4. **Emit `movie.project.json`** — the pure pointer-hub manifest (`project_id`, `title{zh,en}`, `story`,
   `schema_version`, `comic_json`, `art_bible`, `identity_ref`, `dirs`, `text_mode_default`, `palette`). It
   **names** the other artifacts; it NEVER duplicates their content.

### ② RECONCILE page-count integrity (the heart of this skill — INLINE python, NO external script)
**There is no `reconcile_pages.py` — it never existed.** This catch is run *inline* (the schema and the
per-page `run_comic.py` are both blind to it), so paste-and-run this ~15-line whole-comic diff against the
storyboard authority. This is where the orphan-panel class dies. Exit non-zero on ANY blocker; do NOT proceed to
the gate until it is clean.
```python
# inline reconcile — run against the assembled comic.json + the locked storyboard_spec node (NO phantom tool)
import json, sys, re
c  = json.load(open("comic.json", encoding="utf-8"))
sb = json.load(open("wiki/nodes/storyboard_.json", encoding="utf-8"))["payload"]   # the LOCKED authority
page_refs = [pid for p in c["pages"] for pid in p["panel_ids"]]      # every page→panel reference (with dups)
closing_refs = [(p.get("closing") or {}).get("image") for p in c["pages"] if (p.get("closing") or {}).get("image")]
ref_set   = set(page_refs) | set(closing_refs);  defined = set(c["panels"])  # a finale endcard (closing.image) IS a use
orphans   = sorted(defined  - ref_set)            # defined, not on a page AND not a closing endcard → BLOCKER
dangling  = sorted(ref_set  - defined)            # a page/closing points at a missing panel → BLOCKER
# PAGE-ORDER AUTHORITY — ELEMENT-WISE vs the storyboard (NOT a length check): catches reorder / id-swap / dup /
# over-count that a count comparison silently passes.
comic_order = [p["id"] for p in c["pages"]]
sb_order    = list(sb["page_order"])
order_mismatch = comic_order != sb_order               # STRICT: any reorder / missing / extra / dup → BLOCKER
order_missing  = [pid for pid in sb_order if pid not in comic_order]    # storyboard page absent from comic.json
order_extra    = [pid for pid in comic_order if pid not in sb_order]    # comic.json page not in storyboard
page_id_dups   = sorted({pid for pid in comic_order if comic_order.count(pid) > 1})  # duplicate page id (over-count)
bad_grid  = [p["id"] for p in c["pages"] if p["type"] == "grid2x2" and len(p["panel_ids"]) != 4]  # arity → BLOCKER
empty_pg  = [p["id"] for p in c["pages"] if not p["panel_ids"]]    # empty page → BLOCKER
# FAIL-CLOSED engine contracts — jsonschema leaves these OPTIONAL, so this inline step is the REAL enforcer:
def cond(pn): return pn.get("condition") or {}
no_csvg = sorted(pid for pid, pn in c["panels"].items() if not cond(pn).get("content_svg"))   # null/missing content_svg
# cfg_usable EXACTLY (run_comic.py): the EFFECTIVE mode resolves from defaults (a panel inheriting a baked
# default is still baked); a baked panel WITH a content_svg MUST carry expected_literals as a non-empty LIST
# whose every entry is an ascii-tokenizable string. ONE predicate = never weaker than the engine floor.
def emode(pn): return pn.get("text_mode") or (c.get("defaults") or {}).get("text_mode") or "html"
def baked_lits_bad(pn):
    if emode(pn) != "baked" or not cond(pn).get("content_svg"): return False
    exp = cond(pn).get("expected_literals")
    return not (isinstance(exp, list) and len(exp) > 0
                and all(isinstance(e, str) and re.findall(r"[a-z0-9+._-]+", e.lower()) for e in exp))
baked_lits_fail = sorted(pid for pid, pn in c["panels"].items() if baked_lits_bad(pn))
engine_authored = sorted(pid for pid, pn in c["panels"].items()                 # authored ENGINE fields MUST be empty
                         if pn.get("image_path") or pn.get("active_attempt_id") or pn.get("wiki_node_id"))
# bubble.anchor (if set) MUST resolve to a panel safe_zone id. safe_zones is a panel TOP-LEVEL field
# (comic.schema.json) — NOT under condition; jsonschema only TYPES anchor as a string, so assert it inline.
def sz_ids(pn): return {z.get("id") for z in (pn.get("safe_zones") or [])}
bad_anchor = sorted(f"{pid}:{b.get('anchor')}" for pid, pn in c["panels"].items()
                    for b in (pn.get("html_bubbles") or pn.get("bubbles") or [])
                    if isinstance(b, dict) and b.get("anchor") and b["anchor"] not in sz_ids(pn))
# DECLARED recap reuse is the ONLY legitimate multi-reference — read it from the storyboard, never guess.
gp = sb.get("global_policies", {})
sanctioned = set(gp.get("recap_panel_ids") or gp.get("sanctioned_recap") or [])   # storyboard-declared recap/grid set
dups = sorted({pid for pid in page_refs if page_refs.count(pid) > 1} - sanctioned)  # UNDECLARED panel dup → BLOCKER
blockers = {"orphans": orphans, "dangling": dangling, "order_mismatch": order_mismatch, "order_missing": order_missing,
            "order_extra": order_extra, "page_id_dups": page_id_dups, "bad_grid2x2": bad_grid, "empty_pages": empty_pg,
            "undeclared_dups": dups, "content_svg_null": no_csvg, "baked_literals_invalid": baked_lits_fail,
            "engine_fields_authored": engine_authored,
            "bubble_anchor_unresolved": bad_anchor}
if any(blockers.values()):
    sys.exit(f"RECONCILE BLOCKERS: {blockers}  (pages {len(comic_order)} vs storyboard {len(sb_order)})")
print("reconcile OK")
```
- **The orphan check counts `closing.image`.** In the reference `comic.json`, `S22` (the wordless constellation
  finale) is the **endcard of the finale page `P_B12`** (`P_B12.closing.image = "S22"`) — a legitimate use, NOT
  an orphan. (An earlier draft of this check read only `panel_ids` and FALSE-flagged it.) A **TRUE orphan** is a
  panel in `panels{}` referenced by neither a page's `panel_ids` NOR any `closing.image`; that is a real BLOCKER
  the compiler must catch, not ship — page it, or (if genuinely cut) make an upstream *storyboard* edit + re-lock,
  never a quiet drop. When a locked `storyboard_spec` node is present, its `page_order` is the element-wise page
  authority (reorder / missing / extra / duplicate page id all block).
- **Recap reuse is the ONE legitimate multi-reference** and must be *declared*, not inferred. In the
  reference comic, `S12,S13,S14,S15` are each referenced **twice** — once as individual `single` pages
  (`P_B08_1..4`) and once in the `grid2x2` recap/hero page (`P02_b08`). That is by-design (the storyboard's
  `mirror_locks` / recap policy), so a panel referenced >1 time is a **veto ONLY if it is not on the
  storyboard's declared recap/grid set**. The diff reads the storyboard's `global_policies` (the sanctioned
  recap ids) to know which reuses are sanctioned — an *undeclared* duplicate reference is still a BLOCKER.

### ③ VALIDATE against the schema (SHAPE only — it does NOT enforce the fail-closed contracts)
```bash
python3

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [wanshuiyin](https://github.com/wanshuiyin)
- **Source:** [wanshuiyin/ARIS-Movie-Director](https://github.com/wanshuiyin/ARIS-Movie-Director)
- **License:** MIT
- **Homepage:** https://wanshuiyin.github.io/ARIS-Movie-Director/comic/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-wanshuiyin-aris-movie-director-comic-json-compiler
- Seller: https://agentstack.voostack.com/s/wanshuiyin
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
