Skip to content

Commit e87e0f9

Browse files
committed
docs: generate api_references for upcomming API version
1 parent d7a6a00 commit e87e0f9

3 files changed

Lines changed: 1996 additions & 22 deletions

File tree

.claude/skills/api-diff/scripts/generate_migration_plan.py

Lines changed: 168 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,59 @@ def _parse_roadmap_not_implemented(roadmap_path: str) -> list[dict[str, str]]:
7777
return entries
7878

7979

80+
def _parse_roadmap_implemented(roadmap_path: str) -> list[dict[str, str]]:
81+
"""
82+
Parse the '## Implemented' section of ROADMAP.md (all subsections until '## Not Implemented').
83+
84+
Returns a list of dicts with keys:
85+
- api_section: raw name from column 1 (e.g. "File Systems")
86+
- resource_name: Terraform resource name from column 2 (e.g. "flashblade_file_system")
87+
- slug: normalized slug for matching
88+
"""
89+
text = Path(roadmap_path).read_text(encoding="utf-8")
90+
91+
match = re.search(r"^## Implemented\b", text, re.MULTILINE)
92+
if not match:
93+
return []
94+
95+
section_start = match.end()
96+
# Section ends at "## Not Implemented" or next non-subsection H2
97+
next_h2 = re.search(r"^## (?!#)", text[section_start:], re.MULTILINE)
98+
section_text = text[section_start: section_start + next_h2.start()] if next_h2 else text[section_start:]
99+
100+
entries: list[dict[str, str]] = []
101+
for line in section_text.splitlines():
102+
if not line.startswith("|"):
103+
continue
104+
if re.search(r"\|[-: ]+\|", line):
105+
continue # separator row
106+
107+
cols = [c.strip() for c in line.strip("|").split("|")]
108+
if len(cols) < 4:
109+
continue
110+
111+
api_section = cols[0].strip()
112+
if not api_section or api_section.lower() in ("api section", "api_section"):
113+
continue # header row
114+
115+
# Extract resource name — strip backticks, handle "Yes + Yes" style entries
116+
resource_raw = cols[1].strip().strip("`")
117+
resource_name = resource_raw if resource_raw.startswith("flashblade_") else None
118+
119+
# Only include Done entries
120+
status_col = cols[3].strip() if len(cols) > 3 else ""
121+
if status_col != "Done":
122+
continue
123+
124+
entries.append({
125+
"api_section": api_section,
126+
"resource_name": resource_name,
127+
"slug": _make_slug(api_section),
128+
})
129+
130+
return entries
131+
132+
80133
def _make_slug(name: str) -> str:
81134
"""Lowercase slug: letters and digits only, spaces → hyphens."""
82135
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
@@ -110,6 +163,78 @@ def _match_roadmap(normalized_path: str, roadmap_entries: list[dict[str, str]])
110163
# Migration plan builder
111164
# ---------------------------------------------------------------------------
112165

