@@ -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+
80133def _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+
113238def _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:
136261def 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 )
0 commit comments