ptxiagram 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -1,40 +1,69 @@
1
1
  # ptxiagram
2
2
 
3
- **Generate architecture and workflow diagrams as native, editable PowerPoint shapes — not an embedded image.**
3
+ **Generate architecture, workflow, and sequence diagrams as native, editable PowerPoint shapes — not an embedded image.**
4
+
5
+ > Built with [Claude Code](https://claude.com/claude-code) (Claude Sonnet 5) — from the initial prototype through the pptxgenjs/Hancom Office compatibility investigation, the JSON schema design, the layout validator, and this package's release automation. See the [commit history](https://github.com/hiio420official/ptxiagram/commits/main) for the build process.
4
6
 
5
7
  ptxiagram is an agent skill for Claude (and a standalone CLI/library) that turns a plain-English description of a system or process into a `.pptx` slide built from real PowerPoint shapes: rectangles, cylinders, clouds, connectors, text boxes. Anyone can open the result in PowerPoint — or Hancom 한쇼 — and click a box to change its color, text, or position, because it *is* a box, not a picture of one.
6
8
 
7
9
  - **Native shapes, not a picture** — every box and arrow is a real PPTX shape, editable after generation
8
- - **Two diagram types today** — `workflow` (swimlanes, happy-path flows) and `architecture` (free-placed components, trust boundaries, hub-and-spoke)
10
+ - **Five diagram types today** — `workflow` (swimlanes, happy-path flows), `architecture` (free-placed components, trust boundaries, hub-and-spoke), `sequence` (participants + lifelines, API call chains), `dataflow` (stage-to-stage pipelines with PII/lineage annotations), and `lifecycle` (state machines with terminal outcomes)
9
11
  - **A validator that catches real layout bugs before rendering** — node overlap, labels wider than their shape, and connectors that cut through an unrelated node all fail loudly with a specific fix, instead of shipping a broken-looking slide
10
12
  - **Ships a Hancom Office compatibility fix** — every generated file is post-processed to strip a documented PptxGenJS defect that PowerPoint silently repairs but Hancom's 한쇼 flags as a corrupted document (see below)
11
13
  - **Route presets instead of hand-computed coordinates** — `straight`, `drop`, `elbow-right`, `arc-over-top` cover swimlane transitions, hub-and-spoke fan-out, and routing around a boundary box
12
14
 
13
- ## Install
15
+ ## Use it in an agent (Claude Code, Codex, Cursor, ...)
14
16
 
15
- As a Claude Skill:
17
+ Install with the [`skills` CLI](https://github.com/vercel-labs/skills) — it detects every supported agent on your machine and wires the skill into each one:
16
18
 
17
19
  ```bash
18
20
  npx skills add hiio420official/ptxiagram -g
19
21
  ```
20
22
 
21
- As a CLI / library:
23
+ Then just ask in chat:
22
24
 
23
- ```bash
24
- npm install -g ptxiagram
25
- ptxiagram doctor
25
+ ```text
26
+ Use ptxiagram to draw an architecture diagram for this repo's runtime,
27
+ as a PPTX I can drop into a slide deck.
28
+ ```
29
+
30
+ ```text
31
+ Use ptxiagram to make a workflow diagram of our incident response process,
32
+ with a lane per team, so I can paste it into the postmortem deck.
33
+ ```
34
+
35
+ ```text
36
+ Use ptxiagram to draw a sequence diagram of this API's cache-fallback path,
37
+ from request to response, for the engineering review deck.
26
38
  ```
27
39
 
28
- ## Quick start
40
+ The agent reads `SKILL.md`, writes a small JSON file describing your nodes and
41
+ edges, and runs the CLI to produce a `.pptx` with real, editable shapes.
42
+
43
+ **Per-agent notes:**
44
+ - **Claude Code**: works out of the box after `npx skills add`.
45
+ - **Codex CLI / opencode**: install with an explicit `--agent` flag if you don't want it installed everywhere, e.g. `npx skills use hiio420official/ptxiagram@ptxiagram --agent codex`.
46
+ - **Any other `skills`-supported agent** (Cursor, Windsurf, Continue, Antigravity, Gemini CLI, ...): the same `npx skills add` command installs to whichever of these are detected on your machine.
47
+
48
+ ## Use it standalone (CLI / library)
29
49
 
30
50
  ```bash
51
+ npm install -g ptxiagram
52
+ ptxiagram doctor
31
53
  ptxiagram render workflow examples/onboarding.workflow.json out.pptx
32
54
  ptxiagram check out.pptx
33
55
  ```
34
56
 
57
+ Or as a library:
58
+
59
+ ```js
60
+ import { renderWorkflow } from "ptxiagram";
61
+ await renderWorkflow("my-diagram.workflow.json", "out.pptx");
62
+ ```
63
+
35
64
  `render` validates the input (schema + layout) before writing anything, and always repairs the output for Hancom Office compatibility. `check` confirms the repair took, dumps every embedded text run for a spelling/content spot-check, and — if LibreOffice is installed — writes a PNG preview for a quick visual check.
36
65
 
37
- See [`ptxiagram/SKILL.md`](ptxiagram/SKILL.md) for the full JSON shape of both diagram types, the node-type → shape/color mapping, and the route presets.
66
+ See [`SKILL.md`](SKILL.md) for the full JSON shape of all five diagram types, the node-type → shape/color mapping, and the route presets.
38
67
 
39
68
  ## Why every file gets repaired
40
69
 
package/SKILL.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: ptxiagram
3
- description: Create architecture and workflow diagrams as native, editable PowerPoint shapes (real rectangles, connectors, and text boxes a user can click and restyle in PowerPoint or Hancom 한쇼) instead of an embedded image. Accepts a plain-language description, turns it into a small JSON file, and renders it with pptxgenjs. Use when the user asks for a diagram, architecture drawing, or process flow that needs to end up inside a .pptx deck or a Korean-office-suite-compatible presentation - not when they just want a standalone image or an HTML/web diagram (use an SVG-based diagram tool for that instead).
3
+ description: Create architecture, workflow, sequence, dataflow, and lifecycle diagrams as native, editable PowerPoint shapes (real rectangles, connectors, and text boxes a user can click and restyle in PowerPoint or Hancom 한쇼) instead of an embedded image. Accepts a plain-language description, turns it into a small JSON file, and renders it with pptxgenjs. Use when the user asks for a diagram, architecture drawing, process flow, API call sequence, data pipeline, or state machine that needs to end up inside a .pptx deck or a Korean-office-suite-compatible presentation - not when they just want a standalone image or an HTML/web diagram (use an SVG-based diagram tool for that instead).
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.1.0"
6
+ version: "0.3.0"
7
7
  status: prototype
8
8
  based_on: "gitbrent/PptxGenJS (MIT)"
9
9
  ---
@@ -51,6 +51,9 @@ validation and the embedded-text dump, just no rendered preview image).
51
51
  |------|---------|--------|
52
52
  | `workflow` | Cross-team processes, approval chains, anything with swimlanes and a happy path | `schemas/workflow.schema.json` |
53
53
  | `architecture` | System components, services, datastores, trust boundaries, hub-and-spoke integrations | `schemas/architecture.schema.json` |
54
+ | `sequence` | API call chains, request/response traces, cache-fallback flows — up to 6 participants, up to 9 messages | `schemas/sequence.schema.json` |
55
+ | `dataflow` | Data pipelines, ETL/ELT, PII/lineage annotations, stage-to-stage flows | `schemas/dataflow.schema.json` |
56
+ | `lifecycle` | State machines, status transitions, terminal outcomes (same JSON shape as `workflow`) | `schemas/lifecycle.schema.json` |
54
57
 
55
58
  ## The loop
56
59
 
@@ -58,6 +61,9 @@ validation and the embedded-text dump, just no rendered preview image).
58
61
  shape, don't guess the field names:
59
62
  - `examples/onboarding.workflow.json`
60
63
  - `examples/order-processing.architecture.json`
64
+ - `examples/cache-miss.sequence.json`
65
+ - `examples/product-analytics.dataflow.json`
66
+ - `examples/agent-run.lifecycle.json`
61
67
  2. Write `<name>.<type>.json`.
62
68
  3. Render:
63
69
  ```bash
@@ -127,12 +133,83 @@ order. Default node size is 1.8×0.95in.
127
133
  `boundaries[].wraps` takes component ids, not coordinates — the renderer
128
134
  computes a padded frame around them, so never hand-compute a boundary box.
129
135
 
136
+ ## Sequence JSON shape
137
+
138
+ ```json
139
+ {
140
+ "schema_version": 1,
141
+ "diagram_type": "sequence",
142
+ "meta": { "title": "...", "output": "out.pptx" },
143
+ "participants": [
144
+ { "id": "api", "type": "backend", "label": "API", "sublabel": "handler" }
145
+ ],
146
+ "messages": [
147
+ { "from": "client", "to": "api", "label": "GET /orders/123", "variant": "emphasis" }
148
+ ],
149
+ "cards": []
150
+ }
151
+ ```
152
+
153
+ **Layout budget**: up to 6 participants in a row (auto-spaced, 2.2in apart),
154
+ each with a vertical dashed lifeline. Messages are drawn in JSON array order
155
+ at auto-incrementing y positions (0.55in apart) — order matters, there's no
156
+ explicit `y` field. Up to 9 messages without `cards`, ~6 with (cards need
157
+ room at the bottom, so the lifeline gets shorter). Self-messages (same
158
+ `from`/`to`) aren't supported yet.
159
+
160
+ ## Dataflow JSON shape
161
+
162
+ ```json
163
+ {
164
+ "schema_version": 1,
165
+ "diagram_type": "dataflow",
166
+ "meta": { "title": "...", "output": "out.pptx" },
167
+ "stages": [{ "label": "Sources" }, { "label": "Ingest" }],
168
+ "nodes": [
169
+ { "id": "web", "type": "frontend", "label": "웹 앱", "stage": 0, "row": 0 }
170
+ ],
171
+ "flows": [
172
+ { "from": "web", "to": "queue", "label": "이벤트 전송", "classification": "PII touch", "variant": "security" }
173
+ ],
174
+ "cards": []
175
+ }
176
+ ```
177
+
178
+ **Layout budget**: up to 5 stages (columns, 2.6in apart) x 5 rows (fixed y
179
+ positions). `flow.label` is required; `classification` is optional
180
+ free-text rendered in parentheses after the label (e.g. "PII touch",
181
+ "non-PII", "approved only") for sensitivity/lineage annotations. Route is
182
+ picked automatically — `straight` when both ends share a row, `elbow-right`
183
+ otherwise — so flows don't take a `route` field.
184
+
185
+ ## Lifecycle JSON shape
186
+
187
+ Structurally identical to the workflow schema (same `lanes`/`nodes`/`edges`
188
+ shape, same layout budget) — only `diagram_type` differs. Convention: a
189
+ `main` lane for the happy-path states and a `terminal` lane for end states.
190
+
191
+ ```json
192
+ {
193
+ "schema_version": 1,
194
+ "diagram_type": "lifecycle",
195
+ "meta": { "title": "...", "output": "out.pptx" },
196
+ "lanes": [{ "id": "main", "label": "실행 단계" }, { "id": "terminal", "label": "종료 결과" }],
197
+ "nodes": [
198
+ { "id": "queued", "lane": "main", "col": 0, "type": "neutral", "label": "대기 중" },
199
+ { "id": "completed", "lane": "terminal", "col": 3, "type": "success", "label": "완료" }
200
+ ],
201
+ "edges": [{ "from": "queued", "to": "completed", "variant": "emphasis", "route": "drop" }]
202
+ }
203
+ ```
204
+
130
205
  ## Node types
131
206
 
132
207
  `client` `frontend` `neutral` `backend` `database` `cache` `queue`
133
- `external` `security` `decision` each maps to a shape preset (ellipse,
134
- rect, roundRect, can/cylinder, hexagon, cube, cloud, diamond) and a color
135
- pair. Pick by role, not by desired color.
208
+ `external` `security` `decision` `active` `waiting` `success` `failure`
209
+ each maps to a shape preset (ellipse, rect, roundRect, can/cylinder,
210
+ hexagon, cube, cloud, diamond) and a color pair. The last four
211
+ (`active`/`waiting`/`success`/`failure`) exist for `lifecycle` states;
212
+ pick by role, not by desired color.
136
213
 
137
214
  ## Route presets
138
215
 
@@ -169,7 +246,11 @@ this automatically; `check` verifies it actually happened. Source:
169
246
 
170
247
  ## What's not built yet
171
248
 
172
- - `sequence`, `dataflow`, `lifecycle` diagram types (only `workflow` and
173
- `architecture` exist so far)
174
249
  - No CJK exact-metrics font measurement — the label-width check is a
175
250
  conservative estimate, not real glyph widths
251
+ - Sequence self-messages (a participant messaging itself)
252
+ - No label-vs-label collision detection — two edges converging on the same
253
+ node from different rows can still end up with close (though no longer
254
+ overlapping, after the `elbow-right` label-anchor fix) labels in a busy
255
+ diagram; the validator catches label-vs-*shape* and edge-vs-*node*
256
+ collisions, not label-vs-label
package/bin/cli.mjs CHANGED
@@ -7,11 +7,20 @@ import { promisify } from "node:util";
7
7
  import JSZip from "jszip";
8
8
  import { renderWorkflow } from "../renderers/workflow/render-workflow.mjs";
9
9
  import { renderArchitecture } from "../renderers/architecture/render-architecture.mjs";
10
+ import { renderSequence } from "../renderers/sequence/render-sequence.mjs";
11
+ import { renderDataflow } from "../renderers/dataflow/render-dataflow.mjs";
12
+ import { renderLifecycle } from "../renderers/lifecycle/render-lifecycle.mjs";
10
13
  import { validateAll } from "../lib/validate.mjs";
11
14
 
12
15
  const execFileAsync = promisify(execFile);
13
16
 
14
- const RENDERERS = { workflow: renderWorkflow, architecture: renderArchitecture };
17
+ const RENDERERS = {
18
+ workflow: renderWorkflow,
19
+ architecture: renderArchitecture,
20
+ sequence: renderSequence,
21
+ dataflow: renderDataflow,
22
+ lifecycle: renderLifecycle,
23
+ };
15
24
 
16
25
  function usage() {
17
26
  console.log(`Usage:
@@ -20,7 +29,7 @@ function usage() {
20
29
  cli check <output.pptx>
21
30
  cli doctor
22
31
 
23
- Types: workflow, architecture`);
32
+ Types: workflow, architecture, sequence, dataflow, lifecycle`);
24
33
  }
25
34
 
26
35
  async function cmdRender(type, inputPath, outputPath) {
@@ -0,0 +1,26 @@
1
+ {
2
+ "schema_version": 1,
3
+ "diagram_type": "lifecycle",
4
+ "meta": {
5
+ "title": "에이전트 실행 라이프사이클",
6
+ "subtitle": "상태와 종료 결과",
7
+ "output": "test/output/agent-run.schema-driven.pptx"
8
+ },
9
+ "lanes": [
10
+ { "id": "main", "label": "실행 단계" },
11
+ { "id": "terminal", "label": "종료 결과" }
12
+ ],
13
+ "nodes": [
14
+ { "id": "queued", "lane": "main", "col": 0, "type": "neutral", "label": "대기 중", "sublabel": "큐에 등록됨" },
15
+ { "id": "running", "lane": "main", "col": 1, "type": "active", "label": "실행 중", "sublabel": "작업 처리" },
16
+ { "id": "reviewing", "lane": "main", "col": 3, "type": "waiting", "label": "승인 대기", "sublabel": "휴먼 리뷰" },
17
+ { "id": "completed", "lane": "terminal", "col": 3, "type": "success", "label": "완료" },
18
+ { "id": "failed", "lane": "terminal", "col": 5, "type": "failure", "label": "실패", "sublabel": "재시도 필요" }
19
+ ],
20
+ "edges": [
21
+ { "from": "queued", "to": "running", "variant": "default", "route": "straight" },
22
+ { "from": "running", "to": "reviewing", "variant": "default", "route": "straight" },
23
+ { "from": "reviewing", "to": "completed", "variant": "emphasis", "route": "drop", "label": "승인" },
24
+ { "from": "reviewing", "to": "failed", "variant": "security", "route": "drop", "label": "반려" }
25
+ ]
26
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "schema_version": 1,
3
+ "diagram_type": "sequence",
4
+ "meta": {
5
+ "title": "캐시 미스 요청 흐름",
6
+ "subtitle": "Client → API → Cache → DB",
7
+ "output": "test/output/cache-miss.schema-driven.pptx"
8
+ },
9
+ "participants": [
10
+ { "id": "client", "type": "client", "label": "Client", "sublabel": "브라우저" },
11
+ { "id": "api", "type": "backend", "label": "API", "sublabel": "handler" },
12
+ { "id": "cache", "type": "cache", "label": "Cache", "sublabel": "Redis" },
13
+ { "id": "db", "type": "database", "label": "DB", "sublabel": "PostgreSQL" }
14
+ ],
15
+ "messages": [
16
+ { "from": "client", "to": "api", "label": "GET /orders/123", "variant": "emphasis" },
17
+ { "from": "api", "to": "cache", "label": "조회", "variant": "default" },
18
+ { "from": "cache", "to": "api", "label": "캐시 미스", "variant": "dashed" },
19
+ { "from": "api", "to": "db", "label": "쿼리 실행", "variant": "emphasis" },
20
+ { "from": "db", "to": "api", "label": "결과 반환", "variant": "dashed" },
21
+ { "from": "api", "to": "client", "label": "200 OK", "variant": "emphasis" }
22
+ ]
23
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "schema_version": 1,
3
+ "diagram_type": "dataflow",
4
+ "meta": {
5
+ "title": "제품 분석 데이터 파이프라인",
6
+ "subtitle": "이벤트 수집 → 적재 → 소비",
7
+ "output": "test/output/product-analytics.schema-driven.pptx"
8
+ },
9
+ "stages": [
10
+ { "label": "Sources" },
11
+ { "label": "Ingest" },
12
+ { "label": "Store" },
13
+ { "label": "Consumers" }
14
+ ],
15
+ "nodes": [
16
+ { "id": "web", "type": "frontend", "label": "웹 앱", "sublabel": "clickstream", "stage": 0, "row": 0 },
17
+ { "id": "mobile", "type": "frontend", "label": "모바일 앱", "sublabel": "clickstream", "stage": 0, "row": 1 },
18
+ { "id": "queue", "type": "queue", "label": "이벤트 큐", "sublabel": "Kafka", "stage": 1, "row": 0 },
19
+ { "id": "warehouse", "type": "database", "label": "데이터 웨어하우스", "sublabel": "BigQuery", "stage": 2, "row": 0 },
20
+ { "id": "dashboard", "type": "external", "label": "대시보드", "sublabel": "내부 분석팀", "stage": 3, "row": 0 },
21
+ { "id": "ml", "type": "external", "label": "추천 모델", "sublabel": "배치 학습", "stage": 3, "row": 1 }
22
+ ],
23
+ "flows": [
24
+ { "from": "web", "to": "queue", "label": "이벤트 전송", "classification": "PII touch", "variant": "security" },
25
+ { "from": "mobile", "to": "queue", "label": "이벤트 전송", "classification": "PII touch", "variant": "security" },
26
+ { "from": "queue", "to": "warehouse", "label": "적재", "classification": "approved only", "variant": "default" },
27
+ { "from": "warehouse", "to": "dashboard", "label": "집계 쿼리", "classification": "non-PII", "variant": "default" },
28
+ { "from": "warehouse", "to": "ml", "label": "피처 추출", "classification": "non-PII", "variant": "dashed" }
29
+ ]
30
+ }
package/index.mjs CHANGED
@@ -1,4 +1,14 @@
1
1
  export { renderWorkflow } from "./renderers/workflow/render-workflow.mjs";
2
2
  export { renderArchitecture } from "./renderers/architecture/render-architecture.mjs";
3
- export { validateAll, validateSchema, validateWorkflowLayout, validateArchitectureLayout } from "./lib/validate.mjs";
3
+ export { renderSequence } from "./renderers/sequence/render-sequence.mjs";
4
+ export { renderDataflow } from "./renderers/dataflow/render-dataflow.mjs";
5
+ export { renderLifecycle } from "./renderers/lifecycle/render-lifecycle.mjs";
6
+ export {
7
+ validateAll,
8
+ validateSchema,
9
+ validateWorkflowLayout,
10
+ validateArchitectureLayout,
11
+ validateSequenceLayout,
12
+ validateDataflowLayout,
13
+ } from "./lib/validate.mjs";
4
14
  export { repairPptx } from "./lib/repair-pptx.mjs";
package/lib/layout.mjs CHANGED
@@ -47,6 +47,56 @@ export function computeWorkflowLayout(raw) {
47
47
  return { laneBoxes, nodeBoxes, nodeMeta, cardsY };
48
48
  }
49
49
 
50
+ export const SEQUENCE_LAYOUT = {
51
+ X0: 0.7, GAP: 2.2, BOX_W: 1.5, BOX_H: 0.7,
52
+ TOP_Y: 1.1, LIFELINE_BOTTOM: 7.0,
53
+ MSG_START_Y: 2.3, MSG_STEP: 0.55,
54
+ };
55
+
56
+ export function computeSequenceLayout(raw) {
57
+ const L = SEQUENCE_LAYOUT;
58
+ const participantBoxes = {};
59
+ const centerX = {};
60
+ raw.participants.forEach((p, i) => {
61
+ const x = L.X0 + i * L.GAP;
62
+ participantBoxes[p.id] = { x, y: L.TOP_Y, w: L.BOX_W, h: L.BOX_H };
63
+ centerX[p.id] = x + L.BOX_W / 2;
64
+ });
65
+ const messageYs = raw.messages.map((_, i) => L.MSG_START_Y + i * L.MSG_STEP);
66
+ // cards need ~1.75in of vertical room, so diagrams that use them get a
67
+ // shorter lifeline (fewer messages fit) instead of overflowing the slide
68
+ const lifelineBottom = raw.cards && raw.cards.length ? 5.3 : L.LIFELINE_BOTTOM;
69
+ const cardsY = lifelineBottom + 0.15;
70
+ return { participantBoxes, centerX, messageYs, lifelineBottom, cardsY };
71
+ }
72
+
73
+ export const DATAFLOW_LAYOUT = {
74
+ STAGE_X0: 1.7, STAGE_GAP: 2.6,
75
+ ROW_Y: [1.3, 2.5, 3.7, 4.9, 6.1],
76
+ NODE_W: 1.6, NODE_H: 0.85,
77
+ STAGE_LABEL_Y: 0.95,
78
+ };
79
+
80
+ // Shared by the dataflow renderer and validator so route selection can
81
+ // never drift between what's drawn and what's checked.
82
+ export function sameRow(a, b) {
83
+ return Math.abs(a.y - b.y) < 0.05;
84
+ }
85
+
86
+ export function computeDataflowLayout(raw) {
87
+ const L = DATAFLOW_LAYOUT;
88
+ const stageCenterX = raw.stages.map((_, i) => L.STAGE_X0 + i * L.STAGE_GAP);
89
+ const nodeBoxes = {};
90
+ for (const n of raw.nodes) {
91
+ const cx = stageCenterX[n.stage];
92
+ if (cx === undefined) throw new Error(`Node "${n.id}" references unknown stage index ${n.stage}`);
93
+ const y = L.ROW_Y[n.row];
94
+ if (y === undefined) throw new Error(`Node "${n.id}" references unknown row index ${n.row}`);
95
+ nodeBoxes[n.id] = { x: cx - L.NODE_W / 2, y, w: L.NODE_W, h: L.NODE_H };
96
+ }
97
+ return { nodeBoxes, stageCenterX };
98
+ }
99
+
50
100
  const BOUNDARY_PAD = 0.3;
51
101
 
52
102
  export function computeArchitectureLayout(raw) {
package/lib/routes.mjs CHANGED
@@ -65,7 +65,10 @@ export function computeSegments(from, to, opts = {}) {
65
65
  { ...boxFromPoints({ x: midX, y: Math.min(a.y, b.y) }, { x: midX, y: Math.max(a.y, b.y) }), arrow: false },
66
66
  { ...boxFromPoints({ x: midX, y: b.y }, b), arrow: true },
67
67
  ],
68
- labelAnchor: label ? { x: midX - 0.6, y: b.y, w: 1.2 } : null,
68
+ // anchored at the jog's own midpoint (like "drop" does), not at
69
+ // either endpoint's row -- otherwise this collides with a sibling
70
+ // "straight" edge whose label sits right at the target's row
71
+ labelAnchor: label ? { x: midX - 0.6, y: (a.y + b.y) / 2, w: 1.2 } : null,
69
72
  };
70
73
  }
71
74
 
@@ -110,3 +113,19 @@ export function connector(slide, from, to, opts = {}) {
110
113
  });
111
114
  }
112
115
  }
116
+
117
+ /** Draws a single point-to-point arrow at an explicit y (sequence
118
+ * messages, self-loops) instead of anchoring to node box sides. Direction
119
+ * matters: the arrowhead lands at (x2,y). opts: { color, dashed, label } */
120
+ export function drawArrow(slide, { x1, x2, y, color = "94A3B8", dashed = false, label }) {
121
+ slide.addShape("line", {
122
+ x: Math.min(x1, x2), y, w: Math.abs(x2 - x1), h: 0,
123
+ line: { color, width: 2, dashType: dashed ? "dash" : "solid", endArrowType: "triangle" },
124
+ });
125
+ if (label) {
126
+ slide.addText(label, {
127
+ x: Math.min(x1, x2), y: y - 0.26, w: Math.abs(x2 - x1), h: 0.24,
128
+ align: "center", fontFace: FONT, fontSize: 8.5, color: "475569",
129
+ });
130
+ }
131
+ }
package/lib/shapes.mjs CHANGED
@@ -12,6 +12,11 @@ export const NODE_TYPES = {
12
12
  external: { shape: "cloud", fill: "FEF3C7", stroke: "F59E0B" },
13
13
  security: { shape: "roundRect", fill: "FFE4E6", stroke: "F43F5E" },
14
14
  decision: { shape: "diamond", fill: "FFE4E6", stroke: "F43F5E" },
15
+ // lifecycle-diagram states
16
+ active: { shape: "roundRect", fill: "DBEAFE", stroke: "3B82F6" },
17
+ waiting: { shape: "roundRect", fill: "FEF3C7", stroke: "F59E0B" },
18
+ success: { shape: "roundRect", fill: "D1FAE5", stroke: "10B981" },
19
+ failure: { shape: "roundRect", fill: "FFE4E6", stroke: "F43F5E" },
15
20
  };
16
21
 
17
22
  export const FONT = "맑은 고딕";
@@ -92,6 +97,15 @@ export function drawCards(slide, cards, y) {
92
97
  });
93
98
  }
94
99
 
100
+ // Draws a participant's vertical lifeline (a sequence-diagram convention:
101
+ // a thin dashed reference line a participant's messages attach to).
102
+ export function drawLifeline(slide, { x, fromY, toY }) {
103
+ slide.addShape("line", {
104
+ x, y: fromY, w: 0, h: toY - fromY,
105
+ line: { color: "CBD5E1", width: 1, dashType: "dash" },
106
+ });
107
+ }
108
+
95
109
  // Draws a swimlane frame with a title strip. Returns the lane's box.
96
110
  export function drawLane(slide, { x, y, w, h, label }) {
97
111
  slide.addShape("rect", {
package/lib/validate.mjs CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import Ajv2020 from "ajv/dist/2020.js";
5
- import { computeWorkflowLayout, computeArchitectureLayout, SLIDE_W, SLIDE_H } from "./layout.mjs";
5
+ import { computeWorkflowLayout, computeArchitectureLayout, computeSequenceLayout, computeDataflowLayout, sameRow, SLIDE_W, SLIDE_H } from "./layout.mjs";
6
6
  import { computeSegments } from "./routes.mjs";
7
7
 
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -64,7 +64,7 @@ function offSlide(box) {
64
64
  return box.x < 0 || box.y < 0 || box.x + box.w > SLIDE_W || box.y + box.h > SLIDE_H;
65
65
  }
66
66
 
67
- function checkCommon({ problems, ids, boxes, labelOf, fontSizePt, edgesOrConns, resolveEndpoints, edgeLabel }) {
67
+ function checkCommon({ problems, ids, boxes, labelOf, fontSizePt, edgesOrConns, resolveEndpoints, edgeLabel, resolveRoute = (e) => e.route || "straight" }) {
68
68
  for (const id of ids) {
69
69
  const box = boxes[id];
70
70
  const label = labelOf(id);
@@ -89,7 +89,7 @@ function checkCommon({ problems, ids, boxes, labelOf, fontSizePt, edgesOrConns,
89
89
  const { from, to } = resolveEndpoints(e);
90
90
  if (!from || !to) continue; // unknown-id errors surface from the renderer/schema instead
91
91
  const { segments } = computeSegments(from, to, {
92
- route: e.route || "straight", bias: e.bias, clearY: e.clearY, label: e.label,
92
+ route: resolveRoute(e, from, to), bias: e.bias, clearY: e.clearY, label: e.label,
93
93
  });
94
94
  for (const id of ids) {
95
95
  if (id === e.from || id === e.to) continue;
@@ -133,11 +133,68 @@ export function validateArchitectureLayout(raw) {
133
133
  });
134
134
  }
135
135
 
136
+ export function validateDataflowLayout(raw) {
137
+ const { nodeBoxes } = computeDataflowLayout(raw);
138
+ const labelOf = (id) => raw.nodes.find((n) => n.id === id).label;
139
+ return checkCommon({
140
+ problems: [],
141
+ ids: Object.keys(nodeBoxes),
142
+ boxes: nodeBoxes,
143
+ labelOf,
144
+ fontSizePt: 12.5,
145
+ edgesOrConns: raw.flows,
146
+ resolveEndpoints: (f) => ({ from: nodeBoxes[f.from], to: nodeBoxes[f.to] }),
147
+ edgeLabel: (f) => `Flow "${f.from}"->"${f.to}"`,
148
+ resolveRoute: (f, from, to) => (from && to && sameRow(from, to) ? "straight" : "elbow-right"),
149
+ });
150
+ }
151
+
152
+ export function validateSequenceLayout(raw) {
153
+ const problems = [];
154
+ const { participantBoxes, messageYs, lifelineBottom } = computeSequenceLayout(raw);
155
+ const ids = Object.keys(participantBoxes);
156
+
157
+ for (const p of raw.participants) {
158
+ const box = participantBoxes[p.id];
159
+ const w = estimateTextWidth(p.label, 12.5);
160
+ if (w > box.w - 0.1) {
161
+ problems.push(`Participant "${p.id}" label "${p.label}" (~${w.toFixed(2)}in) is wider than its shape (${box.w.toFixed(2)}in) — shorten the label or move detail to sublabel.`);
162
+ }
163
+ if (offSlide(box)) {
164
+ problems.push(`Participant "${p.id}" is off-slide (slide is ${SLIDE_W}x${SLIDE_H}in).`);
165
+ }
166
+ }
167
+
168
+ for (let i = 0; i < ids.length; i++) {
169
+ for (let j = i + 1; j < ids.length; j++) {
170
+ if (rectsOverlap(participantBoxes[ids[i]], participantBoxes[ids[j]])) {
171
+ problems.push(`Participants "${ids[i]}" and "${ids[j]}" overlap.`);
172
+ }
173
+ }
174
+ }
175
+
176
+ raw.messages.forEach((m, i) => {
177
+ if (m.from === m.to) {
178
+ problems.push(`Message ${i} ("${m.label}") has the same participant "${m.from}" as from and to — self-messages aren't supported yet.`);
179
+ }
180
+ if (!participantBoxes[m.from]) problems.push(`Message ${i} references unknown participant "${m.from}".`);
181
+ if (!participantBoxes[m.to]) problems.push(`Message ${i} references unknown participant "${m.to}".`);
182
+ if (messageYs[i] > lifelineBottom - 0.3) {
183
+ problems.push(`Message ${i} ("${m.label}") falls below the lifeline (too many messages for the available space) — split into fewer messages or drop the cards to free up room.`);
184
+ }
185
+ });
186
+
187
+ return problems;
188
+ }
189
+
136
190
  /** Runs schema validation, then layout collision checks. Returns the list
137
191
  * of layout problems (empty = clean); throws if schema validation fails. */
