sdtk-kit 0.3.8 → 1.0.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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -177
  3. package/package.json +52 -46
  4. package/scripts/postinstall.js +40 -0
  5. package/assets/manifest/toolkit-bundle.manifest.json +0 -473
  6. package/assets/manifest/toolkit-bundle.sha256.txt +0 -93
  7. package/assets/toolkit/toolkit/AGENTS.md +0 -131
  8. package/assets/toolkit/toolkit/install.ps1 +0 -270
  9. package/assets/toolkit/toolkit/runtimes/claude/CLAUDE_TEMPLATE.md +0 -54
  10. package/assets/toolkit/toolkit/runtimes/codex/CODEX_TEMPLATE.md +0 -32
  11. package/assets/toolkit/toolkit/scripts/init-feature.ps1 +0 -261
  12. package/assets/toolkit/toolkit/scripts/install-claude-skills.ps1 +0 -129
  13. package/assets/toolkit/toolkit/scripts/install-codex-skills.ps1 +0 -189
  14. package/assets/toolkit/toolkit/scripts/uninstall-claude-skills.ps1 +0 -139
  15. package/assets/toolkit/toolkit/scripts/uninstall-codex-skills.ps1 +0 -116
  16. package/assets/toolkit/toolkit/sdtk.config.json +0 -28
  17. package/assets/toolkit/toolkit/sdtk.config.profiles.example.json +0 -50
  18. package/assets/toolkit/toolkit/skills/sdtk-api-design-spec/SKILL.md +0 -84
  19. package/assets/toolkit/toolkit/skills/sdtk-api-design-spec/references/API_DESIGN_CREATION_RULES.md +0 -22
  20. package/assets/toolkit/toolkit/skills/sdtk-api-design-spec/references/API_DESIGN_FLOWCHART_CREATION_RULES.md +0 -468
  21. package/assets/toolkit/toolkit/skills/sdtk-api-design-spec/references/FLOWCHART_CREATION_RULES.md +0 -20
  22. package/assets/toolkit/toolkit/skills/sdtk-api-design-spec/scripts/generate_api_design_detail.py +0 -656
  23. package/assets/toolkit/toolkit/skills/sdtk-api-doc/SKILL.md +0 -43
  24. package/assets/toolkit/toolkit/skills/sdtk-api-doc/references/API_DESIGN_FLOWCHART_CREATION_RULES.md +0 -468
  25. package/assets/toolkit/toolkit/skills/sdtk-api-doc/references/FLOWCHART_CREATION_RULES.md +0 -20
  26. package/assets/toolkit/toolkit/skills/sdtk-api-doc/references/YAML_CREATION_RULES.md +0 -128
  27. package/assets/toolkit/toolkit/skills/sdtk-arch/SKILL.md +0 -83
  28. package/assets/toolkit/toolkit/skills/sdtk-arch/references/API_DESIGN_CREATION_RULES.md +0 -22
  29. package/assets/toolkit/toolkit/skills/sdtk-arch/references/API_DESIGN_FLOWCHART_CREATION_RULES.md +0 -468
  30. package/assets/toolkit/toolkit/skills/sdtk-arch/references/FLOWCHART_CREATION_RULES.md +0 -20
  31. package/assets/toolkit/toolkit/skills/sdtk-arch/references/FLOW_ACTION_SPEC_CREATION_RULES.md +0 -200
  32. package/assets/toolkit/toolkit/skills/sdtk-arch/references/YAML_CREATION_RULES.md +0 -128
  33. package/assets/toolkit/toolkit/skills/sdtk-ba/SKILL.md +0 -29
  34. package/assets/toolkit/toolkit/skills/sdtk-design-layout/SKILL.md +0 -41
  35. package/assets/toolkit/toolkit/skills/sdtk-design-layout/scripts/render_design_layout_images.py +0 -213
  36. package/assets/toolkit/toolkit/skills/sdtk-dev/SKILL.md +0 -90
  37. package/assets/toolkit/toolkit/skills/sdtk-dev/prompts/code-quality-reviewer.md +0 -35
  38. package/assets/toolkit/toolkit/skills/sdtk-dev/prompts/implementer.md +0 -61
  39. package/assets/toolkit/toolkit/skills/sdtk-dev/prompts/spec-reviewer.md +0 -42
  40. package/assets/toolkit/toolkit/skills/sdtk-dev-backend/SKILL.md +0 -21
  41. package/assets/toolkit/toolkit/skills/sdtk-dev-frontend/SKILL.md +0 -19
  42. package/assets/toolkit/toolkit/skills/sdtk-orchestrator/SKILL.md +0 -80
  43. package/assets/toolkit/toolkit/skills/sdtk-pm/SKILL.md +0 -30
  44. package/assets/toolkit/toolkit/skills/sdtk-qa/SKILL.md +0 -53
  45. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/SKILL.md +0 -73
  46. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/references/FLOW_ACTION_SPEC_CREATION_RULES.md +0 -200
  47. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/references/excel-image-export.md +0 -51
  48. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/references/figma-mcp.md +0 -54
  49. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/references/numbering-rules.md +0 -76
  50. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/scripts/renumber_flow_action_spec_global.py +0 -136
  51. package/assets/toolkit/toolkit/skills/sdtk-screen-design-spec/scripts/validate_flow_action_spec_numbering.py +0 -249
  52. package/assets/toolkit/toolkit/skills/sdtk-test-case-spec/SKILL.md +0 -74
  53. package/assets/toolkit/toolkit/skills/sdtk-test-case-spec/references/TEST_CASE_CREATION_RULES.md +0 -129
  54. package/assets/toolkit/toolkit/skills/sdtk-test-case-spec/scripts/validate_test_case_spec.py +0 -97
  55. package/assets/toolkit/toolkit/skills/skills.catalog.yaml +0 -302
  56. package/assets/toolkit/toolkit/skills-claude/api-design-spec/SKILL.md +0 -76
  57. package/assets/toolkit/toolkit/skills-claude/api-doc/SKILL.md +0 -47
  58. package/assets/toolkit/toolkit/skills-claude/arch/SKILL.md +0 -72
  59. package/assets/toolkit/toolkit/skills-claude/ba/SKILL.md +0 -50
  60. package/assets/toolkit/toolkit/skills-claude/design-layout/SKILL.md +0 -25
  61. package/assets/toolkit/toolkit/skills-claude/dev/SKILL.md +0 -45
  62. package/assets/toolkit/toolkit/skills-claude/dev-backend/SKILL.md +0 -20
  63. package/assets/toolkit/toolkit/skills-claude/dev-frontend/SKILL.md +0 -18
  64. package/assets/toolkit/toolkit/skills-claude/orchestrator/SKILL.md +0 -63
  65. package/assets/toolkit/toolkit/skills-claude/pm/SKILL.md +0 -52
  66. package/assets/toolkit/toolkit/skills-claude/qa/SKILL.md +0 -48
  67. package/assets/toolkit/toolkit/skills-claude/screen-design-spec/SKILL.md +0 -68
  68. package/assets/toolkit/toolkit/skills-claude/test-case-spec/SKILL.md +0 -61
  69. package/assets/toolkit/toolkit/templates/QUALITY_CHECKLIST.md +0 -124
  70. package/assets/toolkit/toolkit/templates/README.md +0 -63
  71. package/assets/toolkit/toolkit/templates/SHARED_PLANNING.md +0 -80
  72. package/assets/toolkit/toolkit/templates/docs/api/API_DESIGN_CREATION_RULES.md +0 -22
  73. package/assets/toolkit/toolkit/templates/docs/api/API_DESIGN_DETAIL_TEMPLATE.md +0 -67
  74. package/assets/toolkit/toolkit/templates/docs/api/API_DESIGN_FLOWCHART_CREATION_RULES.md +0 -468
  75. package/assets/toolkit/toolkit/templates/docs/api/API_ENDPOINTS_TEMPLATE.md +0 -229
  76. package/assets/toolkit/toolkit/templates/docs/api/FEATURE_API_TEMPLATE.yaml +0 -20
  77. package/assets/toolkit/toolkit/templates/docs/api/FLOWCHART_CREATION_RULES.md +0 -20
  78. package/assets/toolkit/toolkit/templates/docs/api/YAML_CREATION_RULES.md +0 -128
  79. package/assets/toolkit/toolkit/templates/docs/api/feature_api_flow_list_TEMPLATE.txt +0 -12
  80. package/assets/toolkit/toolkit/templates/docs/architecture/ARCH_DESIGN_TEMPLATE.md +0 -109
  81. package/assets/toolkit/toolkit/templates/docs/database/DATABASE_SPEC_TEMPLATE.md +0 -175
  82. package/assets/toolkit/toolkit/templates/docs/design/DESIGN_LAYOUT_TEMPLATE.md +0 -49
  83. package/assets/toolkit/toolkit/templates/docs/dev/FEATURE_IMPL_PLAN_TEMPLATE.md +0 -73
  84. package/assets/toolkit/toolkit/templates/docs/product/BACKLOG_TEMPLATE.md +0 -50
  85. package/assets/toolkit/toolkit/templates/docs/product/PRD_TEMPLATE.md +0 -66
  86. package/assets/toolkit/toolkit/templates/docs/product/PROJECT_INITIATION_TEMPLATE.md +0 -98
  87. package/assets/toolkit/toolkit/templates/docs/qa/QA_RELEASE_REPORT_TEMPLATE.md +0 -61
  88. package/assets/toolkit/toolkit/templates/docs/qa/TEST_CASE_CREATION_RULES.md +0 -129
  89. package/assets/toolkit/toolkit/templates/docs/qa/TEST_CASE_TEMPLATE.md +0 -104
  90. package/assets/toolkit/toolkit/templates/docs/specs/BA_SPEC_TEMPLATE.md +0 -139
  91. package/assets/toolkit/toolkit/templates/docs/specs/FLOW_ACTION_SPEC_CREATION_RULES.md +0 -200
  92. package/assets/toolkit/toolkit/templates/docs/specs/FLOW_ACTION_SPEC_TEMPLATE.md +0 -172
  93. package/assets/toolkit/toolkit/templates/handoffs/ARCH_TO_DEV.md +0 -31
  94. package/assets/toolkit/toolkit/templates/handoffs/BA_TO_ARCH.md +0 -28
  95. package/assets/toolkit/toolkit/templates/handoffs/DEV_STAGE1_SPEC_REVIEW.md +0 -26
  96. package/assets/toolkit/toolkit/templates/handoffs/DEV_STAGE2_CODE_QUALITY_REVIEW.md +0 -20
  97. package/assets/toolkit/toolkit/templates/handoffs/DEV_TO_QA.md +0 -23
  98. package/assets/toolkit/toolkit/templates/handoffs/PM_TO_BA.md +0 -26
  99. package/assets/toolkit/toolkit/templates/handoffs/QA_RELEASE_DECISION.md +0 -21
  100. package/bin/sdtk.js +0 -15
  101. package/src/commands/auth.js +0 -85
  102. package/src/commands/generate.js +0 -177
  103. package/src/commands/help.js +0 -101
  104. package/src/commands/init.js +0 -97
  105. package/src/commands/runtime.js +0 -217
  106. package/src/index.js +0 -59
  107. package/src/lib/args.js +0 -116
  108. package/src/lib/errors.js +0 -41
  109. package/src/lib/github-access.js +0 -68
  110. package/src/lib/powershell.js +0 -85
  111. package/src/lib/scope.js +0 -68
  112. package/src/lib/state.js +0 -83
  113. package/src/lib/toolkit-payload.js +0 -99
