Coverage for diagrams / code_diagrams / generate.py: 100.00%

238 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1#!/usr/bin/env python3 

2"""Generate code flowcharts for GCO using pyflowchart + Playwright. 

3 

4For each ``(source_file, function)`` target in :data:`TARGETS`: 

5 

61. Parse the function body with :mod:`pyflowchart` to produce a 

7 flowchart.js DSL string. 

82. Emit an interactive HTML page (flowchart.js renders client-side). 

93. Render the same diagram to a PNG using a headless Chromium via 

10 :mod:`playwright` (optional — when skipped or unavailable, any older PNG 

11 for that target is removed so artifact timestamps cannot disagree). 

124. Stamp the HTML, PNG, generated catalogue, and source marker with one 

13 invocation-wide UTC generation time. 

145. Insert (idempotently) a source comment near the top of the source file 

15 pointing at the generated HTML and PNG. 

16 

17Outputs mirror the source tree under ``diagrams/code_diagrams/``: 

18 

19 lambda/analytics-presigned-url/handler.py::lambda_handler 

20 -> diagrams/code_diagrams/lambda/analytics-presigned-url/handler.lambda_handler.{html,png} 

21 

22The README in ``diagrams/code_diagrams/`` is regenerated at the end 

23with a hierarchical, grouped-by-top-level-directory index so the 

24listing reflects the actual project layout. 

25 

26Usage: 

27 GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/code_diagrams/generate.py 

28 GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/code_diagrams/generate.py --target lambda/analytics-presigned-url/handler.py:lambda_handler 

29 GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/code_diagrams/generate.py --skip-png 

30 GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/code_diagrams/generate.py --skip-marker 

31 python diagrams/code_diagrams/generate.py --strip-markers 

32""" 

33 

34from __future__ import annotations 

35 

36import argparse 

37import hashlib 

38import json 

39import re 

40import subprocess 

41import sys 

42from pathlib import Path 

43 

44# Add project root to path so direct script invocation works without a prior 

45# ``pip install -e .``. The project root is two parents up. 

46sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) 

47 

48from diagrams.code_diagrams._renderer import ( # noqa: E402 

49 RenderedTarget, 

50 _output_stem_for, 

51 prune_orphaned_artifacts, 

52 render_all, 

53 write_readme, 

54) 

55from diagrams.code_diagrams._source_marker import ( # noqa: E402 

56 SENTINEL, 

57 strip_all_markers, 

58 strip_markers_from, 

59 upsert_markers, 

60) 

61from diagrams.code_diagrams._targets import TARGETS, Target # noqa: E402 

62from diagrams.code_diagrams._timestamp import ( # noqa: E402 

63 generation_source_commit, 

64 generation_timestamp_utc, 

65) 

66from gco.lambda_shared_sources import LAMBDA_SHARED_SOURCE_TARGETS # noqa: E402 

67 

68_MARKER_BYTES_RE = re.compile( 

69 rb"(?:\r?\n)?# <" + re.escape(SENTINEL.encode()) + rb"> BEGIN[^\r\n]*\r?\n.*?" 

70 rb"# <" + re.escape(SENTINEL.encode()) + rb"> END(?:\r?\n){2}", 

71 re.DOTALL, 

72) 

73 

74#: Committed next to the catalogue README. Records, per charted source, the 

75#: SHA-256 of its marker-stripped bytes plus the timestamp and commit that 

76#: produced its artifacts. Two properties follow from keeping provenance 

77#: *per source* rather than catalogue-wide: 

78#: 

79#: 1. The contract check verifies working-tree sources against these digests 

80#: instead of resolving ``source_commit`` from Git history — a squash-merged 

81#: PR deletes its branch commits, so a recorded SHA is a human-readable 

82#: label, never a lookup key that must resolve in every future clone. 

83#: 2. Regeneration is incremental. Changing one charted file restamps only 

84#: that file's artifacts, so a PR's diagram diff stays proportional to the 