138
192
  export async function validateAll(type, raw) {
139
193
  await validateSchema(type, raw);
140
194
  if (type === "workflow") return validateWorkflowLayout(raw);
195
+ if (type === "lifecycle") return validateWorkflowLayout(raw); // same JSON shape as workflow
141
196
  if (type === "architecture") return validateArchitectureLayout(raw);
197
+ if (type === "sequence") return validateSequenceLayout(raw);
198
+ if (type === "dataflow") return validateDataflowLayout(raw);
142
199
  throw new Error(`Unknown diagram type: ${type}`);
143
200
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ptxiagram",
3
- "version": "0.1.0",
4
- "description": "Generate native, editable PowerPoint (PPTX) architecture and workflow diagrams from a small JSON file — real shapes and connectors, not an embedded image. Ships with a Claude Skill (SKILL.md) and a CLI.",
3
+ "version": "0.3.0",
4
+ "description": "Generate native, editable PowerPoint (PPTX) architecture, workflow, sequence, dataflow, and lifecycle diagrams from a small JSON file — real shapes and connectors, not an embedded image. Ships with a Claude Skill (SKILL.md) and a CLI.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "bin": {
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "doctor": "node bin/cli.mjs doctor",
21
- "test": "node test/render-workflow.test.mjs && node test/render-architecture.test.mjs && node test/validate.test.mjs && node test/workflow-lib-smoke.test.mjs"
21
+ "test": "node test/render-workflow.test.mjs && node test/render-architecture.test.mjs && node test/render-sequence.test.mjs && node test/render-dataflow.test.mjs && node test/render-lifecycle.test.mjs && node test/validate.test.mjs && node test/workflow-lib-smoke.test.mjs"
22
22
  },
