@ztffn/presentation-generator-plugin 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ description: Translates approved slide outlines into complete graph JSON with no
4
4
  tools:
5
5
  - Read
6
6
  - Write
7
+ - Bash
7
8
  skills:
8
9
  - presentation-generator/system/node-schema
9
10
  - presentation-generator/system/edge-conventions
@@ -28,34 +29,79 @@ You are a design and JSON generation specialist. Your job is to translate an app
28
29
  5. Run the edge validation checklist before finalizing
29
30
  6. Write the complete JSON to `_temp/presentation-draft.json`
30
31
 
32
+ ## Field Mapping
33
+
34
+ When translating from the outline to `data` fields, use **exactly** these SlideNodeData field names — no others:
35
+
36
+ | Outline field | SlideNodeData field | Notes |
37
+ |---|---|---|
38
+ | `### Slide Title` | `label` | Slide heading shown in graph editor and at top of slide |
39
+ | `**Key message:**` + `**Content:**` | `content` | Combine into markdown: key message as first line or `##` heading, then bullets/body. Use `\n\n` between paragraphs. |
40
+ | `**Speaker notes:**` | `notes` | Verbatim from outline. Never put this in `content`. |
41
+
42
+ **DO NOT use these field names** — they are not part of the schema and will be silently ignored by the renderer:
43
+ `headline`, `subheadline`, `bullets`, `speakerNote`, `speakerNotes`, `visualHint`, `theme`, `title`, `body`, `text`, `background`, `showLogo`, `keyMessage`, `claim`, `hook`, `description`, `summary`
44
+
45
+ ## Edge Handle Rules
46
+
47
+ **WRONG** — these handle values will break navigation:
48
+ ```json
49
+ { "sourceHandle": "right", "targetHandle": "left" }
50
+ ```
51
+
52
+ **CORRECT** — source handles prefix `s-`, target handles prefix `t-`:
53
+ ```json
54
+ { "sourceHandle": "s-right", "targetHandle": "t-left" }
55
+ ```
56
+
57
+ All 8 valid handle IDs: `s-right`, `s-left`, `s-top`, `s-bottom`, `t-right`, `t-left`, `t-top`, `t-bottom`
58
+
31
59
  ## Rules
32
60
 
33
61
  - **Translation only**: Do not alter content, order, or structure from the outline. Your role is visual treatment and JSON compilation.
34
62
  - Every node must have `type: "huma"`, `style: { width: 180, height: 70 }`, and `measured: { width: 180, height: 70 }`
35
63
  - Every node ID must be a kebab-case slug
36
64
  - Every forward edge must have a paired return edge
37
- - All handle IDs must be from the valid set of 8
65
+ - All handle IDs must be from the valid set of 8 listed above
38
66
  - For background images, use Unsplash URLs with `?w=1920&q=80`
39
67
  - For video, use placeholder strings: `"PLACEHOLDER: [description]"`
40
68
  - List all slides with video placeholders at the end of output
41
69
 
42
70
  ## Output
43
71
 
44
- Write the JSON to `_temp/presentation-draft.json`:
72
+ 1. Derive the presentation slug from the deck title in `_temp/presentation-outline.md`: lowercase kebab-case, strip special characters. Example: "Calora Investor Pitch" → `calora-investor-pitch`.
73
+ 2. Create the directory `presentations/{slug}/` if it does not exist.
74
+ 3. Write the JSON to `presentations/{slug}/{slug}.json`:
45
75
 
46
76
  ```json
47
77
  {
48
78
  "meta": {
49
- "name": "<deck title from _temp/presentation-outline.md>"
79
+ "name": "<deck title from _temp/presentation-outline.md verbatim>"
50
80
  },
51
81
  "nodes": [ ... ],
52
82
  "edges": [ ... ]
53
83
  }
54
84
  ```
55
85
 
56
- The `meta.name` value must be the presentation title exactly as it appears in the outline verbatim, no reformatting.
86
+ 4. Run `scripts/validate_draft.py` using Bash. This is a Python script you must execute it, not read it. Reading the JSON file is not validation.
87
+
88
+ ```bash
89
+ python3 scripts/validate_draft.py presentations/{slug}/{slug}.json
90
+ ```
91
+
92
+ If the working directory is different, use the absolute path to the script.
93
+
94
+ The script prints either `OK Presentation JSON is valid` (exit 0) or a list of `ERROR` lines (exit 1).
95
+
96
+ If there are any `ERROR` lines:
97
+ - Fix every error directly in the JSON file
98
+ - Run the script again against the same file
99
+ - Repeat until the script prints `OK Presentation JSON is valid`
100
+
101
+ **Your completion message must include the exact terminal output of the script.** A Read tool call on the JSON file is not validation and does not count. If the script output is not shown, the task is not complete.
57
102
 
58
- After writing, output a summary listing:
103
+ After a clean validation pass, output a summary listing:
104
+ - Output path (e.g. `presentations/calora-investor-pitch/calora-investor-pitch.json`)
59
105
  - Total node count
60
106
  - Total edge count
61
107
  - Any slides with video placeholders requiring manual upload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztffn/presentation-generator-plugin",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Claude Code plugin for generating graph-based presentations",