85#: code it actually touched instead of restamping all ~200 artifacts. 

86PROVENANCE_MANIFEST_NAME = "provenance.json" 

87 

88#: Bumped when the manifest layout changes. v1 recorded one catalogue-wide 

89#: ``generated_at`` / ``source_commit`` plus a flat ``source_digests`` map; 

90#: v2 records a per-source ``sources`` mapping. 

91PROVENANCE_SCHEMA_VERSION = 2 

92 

93#: Appended to every provenance failure so the fix never requires reading the 

94#: generator. Regeneration is incremental, so the command below re-renders 

95#: only the stale sources named in the message. 

96REGENERATION_HINT = ( 

97 "To fix: commit the source change first, then regenerate — " 

98 "``SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) " 

99 "GCO_DIAGRAM_SOURCE_COMMIT=$(git rev-parse HEAD) " 

100 "python diagrams/generate.py --code-only`` — and commit the result. " 

101 "The run is incremental: only the sources named above are re-rendered " 

102 "and restamped." 

103) 

104 

105 

106def _target_hint(sources: list[str]) -> str: 

107 """Suggest the narrowest regeneration command for ``sources``.""" 

108 if not sources or len(sources) > 4: 

109 return "" 

110 targets = " ".join(f"--target {source}:<function>" for source in sources) 

111 return ( 

112 " To re-render just these files, add " 

113 f"``{targets}`` (see the source's own marker block for its charted " 

114 "function names)." 

115 ) 

116 

117 

118def _without_generated_marker(source: bytes) -> bytes: 

119 """Remove only the generated marker bytes; preserve every other byte.""" 

120 return _MARKER_BYTES_RE.sub(b"", source) 

121 

122 

123def source_content_digest(source: bytes) -> str: 

124 """SHA-256 hex digest of a charted source with generated markers removed. 

125 

126 Marker blocks are excluded so that restamping timestamps/commits during 

127 regeneration never changes a source's recorded digest — only substantive 

128 code changes do. 

129 """ 

130 return hashlib.sha256(_without_generated_marker(source)).hexdigest() 

131 

132 

133def provenance_manifest_path(project_root: Path) -> Path: 

134 """Absolute path of the committed provenance manifest.""" 

135 return project_root / "diagrams" / "code_diagrams" / PROVENANCE_MANIFEST_NAME 

136 

137 

138def load_provenance_manifest(project_root: Path) -> dict[str, dict[str, str]]: 

139 """Return ``{source: {digest, generated_at, source_commit}}``. 

140 

141 Raises :class:`RuntimeError` with an actionable message when the manifest 

142 is absent or unreadable — the catalogue cannot be verified without it. 

143 """ 

144 path = provenance_manifest_path(project_root) 

145 try: 

146 raw = json.loads(path.read_text(encoding="utf-8")) 

147 except FileNotFoundError: 

148 raise RuntimeError( 

149 f"missing diagrams/code_diagrams/{PROVENANCE_MANIFEST_NAME}. {REGENERATION_HINT}" 

150 ) from None 

151 except (OSError, json.JSONDecodeError) as exc: 

152 raise RuntimeError( 

153 f"unreadable diagrams/code_diagrams/{PROVENANCE_MANIFEST_NAME}: {exc}. " 

154 f"{REGENERATION_HINT}" 

155 ) from exc 

156 

157 sources = raw.get("sources") 

158 if not isinstance(sources, dict) or not sources: 

159 raise RuntimeError(f"{PROVENANCE_MANIFEST_NAME} has no sources mapping") 

160 entries: dict[str, dict[str, str]] = {} 

161 for source, entry in sources.items(): 

162 if not isinstance(entry, dict) or not { 

163 "digest", 

164 "generated_at", 

165 "source_commit", 

166 } <= set(entry): 

167 raise RuntimeError( 

168 f"{PROVENANCE_MANIFEST_NAME} entry for {source} must record " 

169 "digest, generated_at, and source_commit" 

170 ) 

