ptxiagram 0.2.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,13 +1,13 @@
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
4
 
5
- > Built with [Claude Code](https://claude.com/claude-code) — 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.
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.
6
6
 
7
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.
8
8
 
9
9
  - **Native shapes, not a picture** — every box and arrow is a real PPTX shape, editable after generation
10
- - **Three diagram types today** — `workflow` (swimlanes, happy-path flows), `architecture` (free-placed components, trust boundaries, hub-and-spoke), and `sequence` (participants + lifelines, API call chains)
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)
11
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
12
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)
13
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
@@ -32,6 +32,11 @@ Use ptxiagram to make a workflow diagram of our incident response process,
32
32
  with a lane per team, so I can paste it into the postmortem deck.
33
33
  ```
34
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.
38
+ ```
39
+
35
40
  The agent reads `SKILL.md`, writes a small JSON file describing your nodes and
36
41
  edges, and runs the CLI to produce a `.pptx` with real, editable shapes.
37
42
 
@@ -58,7 +63,7 @@ await renderWorkflow("my-diagram.workflow.json", "out.pptx");
58
63
 
59
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.
60
65
 
61
- See [`SKILL.md`](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.
62
67
 
63
68
  ## Why every file gets repaired
64
69
 
package/SKILL.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: ptxiagram
3
- description: Create architecture, workflow, and sequence 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, or API call sequence 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.2.0"
6
+ version: "0.3.0"
7
7
  status: prototype
8
8
  based_on: "gitbrent/PptxGenJS (MIT)"
9
9
  ---
@@ -52,6 +52,8 @@ validation and the embedded-text dump, just no rendered preview image).
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
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` |
55
57
 
56
58
  ## The loop
57
59
 
@@ -60,6 +62,8 @@ validation and the embedded-text dump, just no rendered preview image).
60
62
  - `examples/onboarding.workflow.json`
61
63
  - `examples/order-processing.architecture.json`
62
64
  - `examples/cache-miss.sequence.json`
65
+ - `examples/product-analytics.dataflow.json`
66
+ - `examples/agent-run.lifecycle.json`
63
67
  2. Write `<name>.<type>.json`.
64
68
  3. Render:
65
69
  ```bash
@@ -153,12 +157,59 @@ explicit `y` field. Up to 9 messages without `cards`, ~6 with (cards need
153
157
  room at the bottom, so the lifeline gets shorter). Self-messages (same
154
158
  `from`/`to`) aren't supported yet.
155
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
+
156
205
  ## Node types
157
206
 
158
207
  `client` `frontend` `neutral` `backend` `database` `cache` `queue`
159
- `external` `security` `decision` each maps to a shape preset (ellipse,
160
- rect, roundRect, can/cylinder, hexagon, cube, cloud, diamond) and a color
161
- 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.
162
213
 
163
214
  ## Route presets
164
215
 
@@ -195,8 +246,11 @@ this automatically; `check` verifies it actually happened. Source:
195
246
 
196
247
  ## What's not built yet
197
248
 
198
- - `dataflow`, `lifecycle` diagram types (`workflow`, `architecture`, and
199
- `sequence` exist so far)
200
249
  - No CJK exact-metrics font measurement — the label-width check is a
201
250
  conservative estimate, not real glyph widths
202
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
@@ -8,11 +8,19 @@ 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
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";
11
13
  import { validateAll } from "../lib/validate.mjs";
12
14
 
13
15
  const execFileAsync = promisify(execFile);
14
16
 
15
- const RENDERERS = { workflow: renderWorkflow, architecture: renderArchitecture, sequence: renderSequence };
17
+ const RENDERERS = {
18
+ workflow: renderWorkflow,
19
+ architecture: renderArchitecture,
20
+ sequence: renderSequence,
21
+ dataflow: renderDataflow,
22
+ lifecycle: renderLifecycle,
23
+ };
16
24
 
17
25
  function usage() {
18
26
  console.log(`Usage:
@@ -21,7 +29,7 @@ function usage() {
21
29
  cli check <output.pptx>
22
30
  cli doctor
23
31
 
24
- Types: workflow, architecture, sequence`);
32
+ Types: workflow, architecture, sequence, dataflow, lifecycle`);
25
33
  }
26
34
 
27
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,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,11 +1,14 @@
1
1
  export { renderWorkflow } from "./renderers/workflow/render-workflow.mjs";
2
2
  export { renderArchitecture } from "./renderers/architecture/render-architecture.mjs";
3
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";
4
6
  export {
5
7
  validateAll,
6
8
  validateSchema,
7
9
  validateWorkflowLayout,
8
10
  validateArchitectureLayout,
9
11
  validateSequenceLayout,
12
+ validateDataflowLayout,
10
13
  } from "./lib/validate.mjs";
11
14
  export { repairPptx } from "./lib/repair-pptx.mjs";
package/lib/layout.mjs CHANGED
@@ -70,6 +70,33 @@ export function computeSequenceLayout(raw) {
70
70
  return { participantBoxes, centerX, messageYs, lifelineBottom, cardsY };
71
71
  }
72
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
+
73
100
  const BOUNDARY_PAD = 0.3;
74
101
 
75
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
 
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 = "맑은 고딕";
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, computeSequenceLayout, 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,6 +133,22 @@ 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
+
136
152
  export function validateSequenceLayout(raw) {
137
153
  const problems = [];
138
154
  const { participantBoxes, messageYs, lifelineBottom } = computeSequenceLayout(raw);
@@ -176,7 +192,9 @@ export function validateSequenceLayout(raw) {
176
192
  export async function validateAll(type, raw) {
177
193
  await validateSchema(type, raw);
178
194
  if (type === "workflow") return validateWorkflowLayout(raw);
195
+ if (type === "lifecycle") return validateWorkflowLayout(raw); // same JSON shape as workflow
179
196
  if (type === "architecture") return validateArchitectureLayout(raw);
180
197
  if (type === "sequence") return validateSequenceLayout(raw);
198
+ if (type === "dataflow") return validateDataflowLayout(raw);
181
199
  throw new Error(`Unknown diagram type: ${type}`);
182
200
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ptxiagram",
3
- "version": "0.2.0",
4
- "description": "Generate native, editable PowerPoint (PPTX) architecture, workflow, and sequence 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/render-sequence.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",
@@ -28,6 +28,8 @@
28
28
  "architecture-diagram",
29
29
  "workflow-diagram",
30
30
  "sequence-diagram",
31
+ "dataflow-diagram",
32
+ "lifecycle-diagram",
31
33
  "hancom",
32
34
  "hanshow",
33
35
  "office-automation"
@@ -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
+ }
@@ -7,9 +7,14 @@ import { repairPptx } from "../../lib/repair-pptx.mjs";
7
7
  import { validateSchema, validateWorkflowLayout } from "../../lib/validate.mjs";
8
8
  import { computeWorkflowLayout } from "../../lib/layout.mjs";
9
9
 
10
- 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") {
11
16
  const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
12
- await validateSchema("workflow", raw);
17
+ await validateSchema(schemaType, raw);
13
18
 
14
19
  const { laneBoxes, nodeBoxes, cardsY } = computeWorkflowLayout(raw);
15
20
  const problems = validateWorkflowLayout(raw);
@@ -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
+ }