166+
_SCHEMA_SUFFIXES = ("Post", "Patch", "Get")
167+
168+
169+
def _schema_base_name(schema_name: str) -> str:
170+
"""Strip Post/Patch/Get suffix to get the base resource schema name."""
171+
for suffix in _SCHEMA_SUFFIXES:
172+
if schema_name.endswith(suffix) and len(schema_name) > len(suffix):
173+
return schema_name[: -len(suffix)]
174+
return schema_name
175+
176+
177+
def _group_modified_schemas(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
178+
"""
179+
Group modified schemas by base name (FileSystem + FileSystemPost + FileSystemPatch → FileSystem).
180+
Merge added/removed/changed fields (union, deduplicated).
181+
"""
182+
groups: dict[str, dict[str, Any]] = {}
183+
for item in items:
184+
base = _schema_base_name(item["schema_name"])
185+
details = item.get("details", {})
186+
if base not in groups:
187+
groups[base] = {
188+
"schema_name": base,
189+
"variants": [item["schema_name"]],
190+
"added_fields": list(details.get("added_fields", [])),
191+
"removed_fields": list(details.get("removed_fields", [])),
192+
"changed_fields": list(details.get("changed_fields", [])),
193+
"annotation": item.get("annotation", "needs_verification"),
194+
}
195+
else:
196+
g = groups[base]
197+
g["variants"].append(item["schema_name"])
198+
for f in details.get("added_fields", []):
199+
if f not in g["added_fields"]:
200+
g["added_fields"].append(f)
201+
for f in details.get("removed_fields", []):
202+
if f not in g["removed_fields"]:
203+
g["removed_fields"].append(f)
204+
for f in details.get("changed_fields", []):
205+
if f not in g["changed_fields"]:
206+
g["changed_fields"].append(f)
207+
# Promote annotation: real_change > needs_verification > swagger_artifact
208+
if item.get("annotation") == "real_change":
209+
g["annotation"] = "real_change"
210+
return list(groups.values())
211+
212+
213+
def _match_implemented(
214+
schema_base: str,
215+
implemented_entries: list[dict[str, str]],
216+
) -> dict[str, str] | None:
217+
"""
218+
Match a schema base name (e.g. "FileSystem", "QosPolicy") against implemented
219+
ROADMAP entries by converting both to slugs and checking for overlap.
220+
"""
221+
# Convert CamelCase to slug: "FileSystem" → "file-system", "QosPolicy" → "qos-policy"
222+
slug = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", schema_base).lower()
223+
slug = re.sub(r"[^a-z0-9]+", "-", slug).strip("-")
224+
225+
for entry in implemented_entries:
226+
entry_slug = entry["slug"]
227+
# Direct slug containment (both directions)
228+
if slug in entry_slug or entry_slug in slug:
229+
return entry
230+
# Word overlap: ≥2 shared words
231+
slug_words = set(slug.split("-"))
232+
entry_words = set(entry_slug.split("-"))
233+
if len(slug_words & entry_words) >= 2:
234+
return entry
235+
return None
236+
237+
113238
def _action_for_modified_schema(item: dict[str, Any]) -> str:
114239
details = item.get("details", {})
115240
added = details.get("added_fields", [])
@@ -136,30 +261,49 @@ def _action_for_new_resource(normalized_path: str) -> str:
136261
def build_migration_plan(
137262
diff: dict[str, Any],
138263
roadmap_entries: list[dict[str, str]],
264+
implemented_entries: list[dict[str, str]] | None = None,
139265
) -> dict[str, Any]:
140266
"""
141267
Build a 4-category migration plan from a diff.json dict.
142268
143269
Categories:
144-
update_models — modified_schemas where annotation != swagger_artifact
270+
update_models — modified_schemas grouped by base name, with implemented flag
145271
new_resources — new_endpoints deduplicated by normalized_path (GET as anchor)
146272
where annotation != swagger_artifact
147273
deprecated — removed_endpoints + removed_schemas where annotation != swagger_artifact
148274
roadmap_gaps — subset of new_resources matching a ROADMAP.md Candidate/Deferred entry
149275
"""
150-
# ---- update_models ----
276+
if implemented_entries is None:
277+
implemented_entries = []
278+
279+
# ---- update_models (grouped by base name, cross-referenced) ----
280+
raw_modified = [
281+
item for item in diff.get("modified_schemas", [])
282+
if item.get("annotation") != "swagger_artifact"
283+
]
284+
grouped = _group_modified_schemas(raw_modified)
285+
151286
update_models: list[dict[str, Any]] = []
152-
for item in diff.get("modified_schemas", []):
153-
if item.get("annotation") == "swagger_artifact":
154-
continue
155-
details = item.get("details", {})
287+
for g in grouped:
288+
impl_match = _match_implemented(g["schema_name"], implemented_entries)
289+
action = _action_for_modified_schema({
290+
"schema_name": g["schema_name"],
291+
"details": {
292+
"added_fields": g["added_fields"],
293+
"removed_fields": g["removed_fields"],
294+
"changed_fields": g["changed_fields"],
295+
},
296+
})
156297
update_models.append({
157-
"schema_name": item["schema_name"],
158-
"added_fields": details.get("added_fields", []),
159-
"removed_fields": details.get("removed_fields", []),
160-
"changed_fields": details.get("changed_fields", []),
161-
"annotation": item.get("annotation", "needs_verification"),
162-
"action": _action_for_modified_schema(item),
298+
"schema_name": g["schema_name"],
299+
"variants": g["variants"],
300+
"added_fields": g["added_fields"],
301+
"removed_fields": g["removed_fields"],
302+
"changed_fields": g["changed_fields"],
303+
"annotation": g["annotation"],
304+
"implemented": impl_match is not None,
305+
"terraform_resource": impl_match["resource_name"] if impl_match else None,
306+
"action": action,
163307
})
164308

165309
# ---- new_resources (deduplicated by normalized_path) ----
@@ -223,13 +367,16 @@ def build_migration_plan(
223367
"action": f"Remove {item['schema_name']} struct and all usages",
224368
})
225369

370+
impl_count = sum(1 for m in update_models if m["implemented"])
371+
226372
plan: dict[str, Any] = {
227373
"generated_from": {
228374
"old_version": diff.get("old_version", "unknown"),
229375
"new_version": diff.get("new_version", "unknown"),
230376
},
231377
"summary": {
232378
"update_models": len(update_models),
379+
"update_models_implemented": impl_count,
233380
"new_resources": len(new_resources),
234381
"deprecated": len(deprecated),
235382
"roadmap_gaps": len(roadmap_gaps),
@@ -270,26 +417,28 @@ def render_markdown(plan: dict[str, Any]) -> str:
270417
"",
271418
"| Category | Count |",
272419
"| -------- | ----- |",
273-
f"| Model updates | {s['update_models']} |",
420+
f"| Model updates | {s['update_models']} ({s['update_models_implemented']} impact implemented resources) |",
274421
f"| New resources | {s['new_resources']} |",
275422
f"| Deprecated | {s['deprecated']} |",
276423
f"| Roadmap gaps | {s['roadmap_gaps']} |",
277424
"",
278425
]
279426

280-
# update_models
427+
# update_models — sorted: implemented first, then not implemented
428+
sorted_models = sorted(plan["update_models"], key=lambda m: (not m.get("implemented", False), m["schema_name"]))
281429
lines += ["## Model Updates", ""]
282430
rows = [
283431
[
284432
item["schema_name"],
285433
", ".join(item["added_fields"]) or "—",
286434
", ".join(item["removed_fields"]) or "—",
287-
item["annotation"],
435+
"Yes" if item.get("implemented") else "No",
436+
item.get("terraform_resource") or "—",
288437
item["action"],
289438
]
290-
for item in plan["update_models"]
439+
for item in sorted_models
291440
]
292-
lines.append(_md_table(["Schema", "Added Fields", "Removed Fields", "Annotation", "Action"], rows))
441+
lines.append(_md_table(["Schema", "Added Fields", "Removed Fields", "Implemented", "Resource", "Action"], rows))
293442

294443
# new_resources
295444
lines += ["## New Resources", ""]
@@ -382,8 +531,9 @@ def main() -> int:
382531
diff = json.load(fh)
383532

384533
roadmap_entries = _parse_roadmap_not_implemented(args.roadmap_md)
534+
implemented_entries = _parse_roadmap_implemented(args.roadmap_md)
385535

386-
plan = build_migration_plan(diff, roadmap_entries)
536+
plan = build_migration_plan(diff, roadmap_entries, implemented_entries)
387537

388538
if args.format == "markdown":
389539
output = render_markdown(plan)

.mcp.json

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,32 @@
44
"type": "stdio",
55
"command": "uvx",
66
"args": [
7-
"--from", "git+https://github.com/oraios/serena",
8-
"serena", "start-mcp-server",
9-
"--context", "ide-assistant",
10-
"--project", "."
7+
"--from",
8+
"git+https://github.com/oraios/serena",
9+
"serena",
10+
"start-mcp-server",
11+
"--context",
12+
"ide-assistant",
13+
"--project",
14+
"."
1115
],
1216
"env": {
1317
"ENABLE_TOOL_SEARCH": "true"
1418
}
19+
},
20+
"gsd-workflow": {
21+
"command": "/usr/bin/node",
22+
"args": [
23+
"/usr/lib/node_modules/gsd-pi/packages/mcp-server/dist/cli.js"
24+
],
25+
"cwd": "/home/gule/Workspace/team-infrastructure/terraform-provider-flashblade",
26+
"env": {
27+
"GSD_CLI_PATH": "/usr/bin/gsd",
28+
"GSD_WORKFLOW_EXECUTORS_MODULE": "/home/gule/.gsd/agent/extensions/gsd/tools/workflow-tool-executors.js",
29+
"GSD_WORKFLOW_WRITE_GATE_MODULE": "/home/gule/.gsd/agent/extensions/gsd/bootstrap/write-gate.js",
30+
"GSD_PERSIST_WRITE_GATE_STATE": "1",
31+
"GSD_WORKFLOW_PROJECT_ROOT": "/home/gule/Workspace/team-infrastructure/terraform-provider-flashblade"
32+
}
1533
}
1634
}
1735
}

0 commit comments

Comments
 (0)