171 entries[source] = { 

172 key: str(entry[key]) for key in ("digest", "generated_at", "source_commit") 

173 } 

174 return entries 

175 

176 

177def newest_provenance_stamp(manifest: dict[str, dict[str, str]]) -> tuple[str, str]: 

178 """Return the ``(generated_at, source_commit)`` of the newest entry. 

179 

180 The catalogue README carries one stamp describing the most recent 

181 regeneration; per-source stamps live in the manifest and the markers. 

182 """ 

183 newest = max(manifest.values(), key=lambda entry: entry["generated_at"]) 

184 return newest["generated_at"], newest["source_commit"] 

185 

186 

187def write_provenance_manifest( 

188 *, 

189 project_root: Path, 

190 output_dir: Path, 

191 regenerated_targets: list[Target], 

192 generated_at: str, 

193 source_commit: str, 

194 catalog: list[Target], 

195) -> Path: 

196 """Merge this run's regenerated sources into the manifest and write it. 

197 

198 Entries for sources this run did not regenerate are preserved verbatim, 

199 which is what keeps an incremental run's diff small. Sources no longer in 

200 ``catalog`` are dropped. 

201 """ 

202 try: 

203 existing = load_provenance_manifest(project_root) 

204 except RuntimeError: 

205 existing = {} 

206 

207 charted = {target.source for target in catalog} 

208 merged = {source: entry for source, entry in existing.items() if source in charted} 

209 for source in sorted({target.source for target in regenerated_targets}): 

210 merged[source] = { 

211 "digest": source_content_digest((project_root / source).read_bytes()), 

212 "generated_at": generated_at, 

213 "source_commit": source_commit, 

214 } 

215 

216 manifest = {"schema_version": PROVENANCE_SCHEMA_VERSION, "sources": merged} 

217 path = output_dir / PROVENANCE_MANIFEST_NAME 

218 path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") 

219 return path 

220 

221 

222def select_stale_targets( 

223 *, 

224 project_root: Path, 

225 targets: list[Target], 

226 output_dir: Path, 

227) -> list[Target]: 

228 """Return the targets an incremental run must re-render. 

229 

230 A target is stale when its source's marker-stripped bytes no longer match 

231 the recorded digest (a substantive code change), when the source has no 

232 recorded provenance at all (newly charted), or when either committed 

233 artifact is missing. Everything else is already current and is left byte- 

234 for-byte alone. 

235 """ 

236 try: 

237 manifest = load_provenance_manifest(project_root) 

238 except RuntimeError: 

239 return list(targets) 

240 

241 digest_cache: dict[str, str] = {} 

242 stale: list[Target] = [] 

243 for target in targets: 

244 entry = manifest.get(target.source) 

245 if entry is None: 

246 stale.append(target) 

247 continue 

248 if target.source not in digest_cache: 

249 digest_cache[target.source] = source_content_digest( 

250 (project_root / target.source).read_bytes() 

251 ) 

252 if digest_cache[target.source] != entry["digest"]: 

253 stale.append(target) 

254 continue 

255 stem = _output_stem_for(target, output_dir=output_dir) 

256 if ( 

257 not stem.with_name(f"{stem.name}.html").is_file() 

258 or not stem.with_name(f"{stem.name}.png").is_file() 

259 ): 

260 stale.append(target) 

261 return stale 

262 

263 

264def _verify_targets_match_source_commit( 

265 *, project_root: Path, targets: list[Target], source_commit: str 

266) -> None: 

267 """Require marker-excluded target bytes to equal a real Git commit.""" 

268 object_type = subprocess.run( # noqa: S603 — fixed Git command, hex-only ref 

269 ["git", "cat-file", "-t", source_commit], 

270 cwd=project_root, 

271 capture_output=True, 

272 check=False, 

273 ) 

274 if object_type.returncode != 0: 

