sdtk-kit 0.3.9 → 1.0.1

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