@ztffn/presentation-generator-plugin 1.1.3 → 1.1.5

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,13 +29,40 @@ 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
@@ -55,7 +83,25 @@ You are a design and JSON generation specialist. Your job is to translate an app
55
83
  }
56
84
  ```
57
85
 
58
- After writing, output a summary listing:
86
+ 4. Run the validator script using Bash. This is a Python script — you must execute it, not read it. Reading the JSON file is not validation.
87
+
88
+ The script is bundled with the plugin. Find it and run it:
89
+
90
+ ```bash
91
+ VALIDATE=$(find -L .claude ~/.claude -path "*/presentation-generator/scripts/validate_draft.py" 2>/dev/null | head -1)
92
+ python3 "$VALIDATE" presentations/{slug}/{slug}.json
93
+ ```
94
+
95
+ The script prints either `OK Presentation JSON is valid` (exit 0) or a list of `ERROR` lines (exit 1).
96
+
97
+ If there are any `ERROR` lines:
98
+ - Fix every error directly in the JSON file
99
+ - Run the script again against the same file
100
+ - Repeat until the script prints `OK Presentation JSON is valid`
101
+
102
+ **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.
103
+
104
+ After a clean validation pass, output a summary listing:
59
105
  - Output path (e.g. `presentations/calora-investor-pitch/calora-investor-pitch.json`)
60
106
  - Total node count
61
107
  - Total edge count
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztffn/presentation-generator-plugin",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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)