275 detail = object_type.stderr.decode(errors="replace").strip() 

276 raise RuntimeError(f"GCO_DIAGRAM_SOURCE_COMMIT {source_commit} does not resolve: {detail}") 

277 if object_type.stdout.strip() != b"commit": 

278 actual_type = object_type.stdout.decode(errors="replace").strip() 

279 raise RuntimeError( 

280 f"GCO_DIAGRAM_SOURCE_COMMIT {source_commit} is a {actual_type}, not a commit" 

281 ) 

282 

283 mismatches: list[str] = [] 

284 for source in sorted({target.source for target in targets}): 

285 result = subprocess.run( # noqa: S603 — fixed Git command and catalog paths 

286 ["git", "show", f"{source_commit}:{source}"], 

287 cwd=project_root, 

288 capture_output=True, 

289 check=False, 

290 ) 

291 if result.returncode != 0: 

292 detail = result.stderr.decode(errors="replace").strip() 

293 raise RuntimeError( 

294 f"cannot read {source} from GCO_DIAGRAM_SOURCE_COMMIT {source_commit}: {detail}" 

295 ) 

296 current = (project_root / source).read_bytes() 

297 if _without_generated_marker(current) != _without_generated_marker(result.stdout): 

298 mismatches.append(source) 

299 if mismatches: 

300 raise RuntimeError( 

301 "charted source bytes differ from GCO_DIAGRAM_SOURCE_COMMIT after " 

302 f"removing only generated markers: {mismatches}. Commit substantive " 

303 "source changes first, then regenerate from that commit." 

304 ) 

305 

306 

307def verify_targets_match_provenance_manifest( 

308 *, project_root: Path, targets: list[Target] 

309) -> dict[str, dict[str, str]]: 

310 """Require charted source bytes to match the committed digest manifest. 

311 

312 This is the repository-side freshness contract, and it is deliberately 

313 self-contained: it compares working-tree bytes (markers stripped) against 

314 the SHA-256 digests recorded at generation time and never resolves a 

315 recorded commit from Git history. The generation-time check 

316 (:func:`_verify_targets_match_source_commit`) still anchors generation to a 

317 real committed state on the machine running the generator — but once 

318 committed, the catalogue must stay verifiable in any clone: a squash merge 

319 deletes branch commits, so a recorded SHA can legitimately be unreachable 

320 while the catalogue remains exactly current. 

321 

322 Returns the loaded manifest so the caller can cross-check per-source 

323 marker and artifact stamps against it. 

324 """ 

325 manifest = load_provenance_manifest(project_root) 

326 

327 charted = sorted({target.source for target in targets}) 

328 missing = sorted(set(charted) - set(manifest)) 

329 retired = sorted(set(manifest) - set(charted)) 

330 if missing or retired: 

331 detail = [] 

332 if missing: 

333 detail.append(f"newly charted sources with no recorded provenance: {missing}") 

334 if retired: 

335 detail.append(f"recorded sources no longer in _targets.py: {retired}") 

336 raise RuntimeError( 

337 f"{PROVENANCE_MANIFEST_NAME} is out of sync with the target catalogue " 

338 f"({'; '.join(detail)}). {REGENERATION_HINT}" 

339 ) 

340 

341 mismatches = [ 

342 source 

343 for source in charted 

344 if source_content_digest((project_root / source).read_bytes()) != manifest[source]["digest"] 

345 ] 

346 if mismatches: 

347 raise RuntimeError( 

348 "these charted sources changed since their flowcharts were generated, so " 

349 f"the committed diagrams no longer describe them: {mismatches}. " 

350 f"{REGENERATION_HINT}{_target_hint(mismatches)}" 

351 ) 

352 return manifest 

353 

354 

355def main() -> None: 

356 """CLI entry point.""" 

357 parser = argparse.ArgumentParser( 

358 description="Generate GCO code flowcharts (HTML + PNG).", 

359 ) 

360 parser.add_argument( 

361 "--target", 

362 action="append", 

363 default=None, 

364 metavar="PATH:FUNC", 

365 help=( 

366 "Only generate the named target(s). Repeatable. " 

367 "Format: ``path/to/file.py:function_name``. " 

368 "Default: all targets." 

369 ), 

370 ) 

371 parser.add_argument( 

372 "--all", 

373 dest="force_all", 

374 action="store_true", 

375 help=( 

376 "Re-render every target instead of only the sources whose bytes " 

377 "changed. Restamps the whole catalogue, so prefer the default " 

378 "incremental run unless you changed the generator itself." 

379 ), 

380 ) 

381 parser.add_argument( 

382 "--skip-png", 

383 action="store_true", 

384 help=( 

385 "Skip Playwright PNG rendering, remove selected targets' stale " 

386 "PNGs, and still write HTML." 

387 ), 

388 ) 

389 parser.add_argument( 

390 "--require-png", 

391 action="store_true", 

392 help="Fail a canonical run if any selected PNG could not be rendered.", 

393 ) 

394 parser.add_argument( 

395 "--skip-marker", 

396 action="store_true", 

397 help="Don't insert ``# Flowchart:`` markers into source files.", 

398 ) 

399 parser.add_argument( 

400 "--strip-markers", 

401 action="store_true", 

402 help=( 

403 "Remove every existing ``# <pyflowchart-code-diagram>`` " 

404 "block from the source tree and exit. Useful when " 

405 "refactoring the generator's placement rules or when " 

406 "tearing down the feature entirely. Does not regenerate " 

407 "flowcharts — combine with a normal run afterwards if " 

408 "you want fresh markers." 

409 ), 

410 ) 

411 args = parser.parse_args() 

412 if args.require_png and args.skip_png: 

413 parser.error("--require-png cannot be combined with --skip-png") 

414 

415 project_root = Path(__file__).resolve().parent.parent.parent 

416 output_dir = Path(__file__).resolve().parent 

417 

418 if args.strip_markers: 

419 print("🧹 Stripping pyflowchart markers from source files") 

420 print("=" * 50) 

421 print(f" Project root : {project_root}") 

422 modified = strip_all_markers(project_root) 

423 print("\n" + "=" * 50) 

424 print(f"✅ Stripped markers from {modified} file(s).") 

425 return 

426 

427 selected = _filter_targets(TARGETS, args.target) 

428 full_catalog = args.target is None 

429 generated_at = generation_timestamp_utc() 

430 source_commit = generation_source_commit() 

431 

432 # Incremental by default: re-render only the sources whose marker-stripped 

433 # bytes differ from the recorded provenance (plus newly charted targets and 

434 # any missing artifact). This is what keeps a PR's diagram diff 

435 # proportional to the code it changed. ``--all`` and an explicit 

436 # ``--target`` both bypass the staleness filter. 

437 incremental = full_catalog and not args.force_all 

438 targets = ( 

439 select_stale_targets( 

440 project_root=project_root, 

441 targets=selected, 

442 output_dir=output_dir, 

443 ) 

444 if incremental 

445 else selected 

446 ) 

447 

448 print("🧭 GCO Code Flowchart Generator") 

449 print("=" * 50) 

450 print(f" Project root : {project_root}") 

451 print(f" Output dir : {output_dir}") 

452 print(f" Mode : {'incremental' if incremental else 'full'}") 

453 print(f" Targets : {len(targets)} of {len(selected)} selected") 

454 print(f" Generated at : {generated_at}") 

455 print(f" Source commit: {source_commit}") 

456 

457 if not targets: 

458 # Nothing changed. Leave every committed artifact, marker, README, and 

459 # manifest byte untouched so a no-op regeneration is a no-op diff. 

460 print("\n✅ Every charted source is already current; nothing to re-render.") 

461 return 

462 

463 _verify_targets_match_source_commit( 

464 project_root=project_root, 

465 targets=targets, 

466 source_commit=source_commit, 

467 ) 

468 

469 results = render_all( 

470 targets=targets, 

471 project_root=project_root, 

472 output_dir=output_dir, 

473 render_png=not args.skip_png, 

474 generated_at=generated_at, 

475 source_commit=source_commit, 

476 ) 

477 if args.require_png: 

478 missing_pngs = [ 

479 result.target.source + ":" + result.target.function 

480 for result in results 

481 if result.png_path is None 

482 ] 

483 if missing_pngs: 

484 sys.exit(f"Canonical generation requires every PNG; missing: {missing_pngs}") 

485 

486 if not args.skip_marker: 

487 # Refresh markers only in the sources we just re-rendered, so an 

488 # incremental run leaves every other charted file byte-identical. 

489 # Retired sources still get their stale markers pruned; the shared 

490 # Lambda copies stay in the allowed set (see marker_allowed_sources). 

491 prune_retired_markers(project_root, charted=marker_allowed_sources(TARGETS)) 

492 upsert_markers(results, project_root=project_root) 

493 _sync_shared_lambda_copies(project_root) 

494 

495 if full_catalog: 

496 prune_orphaned_artifacts(targets=TARGETS, output_dir=output_dir) 

497 

498 # Provenance and the index are refreshed on every run, including an 

499 # explicit ``--target`` selection: a run that restamps a source's marker 

500 # and artifacts without recording the new stamp would leave the repository 

501 # contract failing. Entries for sources this run did not touch are 

502 # preserved verbatim (manifest) or reconstructed from their recorded 

503 # provenance (index), so a partial run stays a partial diff. 

504 manifest_path = write_provenance_manifest( 

505 project_root=project_root, 

506 output_dir=output_dir, 

507 regenerated_targets=targets, 

508 generated_at=generated_at, 

509 source_commit=source_commit, 

510 catalog=TARGETS, 

511 ) 

512 print(f"📝 Wrote {manifest_path}") 

513 write_readme( 

514 _catalog_readme_entries( 

515 project_root=project_root, 

516 output_dir=output_dir, 

517 results=results, 

518 ), 

519 output_dir=output_dir, 

520 ) 

521 

522 print("\n" + "=" * 50) 

523 print("✅ Code flowchart generation complete!") 

524 print(f" Output directory: {output_dir.absolute()}") 

525 

526 

527def marker_allowed_sources(targets: list[Target]) -> set[str]: 

528 """Return every source permitted to carry a generated marker block. 

529 

530 That is the charted sources plus the checked-in copies of charted shared 

531 Lambda sources — the copies are byte-identical to their canonical source 

532 (a separate contract), so they carry its marker too. Omitting them makes 

533 marker pruning strip the copies and desynchronise them, so this 

534 computation is shared by the generator and the contract checker rather 

535 than duplicated in both. 

536 """ 

537 allowed = {target.source for target in targets} 

538 for canonical, copies in LAMBDA_SHARED_SOURCE_TARGETS.items(): 

539 if canonical in allowed: 

540 allowed.update(copies) 

541 return allowed 

542 

543 

544def prune_retired_markers(project_root: Path, *, charted: set[str]) -> int: 

545 """Strip markers from files that are no longer charted targets. 

546 

547 A full ``strip_all_markers`` + reinsert pass would rewrite every charted 

548 source on every run, which is exactly the catalogue-wide churn incremental 

549 generation exists to avoid. Only genuinely retired sources are touched. 

550 """ 

551 modified = 0 

552 for source_path in sorted(project_root.rglob("*.py")): 

553 relative = source_path.relative_to(project_root).as_posix() 

554 if relative in charted or "-build" in relative: 

555 continue 

556 if not relative.startswith(("app.py", "cli/", "gco/", "gco_mcp/", "lambda/")): 

557 continue 

558 original = source_path.read_text(encoding="utf-8") 