23
23
  "keywords": [
24
24
  "pptx",
@@ -27,6 +27,9 @@
27
27
  "claude-skill",
28
28
  "architecture-diagram",
29
29
  "workflow-diagram",
30
+ "sequence-diagram",
31
+ "dataflow-diagram",
32
+ "lifecycle-diagram",
30
33
  "hancom",
31
34
  "hanshow",
32
35
  "office-automation"
@@ -1,5 +1,6 @@
1
1
  import pptxgen from "pptxgenjs";
2
2
  import fs from "node:fs/promises";
3
+ import path from "node:path";
3
4
  import { drawNode, drawBoundary, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
4
5
  import { connector } from "../../lib/routes.mjs";
5
6
  import { repairPptx } from "../../lib/repair-pptx.mjs";
@@ -62,6 +63,7 @@ export async function renderArchitecture(inputPath, outputPath) {
62
63
  });
63
64
 
64
65
  const outFile = outputPath || raw.meta.output || "output.pptx";
66
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
65
67
  await pptx.writeFile({ fileName: outFile });
66
68
  await repairPptx(outFile);
67
69
  return outFile;
@@ -0,0 +1,73 @@
1
+ import pptxgen from "pptxgenjs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { drawNode, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
5
+ import { connector } from "../../lib/routes.mjs";
6
+ import { repairPptx } from "../../lib/repair-pptx.mjs";
7
+ import { validateSchema, validateDataflowLayout } from "../../lib/validate.mjs";
8
+ import { computeDataflowLayout, DATAFLOW_LAYOUT, sameRow } from "../../lib/layout.mjs";
9
+
10
+ export async function renderDataflow(inputPath, outputPath) {
11
+ const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
12
+ await validateSchema("dataflow", raw);
13
+
14
+ const { nodeBoxes, stageCenterX } = computeDataflowLayout(raw);
15
+ const problems = validateDataflowLayout(raw);
16
+ if (problems.length) {
17
+ throw new Error(`Dataflow layout validation failed:\n- ${problems.join("\n- ")}`);
18
+ }
19
+
20
+ const pptx = new pptxgen();
21
+ pptx.defineLayout({ name: "WIDE", width: 13.33, height: 7.5 });
22
+ pptx.layout = "WIDE";
23
+ const slide = pptx.addSlide();
24
+
25
+ slide.addText(raw.meta.title, {
26
+ x: 0.4, y: 0.2, w: 12.5, h: 0.5,
27
+ fontFace: FONT, fontSize: 20, bold: true, color: "0F172A",
28
+ });
29
+ if (raw.meta.subtitle) {
30
+ slide.addText(raw.meta.subtitle, {
31
+ x: 0.4, y: 0.65, w: 12.5, h: 0.3,
32
+ fontFace: FONT, fontSize: 11, color: "64748B",
33
+ });
34
+ }
35
+
36
+ raw.stages.forEach((s, i) => {
37
+ slide.addText(s.label, {
38
+ x: stageCenterX[i] - 1.0, y: DATAFLOW_LAYOUT.STAGE_LABEL_Y, w: 2.0, h: 0.3,
39
+ align: "center", fontFace: FONT, fontSize: 11, bold: true, color: "64748B",
40
+ });
41
+ });
42
+
43
+ for (const n of raw.nodes) {
44
+ drawNode(slide, { ...nodeBoxes[n.id], label: n.label, sublabel: n.sublabel, type: n.type, tag: n.tag });
45
+ }
46
+
47
+ for (const f of raw.flows) {
48
+ const variant = EDGE_VARIANTS[f.variant || "default"];
49
+ const from = nodeBoxes[f.from], to = nodeBoxes[f.to];
50
+ if (!from) throw new Error(`Flow references unknown node "${f.from}"`);
51
+ if (!to) throw new Error(`Flow references unknown node "${f.to}"`);
52
+ const label = f.classification ? `${f.label} (${f.classification})` : f.label;
53
+ connector(slide, from, to, {
54
+ color: variant.color, dashed: variant.dashed, label,
55
+ route: sameRow(from, to) ? "straight" : "elbow-right",
56
+ });
57
+ }
58
+
59
+ if (raw.cards && raw.cards.length) {
60
+ drawCards(slide, raw.cards, 6.5);
61
+ }
62
+
63
+ slide.addText("Made with a Claude Skill · native PPTX shapes, editable in PowerPoint", {
64
+ x: 0.4, y: 7.15, w: 12.5, h: 0.3,
65
+ fontFace: FONT, fontSize: 8, color: "94A3B8", italic: true,
66
+ });
67
+
68
+ const outFile = outputPath || raw.meta.output || "output.pptx";
69
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
70
+ await pptx.writeFile({ fileName: outFile });
71
+ await repairPptx(outFile);
72
+ return outFile;
73
+ }
@@ -0,0 +1,10 @@
1
+ import { renderWorkflow } from "../workflow/render-workflow.mjs";
2
+
3
+ // lifecycle.schema.json is structurally identical to workflow.schema.json
4
+ // (lanes/nodes/edges, same layout budget) -- it only differs in its
5
+ // diagram_type const and typical usage (states + terminal outcomes instead
6
+ // of process steps), so it reuses the workflow renderer wholesale rather
7
+ // than duplicating the same ~70 lines under a different file name.
8
+ export async function renderLifecycle(inputPath, outputPath) {
9
+ return renderWorkflow(inputPath, outputPath, "lifecycle");
10
+ }
@@ -0,0 +1,67 @@
1
+ import pptxgen from "pptxgenjs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { drawNode, drawLifeline, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
5
+ import { drawArrow } from "../../lib/routes.mjs";
6
+ import { repairPptx } from "../../lib/repair-pptx.mjs";
7
+ import { validateSchema, validateSequenceLayout } from "../../lib/validate.mjs";
8
+ import { computeSequenceLayout } from "../../lib/layout.mjs";
9
+
10
+ export async function renderSequence(inputPath, outputPath) {
11
+ const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
12
+ await validateSchema("sequence", raw);
13
+
14
+ const { participantBoxes, centerX, messageYs, lifelineBottom, cardsY } = computeSequenceLayout(raw);
15
+ const problems = validateSequenceLayout(raw);
16
+ if (problems.length) {
17
+ throw new Error(`Sequence layout validation failed:\n- ${problems.join("\n- ")}`);
18
+ }
19
+
20
+ const pptx = new pptxgen();
21
+ pptx.defineLayout({ name: "WIDE", width: 13.33, height: 7.5 });
22
+ pptx.layout = "WIDE";
23
+ const slide = pptx.addSlide();
24
+
25
+ slide.addText(raw.meta.title, {
26
+ x: 0.4, y: 0.2, w: 12.5, h: 0.5,
27
+ fontFace: FONT, fontSize: 20, bold: true, color: "0F172A",
28
+ });
29
+ if (raw.meta.subtitle) {
30
+ slide.addText(raw.meta.subtitle, {
31
+ x: 0.4, y: 0.65, w: 12.5, h: 0.3,
32
+ fontFace: FONT, fontSize: 11, color: "64748B",
33
+ });
34
+ }
35
+
36
+ for (const p of raw.participants) {
37
+ drawNode(slide, { ...participantBoxes[p.id], label: p.label, sublabel: p.sublabel, type: p.type });
38
+ drawLifeline(slide, {
39
+ x: centerX[p.id],
40
+ fromY: participantBoxes[p.id].y + participantBoxes[p.id].h,
41
+ toY: lifelineBottom,
42
+ });
43
+ }
44
+
45
+ raw.messages.forEach((m, i) => {
46
+ const variant = EDGE_VARIANTS[m.variant || "default"];
47
+ drawArrow(slide, {
48
+ x1: centerX[m.from], x2: centerX[m.to], y: messageYs[i],
49
+ color: variant.color, dashed: variant.dashed, label: m.label,
50
+ });
51
+ });
52
+
53
+ if (raw.cards && raw.cards.length) {
54
+ drawCards(slide, raw.cards, cardsY);
55
+ }
56
+
57
+ slide.addText("Made with a Claude Skill · native PPTX shapes, editable in PowerPoint", {
58
+ x: 0.4, y: 7.15, w: 12.5, h: 0.3,
59
+ fontFace: FONT, fontSize: 8, color: "94A3B8", italic: true,
60
+ });
61
+
62
+ const outFile = outputPath || raw.meta.output || "output.pptx";
63
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
64
+ await pptx.writeFile({ fileName: outFile });
65
+ await repairPptx(outFile);
66
+ return outFile;
67
+ }
@@ -1,14 +1,20 @@
1
1
  import pptxgen from "pptxgenjs";
2
2
  import fs from "node:fs/promises";
3
+ import path from "node:path";
3
4
  import { drawNode, drawLane, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
4
5
  import { connector } from "../../lib/routes.mjs";
5
6
  import { repairPptx } from "../../lib/repair-pptx.mjs";
6
7
  import { validateSchema, validateWorkflowLayout } from "../../lib/validate.mjs";
7
8
  import { computeWorkflowLayout } from "../../lib/layout.mjs";
8
9
 
9
- export async function renderWorkflow(inputPath, outputPath) {
10
+ // `schemaType` lets renderLifecycle (schemas/lifecycle.schema.json) reuse
11
+ // this renderer as-is: lifecycle JSON is structurally identical to
12
+ // workflow's (lanes/nodes/edges), it just validates against its own
13
+ // diagram_type const and typically uses the start/active/waiting/
14
+ // success/failure node types instead of workflow's process-step ones.
15
+ export async function renderWorkflow(inputPath, outputPath, schemaType = "workflow") {
10
16
  const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
11
- await validateSchema("workflow", raw);
17
+ await validateSchema(schemaType, raw);
12
18
 
13
19
  const { laneBoxes, nodeBoxes, cardsY } = computeWorkflowLayout(raw);
14
20
  const problems = validateWorkflowLayout(raw);
@@ -61,6 +67,7 @@ export async function renderWorkflow(inputPath, outputPath) {
61
67
  });
62
68
 
63
69
  const outFile = outputPath || raw.meta.output || "output.pptx";
70
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
64
71
  await pptx.writeFile({ fileName: outFile });
65
72
  await repairPptx(outFile);
66
73
  return outFile;
@@ -15,7 +15,7 @@
15
15
  "maxItems": 2
16
16
  },
17
17
  "nodeType": {
18
- "enum": ["client", "frontend", "neutral", "backend", "database", "cache", "queue", "external", "security", "decision"]
18
+ "enum": ["client", "frontend", "neutral", "backend", "database", "cache", "queue", "external", "security", "decision", "active", "waiting", "success", "failure"]
19
19
  },
20
20
  "route": {
21
21
  "enum": ["straight", "drop", "elbow-right", "arc-over-top"]
@@ -0,0 +1,80 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://claude-skills/pptx-diagram/schemas/dataflow.schema.json",
4
+ "title": "Native PPTX Dataflow Diagram",
5
+ "description": "Nodes sit in a stage (column, left-to-right) x row (0-4) grid. Flows carry a mandatory label plus an optional classification (e.g. 'PII touch', 'non-PII') for sensitivity/lineage annotations.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "diagram_type", "meta", "stages", "nodes", "flows"],
9
+ "properties": {
10
+ "schema_version": { "const": 1 },
11
+ "diagram_type": { "const": "dataflow" },
12
+ "meta": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["title"],
16
+ "properties": {
17
+ "title": { "type": "string", "minLength": 1 },
18
+ "subtitle": { "type": "string" },
19
+ "output": { "type": "string" }
20
+ }
21
+ },
22
+ "stages": {
23
+ "type": "array",
24
+ "minItems": 2,
25
+ "maxItems": 5,
26
+ "items": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["label"],
30
+ "properties": { "label": { "type": "string", "minLength": 1 } }
31
+ }
32
+ },
33
+ "nodes": {
34
+ "type": "array",
35
+ "minItems": 1,
36
+ "items": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["id", "type", "label", "stage", "row"],
40
+ "properties": {
41
+ "id": { "$ref": "common.schema.json#/$defs/id" },
42
+ "type": { "$ref": "common.schema.json#/$defs/nodeType" },
43
+ "label": { "type": "string", "minLength": 1 },
44
+ "sublabel": { "type": "string" },
45
+ "tag": { "type": "string" },
46
+ "stage": { "type": "integer", "minimum": 0, "maximum": 4 },
47
+ "row": { "type": "integer", "minimum": 0, "maximum": 4 }
48
+ }
49
+ }
50
+ },
51
+ "flows": {
52
+ "type": "array",
53
+ "items": {
54
+ "type": "object",
55
+ "additionalProperties": false,
56
+ "required": ["from", "to", "label"],
57
+ "properties": {
58
+ "from": { "$ref": "common.schema.json#/$defs/id" },
59
+ "to": { "$ref": "common.schema.json#/$defs/id" },
60
+ "label": { "type": "string", "minLength": 1 },
61
+ "classification": { "type": "string" },
62
+ "variant": { "enum": ["default", "emphasis", "security", "dashed"] }
63
+ }
64
+ }
65
+ },
66
+ "cards": {
67
+ "type": "array",
68
+ "items": {
69
+ "type": "object",
70
+ "additionalProperties": false,
71
+ "required": ["title", "items"],
72
+ "properties": {
73
+ "title": { "type": "string", "minLength": 1 },
74
+ "color": { "$ref": "common.schema.json#/$defs/hexColor" },
75
+ "items": { "type": "array", "items": { "type": "string" } }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://claude-skills/pptx-diagram/schemas/lifecycle.schema.json",
4
+ "title": "Native PPTX Lifecycle Diagram",
5
+ "description": "Structurally identical to the workflow schema (lanes/nodes/edges, same layout budget) -- use it for state machines instead of process flows. Convention: a 'main' lane for the happy-path states and a 'terminal' lane for end states, with node types start/active/waiting/success/failure.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "diagram_type", "meta", "lanes", "nodes", "edges"],
9
+ "properties": {
10
+ "schema_version": { "const": 1 },
11
+ "diagram_type": { "const": "lifecycle" },
12
+ "meta": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["title"],
16
+ "properties": {
17
+ "title": { "type": "string", "minLength": 1 },
18
+ "subtitle": { "type": "string" },
19
+ "output": { "type": "string" }
20
+ }
21
+ },
22
+ "lanes": {
23
+ "type": "array",
24
+ "minItems": 1,
25
+ "items": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "required": ["id", "label"],
29
+ "properties": {
30
+ "id": { "$ref": "common.schema.json#/$defs/id" },
31
+ "label": { "type": "string", "minLength": 1 },
32
+ "height": { "type": "number", "minimum": 1.2, "maximum": 3, "description": "inches; default 1.75" }
33
+ }
34
+ }
35
+ },
36
+ "nodes": {
37
+ "type": "array",
38
+ "minItems": 1,
39
+ "items": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["id", "lane", "type", "label"],
43
+ "properties": {
44
+ "id": { "$ref": "common.schema.json#/$defs/id" },
45
+ "lane": { "$ref": "common.schema.json#/$defs/id" },
46
+ "col": { "type": "integer", "minimum": 0, "maximum": 5, "description": "slot index within the lane; auto-assigned to the next free slot if omitted" },
47
+ "type": { "$ref": "common.schema.json#/$defs/nodeType" },
48
+ "label": { "type": "string", "minLength": 1 },
49
+ "sublabel": { "type": "string" },
50
+ "tag": { "type": "string" },
51
+ "width": { "type": "number", "minimum": 0.6, "maximum": 1.85, "description": "inches; default 1.8, capped so it never exceeds one slot" },
52
+ "height": { "type": "number", "minimum": 0.5, "maximum": 1.5, "description": "inches; default 0.95" }
53
+ }
54
+ }
55
+ },
56
+ "edges": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "required": ["from", "to"],
62
+ "properties": {
63
+ "from": { "$ref": "common.schema.json#/$defs/id" },
64
+ "to": { "$ref": "common.schema.json#/$defs/id" },
65
+ "label": { "type": "string" },
66
+ "variant": { "enum": ["default", "emphasis", "security", "dashed"] },
67
+ "route": { "$ref": "common.schema.json#/$defs/route" },
68
+ "bias": { "type": "number", "minimum": 0.1, "maximum": 0.9 }
69
+ }
70
+ }
71
+ },
72
+ "cards": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["title", "items"],
78
+ "properties": {
79
+ "title": { "type": "string", "minLength": 1 },
80
+ "color": { "$ref": "common.schema.json#/$defs/hexColor" },
81
+ "items": { "type": "array", "items": { "type": "string" } }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://claude-skills/pptx-diagram/schemas/sequence.schema.json",
4
+ "title": "Native PPTX Sequence Diagram",
5
+ "description": "Participants sit in a row (max 6, auto-spaced) with a vertical lifeline; messages are drawn in JSON order at auto-incrementing y positions. See renderers/sequence for the layout budget.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "diagram_type", "meta", "participants", "messages"],
9
+ "properties": {
10
+ "schema_version": { "const": 1 },
11
+ "diagram_type": { "const": "sequence" },
12
+ "meta": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["title"],
16
+ "properties": {
17
+ "title": { "type": "string", "minLength": 1 },
18
+ "subtitle": { "type": "string" },
19
+ "output": { "type": "string" }
20
+ }
21
+ },
22
+ "participants": {
23
+ "type": "array",
24
+ "minItems": 2,
25
+ "maxItems": 6,
26
+ "items": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["id", "type", "label"],
30
+ "properties": {
31
+ "id": { "$ref": "common.schema.json#/$defs/id" },
32
+ "type": { "$ref": "common.schema.json#/$defs/nodeType" },
33
+ "label": { "type": "string", "minLength": 1 },
34
+ "sublabel": { "type": "string" }
35
+ }
36
+ }
37
+ },
38
+ "messages": {
39
+ "type": "array",
40
+ "minItems": 1,
41
+ "maxItems": 9,
42
+ "items": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["from", "to", "label"],
46
+ "properties": {
47
+ "from": { "$ref": "common.schema.json#/$defs/id" },
48
+ "to": { "$ref": "common.schema.json#/$defs/id" },
49
+ "label": { "type": "string", "minLength": 1 },
50
+ "variant": { "enum": ["default", "emphasis", "security", "dashed"] }
51
+ }
52
+ }
53
+ },
54
+ "cards": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["title", "items"],
60
+ "properties": {
61
+ "title": { "type": "string", "minLength": 1 },
62
+ "color": { "$ref": "common.schema.json#/$defs/hexColor" },
63
+ "items": { "type": "array", "items": { "type": "string" } }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }