Install
$ agentstack add skill-wanshuiyin-aris-movie-director-comic-json-compiler ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
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.
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 assembledcomic.jsonpage 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
compilegate's legal set, per the gate skill):approve/revise. Onapproveflip
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_idare 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_filenamehelper (:→_),
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") — itspage_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_ledgerit points at — the per-panel ground-truth the gate uses to confirmconditionprose
agrees with each panel's continuity row.
- One
panel_specper panel (status: "locked") —page_type,world,text_mode,expected_literals,
content_blueprint, bubbles, side_narration, motifs, (cover/finale: safe_zones + endcard fields).
- One
blueprintper panel (status: "locked") —content_svg,expected_literals(verbatim),
safe_zones, html_bubbles, crop, negative_space_policy, generator_script, file_sha256.
- (optional) the
prompt_bundleper panel — foridentity_ref_paths(else fall back to the project
canonical from identity_refs).
- HALT if any of these is not
locked(or itscontent_svg/output_reffile 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.
- 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).
pages[]— reading order, ONE entry perstoryboard_spec.page_orderpage, 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).
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.
- 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.
# 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 referencecomic.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)
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.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.