559 if SENTINEL not in original: 

560 continue 

561 stripped = strip_markers_from(original) 

562 if stripped != original: 

563 source_path.write_text(stripped, encoding="utf-8") 

564 print(f" 🧹 stripped retired marker from {relative}") 

565 modified += 1 

566 return modified 

567 

568 

569def _catalog_readme_entries( 

570 *, 

571 project_root: Path, 

572 output_dir: Path, 

573 results: list[RenderedTarget], 

574) -> list[RenderedTarget]: 

575 """Return one entry per catalogue target for the README index. 

576 

577 Freshly rendered targets contribute their real results; every other target 

578 is reconstructed from its recorded provenance stamp and committed artifact 

579 paths, so an incremental run still writes a complete index without 

580 re-rendering (or restamping) the untouched majority. 

581 """ 

582 rendered = {(result.target.source, result.target.function): result for result in results} 

583 manifest = load_provenance_manifest(project_root) 

584 entries: list[RenderedTarget] = [] 

585 for target in TARGETS: 

586 existing = rendered.get((target.source, target.function)) 

587 if existing is not None: 

588 entries.append(existing) 

589 continue 

590 entry = manifest[target.source] 

591 stem = _output_stem_for(target, output_dir=output_dir) 

592 png_path = stem.with_name(f"{stem.name}.png") 

593 entries.append( 

594 RenderedTarget( 

595 target=target, 

596 html_path=stem.with_name(f"{stem.name}.html"), 

597 png_path=png_path if png_path.is_file() else None, 

598 generated_at=entry["generated_at"], 

599 source_commit=entry["source_commit"], 

600 ) 

601 ) 

602 return entries 

603 

604 

605def _sync_shared_lambda_copies(project_root: Path) -> None: 

606 """Propagate refreshed canonical shared Lambda sources to their copies. 

607 

608 ``upsert_markers`` rewrites the pyflowchart header inside canonical 

609 shared sources (``lambda/tls-shared/backend_tls.py``, 

610 ``lambda/proxy-shared/proxy_utils.py``), but the checked-in per-function 

611 copies are not diagram targets, so a regeneration used to leave them one 

612 header behind. That drift is exactly what 

613 ``tests/test_lambda_shared_sources.py`` rejects and what made every 

614 deploy rewrite tracked files mid-run (``StackManager._sync_lambda_sources`` 

615 re-syncs at deploy time). Reuse the deploy path's own map so the two 

616 sync points can never disagree about what a copy is. 

617 """ 

618 synced = 0 

619 for source_rel, target_rels in LAMBDA_SHARED_SOURCE_TARGETS.items(): 

620 source = project_root / source_rel 

621 if not source.is_file(): 

622 continue 

623 source_bytes = source.read_bytes() 

624 for target_rel in target_rels: 

625 target = project_root / target_rel 

626 if not target.parent.is_dir(): 

627 continue 

628 if target.is_file() and target.read_bytes() == source_bytes: 

629 continue 

630 target.write_bytes(source_bytes) 

631 synced += 1 

632 print(f" Synced shared copy: {target_rel} <- {source_rel}") 

633 if synced: 

634 print(f"🔁 Refreshed {synced} shared Lambda cop{'y' if synced == 1 else 'ies'}.") 

635 

636 

637def _filter_targets( 

638 all_targets: list[Target], 

639 requested: list[str] | None, 

640) -> list[Target]: 

641 """Filter :data:`TARGETS` by optional ``PATH:FUNC`` arguments.""" 

642 if not requested: 

643 return list(all_targets) 

644 wanted = set(requested) 

645 filtered = [t for t in all_targets if f"{t.source}:{t.function}" in wanted] 

646 missing = wanted - {f"{t.source}:{t.function}" for t in filtered} 

647 if missing: 

648 sys.exit(f"Unknown target(s): {sorted(missing)}") 

649 return filtered 

650 

651 

652if __name__ == "__main__": 

653 main()