@@ -1,656 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Generate [FEATURE_KEY]_API_DESIGN_DETAIL.md from OpenAPI YAML and flow list.
4
-
5
- This script follows API_DESIGN_FLOWCHART_CREATION_RULES.md and produces:
6
- - Markdown detail spec
7
- - Per-endpoint .puml files
8
- - Per-endpoint .svg flow images (if PlantUML render is available)
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import argparse
14
- import copy
15
- import os
16
- import re
17
- import shutil
18
- import subprocess
19
- import sys
20
- from pathlib import Path
21
- from typing import Dict, List, Optional, Tuple
22
-
23
- import yaml
24
-
25
-
26
- HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}
27
-
28
-
29
- def parse_args() -> argparse.Namespace:
30
- parser = argparse.ArgumentParser(description="Generate API design detail markdown from YAML + flow list.")
31
- parser.add_argument("--feature-key", required=True, help="Feature key, e.g. SCHEDULE_WHITEBOARD")
32
- parser.add_argument("--yaml", required=True, help="Path to OpenAPI YAML")
33
- parser.add_argument("--flow-list", required=True, help="Path to API flow list txt containing PlantUML blocks")
34
- parser.add_argument("--output", required=True, help="Output markdown path, e.g. docs/api/[FEATURE_KEY]_API_DESIGN_DETAIL.md")
35
- parser.add_argument("--flows-dir", default="docs/api/flows", help="Directory for generated .puml files")
36
- parser.add_argument("--images-dir", default="docs/api/images", help="Directory for rendered .svg files")
37
- parser.add_argument(
38
- "--include",
39
- action="append",
40
- default=[],
41
- help='Optional filter. Repeatable. Format: "METHOD /path" or "/path"',
42
- )
43
- parser.add_argument("--plantuml-jar", default="", help="Optional explicit path to plantuml.jar")
44
- parser.add_argument("--skip-render", action="store_true", help="Skip SVG rendering step")
45
- return parser.parse_args()
46
-
47
-
48
- def load_yaml(path: Path) -> dict:
49
- return yaml.safe_load(path.read_text(encoding="utf-8"))
50
-
51
-
52
- def normalize_feature_snake(feature_key: str) -> str:
53
- return re.sub(r"[^a-z0-9]+", "_", feature_key.lower()).strip("_")
54
-
55
-
56
- def parse_include_filters(raw_filters: List[str]) -> List[Tuple[Optional[str], str]]:
57
- parsed: List[Tuple[Optional[str], str]] = []
58
- for raw in raw_filters:
59
- text = raw.strip()
60
- if not text:
61
- continue
62
- parts = text.split(maxsplit=1)
63
- if len(parts) == 1:
64
- if not parts[0].startswith("/"):
65
- raise ValueError(f"Invalid --include format: {raw}")
66
- parsed.append((None, parts[0]))
67
- continue
68
- method = parts[0].lower().strip()
69
- path = parts[1].strip()
70
- if method not in HTTP_METHODS:
71
- raise ValueError(f"Invalid HTTP method in --include: {raw}")
72
- if not path.startswith("/"):
73
- raise ValueError(f"Path must start with '/' in --include: {raw}")
74
- parsed.append((method, path))
75
- return parsed
76
-
77
-
78
- def match_include(method: str, path: str, filters: List[Tuple[Optional[str], str]]) -> bool:
79
- if not filters:
80
- return True
81
- for f_method, f_path in filters:
82
- if f_path != path:
83
- continue
84
- if f_method is None or f_method == method:
85
- return True
86
- return False
87
-
88
-
89
- def collect_operations(spec: dict, include_filters: List[Tuple[Optional[str], str]]) -> List[Tuple[str, str, dict]]:
90
- operations: List[Tuple[str, str, dict]] = []
91
- for path, path_item in spec.get("paths", {}).items():
92
- for method, op in path_item.items():
93
- m = method.lower()
94
- if m not in HTTP_METHODS:
95
- continue
96
- if not match_include(m, path, include_filters):
97
- continue
98
- operations.append((m, path, op))
99
- return operations
100
-
101
-
102
- def collect_flow_map(flow_text: str) -> Dict[Tuple[str, str], str]:
103
- flow_map: Dict[Tuple[str, str], str] = {}
104
- blocks = re.findall(r"@startuml[\s\S]*?@enduml", flow_text)
105
- for block in blocks:
106
- m = re.search(r'partition\s+"([A-Z]+)\s+\*\*(.*?)\*\*', block)
107
- if not m:
108
- continue
109
- method = m.group(1).lower().strip()
110
- path = m.group(2).strip()
111
- sanitized = re.sub(r";<<#[^>]+>>", ";", block.strip())
112
- flow_map[(method, path)] = sanitized + "\n"
113
- return flow_map
114
-
115
-
116
- def slugify(text: str) -> str:
117
- s = re.sub(r"[^a-zA-Z0-9]+", "_", text.lower())
118
- s = re.sub(r"_+", "_", s).strip("_")
119
- return s
120
-
121
-
122
- def resolve_ref(spec: dict, ref: str):
123
- if not ref.startswith("#/components/"):
124
- raise ValueError(f"Unsupported ref: {ref}")
125
- cur = spec
126
- for p in ref.lstrip("#/").split("/"):
127
- cur = cur[p]
128
- return copy.deepcopy(cur)
129
-
130
-
131
- def deref(spec: dict, obj):
132
- if isinstance(obj, dict) and "$ref" in obj:
133
- base = deref(spec, resolve_ref(spec, obj["$ref"]))
134
- merged = copy.deepcopy(base)
135
- for k, v in obj.items():
136
- if k == "$ref":
137
- continue
138
- merged[k] = deref(spec, v)
139
- return merged
140
- if isinstance(obj, dict):
141
- return {k: deref(spec, v) for k, v in obj.items()}
142
- if isinstance(obj, list):
143
- return [deref(spec, v) for v in obj]
144
- return obj
145
-
146
-
147
- def normalize_schema(spec: dict, schema):
148
- s = deref(spec, schema)
149
- if not isinstance(s, dict):
150
- return {}
151
- if "allOf" in s:
152
- merged: Dict = {}
153
- merged_required: List[str] = []
154
- merged_props: Dict = {}
155
- for part in s["allOf"]:
156
- p = normalize_schema(spec, part)
157
- for k in ("type", "description", "format", "nullable", "default", "enum", "additionalProperties"):
158
- if k in p and k not in merged:
159
- merged[k] = p[k]
160
- for req in p.get("required", []):
161
- if req not in merged_required:
162
- merged_required.append(req)
163
- for prop_k, prop_v in p.get("properties", {}).items():
164
- merged_props[prop_k] = prop_v
165
- if "items" in p:
166
- merged["items"] = p["items"]
167
- if merged_required:
168
- merged["required"] = merged_required
169
- if merged_props:
170
- merged["properties"] = merged_props
171
- rest = {k: v for k, v in s.items() if k != "allOf"}
172
- merged.update(rest)
173
- s = merged
174
- return s
175
-
176
-
177
- def schema_type(spec: dict, schema) -> str:
178
- s = normalize_schema(spec, schema)
179
- t = s.get("type")
180
- if not t:
181
- if "properties" in s:
182
- t = "object"
183
- elif "items" in s:
184
- t = "array"
185
- elif s.get("additionalProperties"):
186
- t = "object"
187
- else:
188
- t = "object"
189
- if t == "array":
190
- items = normalize_schema(spec, s.get("items", {}))
191
- it = items.get("type", "object")
192
- return f"array<{it}>"
193
- return str(t)
194
-
195
-
196
- def schema_notes(spec: dict, schema) -> str:
197
- s = normalize_schema(spec, schema)
198
- notes: List[str] = []
199
- desc = s.get("description")
200
- if desc:
201
- notes.append(str(desc).replace("\n", " ").strip())
202
- if "enum" in s:
203
- notes.append("enum(" + ",".join(map(str, s["enum"])) + ")")
204
- if "default" in s:
205
- notes.append(f"default={s['default']}")
206
- if s.get("nullable") is True:
207
- notes.append("nullable")
208
- if s.get("additionalProperties") is True:
209
- notes.append("additionalProperties=true")
210
- return "; ".join(notes)
211
-
212
-
213
- def schema_format(spec: dict, schema) -> str:
214
- s = normalize_schema(spec, schema)
215
- return str(s.get("format", "")) if s.get("format") is not None else ""
216
-
217
-
218
- def schema_length(spec: dict, schema) -> str:
219
- fmt = schema_format(spec, schema)
220
- if fmt == "uuid":
221
- return "36"
222
- return ""
223
-
224
-
225
- def flatten_schema(spec: dict, schema) -> List[dict]:
226
- root = normalize_schema(spec, schema)
227
- rows: List[dict] = []
228
-
229
- def walk(node, prefix: List[str]):
230
- n = normalize_schema(spec, node)
231
- props = n.get("properties", {})
232
- required = set(n.get("required", []))
233
- for key, value in props.items():
234
- v = normalize_schema(spec, value)
235
- levels = prefix + [key]
236
- rows.append(
237
- {
238
- "levels": levels,
239
- "type": schema_type(spec, v),
240
- "format": schema_format(spec, v),
241
- "length": schema_length(spec, v),
242
- "required": "Yes" if key in required else "No",
243
- "notes": schema_notes(spec, v),
244
- }
245
- )
246
- t = v.get("type")
247
- if t == "object" or "properties" in v:
248
- if v.get("properties"):
249
- walk(v, levels)
250
- elif t == "array":
251
- items = normalize_schema(spec, v.get("items", {}))
252
- if items.get("type") == "object" or items.get("properties"):
253
- walk(items, levels)
254
-
255
- walk(root, [])
256
- return rows
257
-
258
-
259
- def table(headers: List[str], rows: List[str]) -> str:
260
- line1 = "| " + " | ".join(headers) + " |"
261
- line2 = "|" + "|".join([" ---: " if i == 0 else " --- " for i, _ in enumerate(headers)]) + "|"
262
- return "\n".join([line1, line2] + rows)
263
-
264
-
265
- def request_row(idx: int, row: dict) -> str:
266
- item_name = row["levels"][-1] if row["levels"] else ""
267
- levels = row["levels"][:6] + [""] * (6 - len(row["levels"][:6]))
268
- cols = [
269
- str(idx),
270
- item_name,
271
- levels[0],
272
- levels[1],
273
- levels[2],
274
- levels[3],
275
- levels[4],
276
- levels[5],
277
- row["type"],
278
- row["format"],
279
- row["length"],
280
- row["required"],
281
- row["notes"],
282
- ]
283
- return "| " + " | ".join(cols) + " |"
284
-
285
-
286
- def response_row(idx: int, row: dict) -> str:
287
- item_name = row["levels"][-1] if row["levels"] else ""
288
- levels = row["levels"][:6] + [""] * (6 - len(row["levels"][:6]))
289
- cols = [
290
- str(idx),
291
- item_name,
292
- levels[0],
293
- levels[1],
294
- levels[2],
295
- levels[3],
296
- levels[4],
297
- levels[5],
298
- row["type"],
299
- row["notes"],
300
- ]
301
- return "| " + " | ".join(cols) + " |"
302
-
303
-
304
- def get_request_schema(spec: dict, op: dict):
305
- rb = op.get("requestBody")
306
- if not rb:
307
- return None
308
- rb = deref(spec, rb)
309
- return (((rb.get("content") or {}).get("application/json") or {}).get("schema"))
310
-
311
-
312
- def get_response_schema(spec: dict, op: dict, code: str):
313
- resp = (op.get("responses") or {}).get(code)
314
- if not resp:
315
- return None
316
- resp = deref(spec, resp)
317
- return (((resp.get("content") or {}).get("application/json") or {}).get("schema"))
318
-
319
-
320
- def get_parameters(spec: dict, path_item: dict, op: dict) -> List[dict]:
321
- out: List[dict] = []
322
- for p in path_item.get("parameters", []):
323
- out.append(deref(spec, p))
324
- for p in op.get("parameters", []):
325
- out.append(deref(spec, p))
326
- return out
327
-
328
-
329
- def parse_description_sections(text: str) -> Dict[str, List[str]]:
330
- sections: Dict[str, List[str]] = {"process_flow": [], "notes": [], "login": []}
331
- current: Optional[str] = None
332
- for raw in text.splitlines():
333
- stripped = raw.strip()
334
- if not stripped:
335
- continue
336
- if stripped.startswith("## "):
337
- key = stripped[3:].strip().lower()
338
- if key == "process flow":
339
- current = "process_flow"
340
- elif key == "notes":
341
- current = "notes"
342
- elif key == "login":
343
- current = "login"
344
- else:
345
- current = None
346
- continue
347
- if current is None:
348
- continue
349
- if current == "process_flow" and raw.startswith(" ") and stripped.startswith("- ") and sections[current]:
350
- continuation = re.sub(r"^-\s*", "", stripped).strip()
351
- if continuation:
352
- sections[current][-1] = sections[current][-1] + f" {continuation}"
353
- continue
354
- normalized = re.sub(r"^\d+\.\s*", "", stripped)
355
- normalized = re.sub(r"^-\s*", "", normalized)
356
- normalized = normalized.strip()
357
- if normalized:
358
- sections[current].append(normalized)
359
- return sections
360
-
361
-
362
- def collect_error_cases(flow_text: str) -> List[Tuple[int, str]]:
363
- cases: Dict[int, List[str]] = {}
364
- for raw in flow_text.splitlines():
365
- line = raw.strip()
366
- m = re.search(r"Return error status =\s*(\d+)\s*\((.*?)\)", line)
367
- if m:
368
- code = int(m.group(1))
369
- reason = m.group(2).strip().rstrip(".;")
370
- cases.setdefault(code, [])
371
- if reason and reason not in cases[code]:
372
- cases[code].append(reason)
373
- if "Return business duplicate status" in line:
374
- cases.setdefault(100, [])
375
- reason = "duplicate/business rule rejection"
376
- if reason not in cases[100]:
377
- cases[100].append(reason)
378
- ordered: List[Tuple[int, str]] = []
379
- for code in sorted(cases.keys()):
380
- ordered.append((code, "; ".join(cases[code])))
381
- return ordered
382
-
383
-
384
- def find_plantuml_jar(explicit: str) -> Optional[Path]:
385
- if explicit:
386
- p = Path(explicit).expanduser().resolve()
387
- return p if p.exists() else None
388
-
389
- user_home = Path.home()
390
- candidates = sorted((user_home / ".vscode" / "extensions").glob("jebbs.plantuml-*/plantuml.jar"))
391
- if candidates:
392
- return candidates[-1]
393
- return None
394
-
395
-
396
- def render_svgs(plantuml_jar: Path, puml_files: List[Path], images_dir: Path) -> List[str]:
397
- errors: List[str] = []
398
- for puml in puml_files:
399
- proc = subprocess.run(
400
- ["java", "-jar", str(plantuml_jar), "-tsvg", str(puml)],
401
- stdout=subprocess.PIPE,
402
- stderr=subprocess.STDOUT,
403
- text=True,
404
- check=False,
405
- )
406
- svg_src = puml.with_suffix(".svg")
407
- svg_dst = images_dir / svg_src.name
408
- if not svg_src.exists():
409
- errors.append(f"Render failed (no svg): {puml}")
410
- continue
411
- shutil.copy2(svg_src, svg_dst)
412
- svg_src.unlink(missing_ok=True)
413
-
414
- svg_text = svg_dst.read_text(encoding="utf-8", errors="ignore")
415
- if any(x in svg_text for x in ("Cannot find group", "Syntax Error", "Some diagram description contains errors")):
416
- errors.append(f"Rendered with error content: {svg_dst}")
417
- if proc.returncode != 0:
418
- # Keep file if generated, but still report CLI failure.
419
- errors.append(f"PlantUML exit={proc.returncode} for {puml}")
420
- return errors
421
-
422
-
423
- def main() -> int:
424
- args = parse_args()
425
-
426
- feature_key = args.feature_key.strip()
427
- if not feature_key:
428
- raise ValueError("feature-key is empty")
429
- feature_snake = normalize_feature_snake(feature_key)
430
-
431
- yaml_path = Path(args.yaml)
432
- flow_path = Path(args.flow_list)
433
- output_path = Path(args.output)
434
- flows_dir = Path(args.flows_dir)
435
- images_dir = Path(args.images_dir)
436
-
437
- spec = load_yaml(yaml_path)
438
- include_filters = parse_include_filters(args.include)
439
- operations = collect_operations(spec, include_filters)
440
- if not operations:
441
- raise RuntimeError("No API operations selected from YAML")
442
-
443
- flow_map = collect_flow_map(flow_path.read_text(encoding="utf-8"))
444
- missing_flows: List[str] = []
445
-
446
- flows_dir.mkdir(parents=True, exist_ok=True)
447
- images_dir.mkdir(parents=True, exist_ok=True)
448
- output_path.parent.mkdir(parents=True, exist_ok=True)
449
-
450
- lines: List[str] = []
451
- lines.append(f"# {feature_key} API DESIGN DETAIL")
452
- lines.append("")
453
- lines.append("## 0. Abbreviations")
454
- lines.append("")
455
- lines.append("| No | Term | Meaning |")
456
- lines.append("| ---: | --- | --- |")
457
- lines.append("| 1 | API | Application Programming Interface |")
458
- lines.append("| 2 | UUID | Universally Unique Identifier |")
459
- lines.append("| 3 | FE | Frontend |")
460
- lines.append("| 4 | BE | Backend |")
461
- lines.append("| 5 | DT | Datetime |")
462
- lines.append("")
463
- lines.append("## 1. Document Scope")
464
- lines.append("")
465
- lines.append("| No | Method | Endpoint | Reference Template |")
466
- lines.append("| ---: | --- | --- | --- |")
467
- for i, (method, path, _op) in enumerate(operations, 1):
468
- lines.append(f"| {i} | {method.upper()} | `{path}` | `API_design.xlsx` style |")
469
- lines.append("")
470
-
471
- puml_files: List[Path] = []
472
-
473
- for i, (method, path, op) in enumerate(operations, 1):
474
- section_no = i + 1
475
- title = str(op.get("summary", f"{method.upper()} {path}")).strip()
476
- slug = f"{feature_snake}__{method}_{slugify(path)}"
477
- puml_name = f"{slug}.puml"
478
- svg_name = f"{slug}.svg"
479
- puml_path = flows_dir / puml_name
480
- svg_path = images_dir / svg_name
481
- svg_rel = Path(os.path.relpath(svg_path, output_path.parent))
482
- desc_sections = parse_description_sections(str(op.get("description", "")))
483
-
484
- flow = flow_map.get((method, path))
485
- if not flow:
486
- missing_flows.append(f"{method.upper()} {path}")
487
- flow = (
488
- "@startuml\n"
489
- f'partition "{method.upper()} **{path}**" {{\n'
490
- "start\n"
491
- ":TODO add flow;\n"
492
- "stop\n"
493
- "}\n"
494
- "@enduml\n"
495
- )
496
-
497
- puml_path.write_text(flow, encoding="utf-8")
498
- puml_files.append(puml_path)
499
-
500
- lines.append(f"## {section_no}. API Detail {i} - {title}")
501
- lines.append("")
502
- lines.append(f"**Endpoint:** `{method.upper()} {path}`")
503
- lines.append("")
504
- lines.append(f"### {section_no}.1 Process Flow")
505
- lines.append("")
506
- if desc_sections["process_flow"]:
507
- lines.append("**Flow Summary (from YAML description):**")
508
- lines.append("")
509
- for idx, item in enumerate(desc_sections["process_flow"], 1):
510
- lines.append(f"{idx}. {item}")
511
- lines.append("")
512
- if desc_sections["notes"]:
513
- lines.append("**Notes (from YAML description):**")
514
- lines.append("")
515
- for item in desc_sections["notes"]:
516
- lines.append(f"- {item}")
517
- lines.append("")
518
- if desc_sections["login"]:
519
- lines.append("**Login (from YAML description):**")
520
- lines.append("")
521
- for item in desc_sections["login"]:
522
- lines.append(f"- {item}")
523
- lines.append("")
524
- lines.append(f"Source of truth: `{flow_path.as_posix()}`")
525
- lines.append("")
526
- lines.append("```text")
527
- lines.extend(flow.rstrip("\n").split("\n"))
528
- lines.append("```")
529
- lines.append("")
530
- lines.append(f"![Flowchart - API {i} {title}]({svg_rel.as_posix()})")
531
- lines.append("")
532
-
533
- lines.append(f"### {section_no}.2 Parameters")
534
- lines.append("")
535
- path_item = spec.get("paths", {}).get(path, {})
536
- parameters = get_parameters(spec, path_item, op)
537
- if parameters:
538
- param_rows = ["| No | Parameter | Type | Required | Description |", "| ---: | --- | --- | --- | --- |"]
539
- for p_idx, p in enumerate(parameters, 1):
540
- sch = p.get("schema", {})
541
- p_type = sch.get("type", "string")
542
- p_fmt = sch.get("format")
543
- type_text = f"{p.get('in', 'path')} {p_type}" + (f"({p_fmt})" if p_fmt else "")
544
- req = "Yes" if p.get("required") else "No"
545
- desc = str(p.get("description", "")).replace("\n", " ").strip()
546
- param_rows.append(f"| {p_idx} | `{p.get('name', '')}` | {type_text} | {req} | {desc} |")
547
- lines.extend(param_rows)
548
- else:
549
- lines.append("`None`")
550
- lines.append("")
551
-
552
- lines.append(f"### {section_no}.3 Request Parameters (JSON format)")
553
- lines.append("")
554
- req_schema = get_request_schema(spec, op)
555
- if req_schema:
556
- req_rows = flatten_schema(spec, req_schema)
557
- req_md_rows = [request_row(x, r) for x, r in enumerate(req_rows, 1)]
558
- headers = [
559
- "No",
560
- "Item Name",
561
- "Level 1",
562
- "Level 2",
563
- "Level 3",
564
- "Level 4",
565
- "Level 5",
566
- "Level 6",
567
- "Type",
568
- "Format",
569
- "Length",
570
- "Required",
571
- "Notes",
572
- ]
573
- lines.append(table(headers, req_md_rows))
574
- else:
575
- lines.append("`None`")
576
- lines.append("")
577
-
578
- lines.append(f"### {section_no}.4 Success Response (JSON format)")
579
- lines.append("")
580
- ok_schema = get_response_schema(spec, op, "200")
581
- if ok_schema:
582
- ok_rows = flatten_schema(spec, ok_schema)
583
- ok_md_rows = [response_row(x, r) for x, r in enumerate(ok_rows, 1)]
584
- headers = ["No", "Item Name", "Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Type", "Notes"]
585
- lines.append(table(headers, ok_md_rows))
586
- else:
587
- lines.append("`None`")
588
- lines.append("")
589
-
590
- lines.append(f"### {section_no}.5 Error Response (JSON format)")
591
- lines.append("")
592
- err_schema = get_response_schema(spec, op, "400")
593
- if err_schema:
594
- err_rows = flatten_schema(spec, err_schema)
595
- err_md_rows = [response_row(x, r) for x, r in enumerate(err_rows, 1)]
596
- headers = ["No", "Item Name", "Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Type", "Notes"]
597
- lines.append(table(headers, err_md_rows))
598
- else:
599
- headers = ["No", "Item Name", "Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Type", "Notes"]
600
- shared_rows = [
601
- "| 1 | status | status | | | | | | integer | Shared business error status code in the common response envelope |"
602
- ]
603
- lines.append("Shared error envelope applies for non-200 exits.")
604
- lines.append("")
605
- lines.append(table(headers, shared_rows))
606
- inferred = collect_error_cases(flow)
607
- if inferred:
608
- lines.append("")
609
- lines.append("**Applicable Business Statuses:**")
610
- lines.append("")
611
- lines.append("| No | Status | Typical Trigger |")
612
- lines.append("| ---: | --- | --- |")
613
- for idx, (code, meaning) in enumerate(inferred, 1):
614
- lines.append(f"| {idx} | {code} | {meaning} |")
615
- lines.append("")
616
-
617
- final_section_no = len(operations) + 2
618
- lines.append(f"## {final_section_no}. Flowchart Image Rendering Recommendation (for Markdown)")
619
- lines.append("")
620
- lines.append("1. Keep `.puml` files in `docs/api/flows/` as source of truth.")
621
- lines.append("2. Render `.svg` files into `docs/api/images/` and embed them in markdown.")
622
- lines.append("3. Keep process flow code block as `text` to avoid duplicate diagram rendering in markdown preview.")
623
- lines.append("")
624
-
625
- output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
626
-
627
- render_errors: List[str] = []
628
- if not args.skip_render:
629
- jar = find_plantuml_jar(args.plantuml_jar)
630
- if jar:
631
- render_errors = render_svgs(jar, puml_files, images_dir)
632
- else:
633
- render_errors.append("PlantUML jar not found. Use --plantuml-jar or install VSCode PlantUML extension.")
634
-
635
- print(f"[OK] Generated markdown: {output_path}")
636
- print(f"[OK] Generated puml files: {len(puml_files)}")
637
- if missing_flows:
638
- print("[WARN] Missing flow blocks:")
639
- for item in missing_flows:
640
- print(f" - {item}")
641
- if render_errors:
642
- print("[WARN] Render issues:")
643
- for item in render_errors:
644
- print(f" - {item}")
645
- else:
646
- if not args.skip_render:
647
- print("[OK] Rendered SVG flow images successfully")
648
- return 0
649
-
650
-
651
- if __name__ == "__main__":
652
- try:
653
- raise SystemExit(main())
654
- except Exception as exc: # pragma: no cover
655
- print(f"[ERROR] {exc}", file=sys.stderr)
656
- raise
@@ -1,43 +0,0 @@
1
- ---
2
- name: sdtk-api-doc
3
- description: Generate OpenAPI 3.x YAML and PlantUML flow diagrams for a feature following this toolkit's API conventions. Use when you need to create/update docs/api/* (API spec + flow list) from BA_SPEC/ARCH_DESIGN.
4
- ---
5
-
6
- # SDTK API Documentation
7
-
8
- ## Critical Constraints
9
- - I do not invent API paths or payload contracts that contradict BA or ARCH artifacts.
10
- - I do not leave YAML, endpoint markdown, and flow-list outputs out of sync.
11
-
12
- ## Outputs
13
- - `docs/api/[FeaturePascal]_API.yaml`
14
- - `docs/api/[FEATURE_KEY]_ENDPOINTS.md`
15
- - `docs/api/[feature_snake]_api_flow_list.txt`
16
- - Optional downstream (via `sdtk-api-design-spec`):
17
- - `docs/api/[FEATURE_KEY]_API_DESIGN_DETAIL.md`
18
-
19
- ## Inputs (minimum)
20
- - Feature name/key
21
- - Entities + key fields
22
- - Use cases (UC-xx) + business rules (BR-xx)
23
- - Auth/permission model
24
-
25
- ## Process
26
- 1. Read `docs/specs/BA_SPEC_[FEATURE_KEY].md` and/or `docs/architecture/ARCH_DESIGN_[FEATURE_KEY].md`.
27
- 2. Read and apply split API rule sources:
28
- - `./references/YAML_CREATION_RULES.md` for YAML contract rules
29
- - `./references/API_DESIGN_FLOWCHART_CREATION_RULES.md` for flow list / flowchart rules
30
- 3. Define endpoints mapped to UC-xx; keep path naming consistent across CRUD/search/list/mst patterns and apply `governance/ai/core/SDTK_API_PATH_STYLE_POLICY.md` for canonical resource naming.
31
- 4. For each endpoint, document request/response schema and error cases.
32
- 5. Generate/update endpoint markdown (`[FEATURE_KEY]_ENDPOINTS.md`) with summary tables, API type grouping, and screen-logic mapping.
33
- 6. Generate PlantUML flows including auth, permission check, validation, main logic, and error exits.
34
- 7. Ensure traceability notes reference UC/BR where relevant.
35
- 8. For benchmark runs, if the requirement or upstream artifacts mark an OQ as expected OPEN, keep that ambiguity explicit in flow list / endpoint docs instead of silently collapsing it.
36
- 9. Validate English output hygiene when generating English artifacts:
37
- - no mixed-language leftovers in narrative text
38
- - no mojibake/encoding corruption markers
39
- - terminology consistency across endpoint detail, summary tables, and flow labels
40
- 10. If orchestrator mode requires API design detail generation (`apiDesignDetailMode=auto/on`), handoff to `sdtk-api-design-spec` after YAML + flow list are updated.
41
-
42
- ## Reference
43
- - Deeper analysis: `docs/specs/API_DOC_SKILL_ANALYSIS.md` (if present).