5
5
  "bin": {
6
6
  "presentation-generator-plugin": "bin/index.js"
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_draft.py — Validates a presentation draft JSON before import.
4
+ Checks node schema correctness, edge handle IDs, bidirectional pairs, and duplicates.
5
+ Intended to be run by the design agent after writing the draft, before reporting complete.
6
+ Usage: python3 validate_draft.py <path-to-draft.json>
7
+ Exit: 0 if valid, 1 if errors found.
8
+ """
9
+
10
+ import sys
11
+ import json
12
+ from pathlib import Path
13
+
14
+ # Derived from SlideNodeData interface in src/types/presentation.ts.
15
+ # Any field not in this set is unknown to the renderer and will be silently ignored.
16
+ ALLOWED_DATA_FIELDS = {
17
+ "label", "topic", "content", "notes", "type", "centered", "layout",
18
+ "lightText", "brandFont", "showBranding", "brandingText",
19
+ "backgroundImage", "backgroundImageFit", "backgroundImageOverlay",
20
+ "backgroundVideo", "backgroundVideoFit", "backgroundVideoLoop",
21
+ "inlineVideoControls", "inlineVideoAutoplay", "inlineVideoLoop",
22
+ "scene", "chart", "charts",
23
+ "sceneGroup", "focus",
24
+ }
25
+
26
+ VALID_SOURCE_HANDLES = {"s-right", "s-left", "s-top", "s-bottom"}
27
+ VALID_TARGET_HANDLES = {"t-right", "t-left", "t-top", "t-bottom"}
28
+ BARE_HANDLES = {"right", "left", "top", "bottom"}
29
+
30
+
31
+ def validate(path):
32
+ errors = []
33
+
34
+ try:
35
+ data = json.loads(Path(path).read_text())
36
+ except FileNotFoundError:
37
+ print(f"ERROR File not found: {path}")
38
+ return False
39
+ except json.JSONDecodeError as e:
40
+ print(f"ERROR Invalid JSON: {e}")
41
+ return False
42
+
43
+ # --- meta ---
44
+ meta = data.get("meta")
45
+ if not isinstance(meta, dict):
46
+ errors.append("meta: missing or not an object")
47
+ elif not str(meta.get("name", "")).strip():
48
+ errors.append("meta.name: missing or empty — must be the deck title")
49
+
50
+ # --- nodes ---
51
+ nodes = data.get("nodes")
52
+ node_ids = set()
53
+
54
+ if not isinstance(nodes, list) or len(nodes) == 0:
55
+ errors.append("nodes: missing or empty array")
56
+ else:
57
+ for i, node in enumerate(nodes):
58
+ nid = node.get("id", f"[index {i}]")
59
+ p = f"node '{nid}'"
60
+
61
+ if nid in node_ids:
62
+ errors.append(f"{p}: duplicate node ID")
63
+ node_ids.add(nid)
64
+
65
+ if node.get("type") != "huma":
66
+ errors.append(f"{p}: type must be 'huma', got {node.get('type')!r}")
67
+
68
+ style = node.get("style") or {}
69
+ measured = node.get("measured") or {}
70
+ if style.get("width") != 180 or style.get("height") != 70:
71
+ errors.append(f"{p}: style must be {{width: 180, height: 70}}, got {style}")
72
+ if measured.get("width") != 180 or measured.get("height") != 70:
73
+ errors.append(f"{p}: measured must be {{width: 180, height: 70}}, got {measured}")
74
+
75
+ node_data = node.get("data")
76
+ if not isinstance(node_data, dict):
77
+ errors.append(f"{p}: data must be an object")
78
+ continue
79
+
80
+ if not str(node_data.get("label", "")).strip():
81
+ errors.append(f"{p}: data.label is missing or empty (this is the slide title)")
82
+
83
+ unknown = set(node_data.keys()) - ALLOWED_DATA_FIELDS
84
+ if unknown:
85
+ errors.append(
86
+ f"{p}: unknown field(s) in data: {', '.join(sorted(unknown))} — "
87
+ f"only SlideNodeData fields are valid (label, content, notes, topic, type, layout, ...)"
88
+ )
89
+
90
+ # --- edges ---
91
+ edges = data.get("edges")
92
+ edge_ids = set()
93
+ valid_edges = [] # (source, target, sourceHandle, targetHandle, id)
94
+
95
+ if not isinstance(edges, list):
96
+ errors.append("edges: missing or not an array")
97
+ else:
98
+ for i, edge in enumerate(edges):
99
+ eid = edge.get("id", f"[index {i}]")
100
+ p = f"edge '{eid}'"
101
+
102
+ if eid in edge_ids:
103
+ errors.append(f"{p}: duplicate edge ID")
104
+ edge_ids.add(eid)
105
+
106
+ src = edge.get("source")
107
+ tgt = edge.get("target")
108
+ sh = edge.get("sourceHandle")
109
+ th = edge.get("targetHandle")
110
+
111
+ if node_ids and src not in node_ids:
112
+ errors.append(f"{p}: source '{src}' does not reference a known node ID")
113
+ if node_ids and tgt not in node_ids:
114
+ errors.append(f"{p}: target '{tgt}' does not reference a known node ID")
115
+
116
+ sh_ok = True
117
+ if sh in BARE_HANDLES:
118
+ errors.append(f"{p}: sourceHandle '{sh}' — use 's-{sh}' (source handles require 's-' prefix)")
119
+ sh_ok = False
120
+ elif sh not in VALID_SOURCE_HANDLES:
121
+ errors.append(f"{p}: sourceHandle '{sh}' invalid — must be one of {sorted(VALID_SOURCE_HANDLES)}")
122
+ sh_ok = False
123
+
124
+ th_ok = True
125
+ if th in BARE_HANDLES:
126
+ errors.append(f"{p}: targetHandle '{th}' — use 't-{th}' (target handles require 't-' prefix)")
127
+ th_ok = False
128
+ elif th not in VALID_TARGET_HANDLES:
129
+ errors.append(f"{p}: targetHandle '{th}' invalid — must be one of {sorted(VALID_TARGET_HANDLES)}")
130
+ th_ok = False
131
+
132
+ if sh_ok and th_ok:
133
+ valid_edges.append((src, tgt, sh, th, eid))
134
+
135
+ # --- bidirectional pair check ---
136
+ edge_set = {(src, tgt, sh, th) for src, tgt, sh, th, _ in valid_edges}
137
+ for src, tgt, sh, th, eid in valid_edges:
138
+ ret_sh = "s-" + th[2:] # t-left -> s-left
139
+ ret_th = "t-" + sh[2:] # s-right -> t-right
140
+ if (tgt, src, ret_sh, ret_th) not in edge_set:
141
+ errors.append(
142
+ f"edge '{eid}' ({src} -[{sh}/{th}]-> {tgt}): "
143
+ f"missing return edge ({tgt} -[{ret_sh}/{ret_th}]-> {src})"
144
+ )
145
+
146
+ if not errors:
147
+ print("OK Presentation JSON is valid")
148
+ return True
149
+
150
+ for e in errors:
151
+ print(f"ERROR {e}")
152
+ print()
153
+ print(f"FAILED {len(errors)} error(s) — fix all errors before importing")
154
+ return False
155
+
156
+
157
+ if __name__ == "__main__":
158
+ if len(sys.argv) != 2:
159
+ print("Usage: python3 validate_draft.py <path-to-draft.json>")
160
+ sys.exit(1)
161
+ ok = validate(sys.argv[1])
162
+ sys.exit(0 if ok else 1)
@@ -106,7 +106,7 @@ Call to Action: [headline]
106
106
  Write the tree to `_temp/presentation-plan.md` and present it in the chat.
107
107
 
108
108
  Wait for the user to respond:
109
- - **"approved"** or similar → proceed to Phase 5
109
+ - **"approved"** or similar → derive the slug (lowercase kebab-case of the deck title, e.g. `calora-investor-pitch`) and proceed to Phase 5
110
110
  - **change requests** → update the outline, re-render the tree, wait again
111
111
  - **"start over"** → return to Phase 1
112
112
 
@@ -119,24 +119,28 @@ Invoke the `presentation-design` sub-agent:
119
119
  ```
120
120
  Agent: presentation-design
121
121
  Input: Reads _temp/presentation-outline.md
122
- Output: _temp/presentation-draft.json
122
+ Output: presentations/{slug}/{slug}.json
123
123
  ```
124
124
 
125
125
  The design agent translates each slide into a fully specified graph node, wires all edges with bidirectional pairs, and validates the result.
126
126
 
127
127
  ## Phase 6 — Delivery
128
128
 
129
- After `_temp/presentation-draft.json` is written:
129
+ After `presentations/{slug}/{slug}.json` is written:
130
130
 
131
- 1. Confirm the JSON was written successfully
132
- 2. Provide import instructions:
131
+ 1. Copy the three intermediate files into the presentation folder so everything is self-contained:
132
+ - `_temp/presentation-content-brief.json` `presentations/{slug}/content-brief.json`
133
+ - `_temp/presentation-outline.md` → `presentations/{slug}/outline.md`
134
+ - `_temp/presentation-plan.md` → `presentations/{slug}/plan.md`
135
+
136
+ 2. Confirm the output path and provide import instructions:
133
137
 
134
138
  ```
135
139
  Import your presentation:
136
140
  1. Open the graph editor at /present/plan
137
141
  2. Click "New presentation"
138
142
  3. Choose "Import JSON"
139
- 4. Select _temp/presentation-draft.json
143
+ 4. Select presentations/{slug}/{slug}.json
140
144
  ```
141
145
 
142
146
  3. If any slides have video placeholders, list them:
@@ -187,4 +191,7 @@ All intermediate and output files:
187
191
  | `_temp/presentation-content-brief.json` | Content agent or orchestrator | Narrative agent |
188
192
  | `_temp/presentation-outline.md` | Narrative agent | Design agent, orchestrator |
189
193
  | `_temp/presentation-plan.md` | Orchestrator | User (approval) |
190
- | `_temp/presentation-draft.json` | Design agent | User (import) |
194
+ | `presentations/{slug}/{slug}.json` | Design agent | User (import) |
195
+ | `presentations/{slug}/content-brief.json` | Orchestrator (Phase 6) | Reference |
196
+ | `presentations/{slug}/outline.md` | Orchestrator (Phase 6) | Reference |
197
+ | `presentations/{slug}/plan.md` | Orchestrator (Phase 6) | Reference |