@ztffn/presentation-generator-plugin 1.1.3 → 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.
- package/agents/presentation-design.md +47 -2
- package/package.json +1 -1
- package/scripts/validate_draft.py +162 -0
|
@@ -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,24 @@ You are a design and JSON generation specialist. Your job is to translate an app
|
|
|
55
83
|
}
|
|
56
84
|
```
|
|
57
85
|
|
|
58
|
-
|
|
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.
|
|
102
|
+
|
|
103
|
+
After a clean validation pass, output a summary listing:
|
|
59
104
|
- Output path (e.g. `presentations/calora-investor-pitch/calora-investor-pitch.json`)
|
|
60
105
|
- Total node count
|
|
61
106
|
- Total edge count
|
package/package.json
CHANGED
|
@@ -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)
|