Coverage for diagrams / code_diagrams / generate.py: 100.00%
240 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-10-28 16:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-10-28 16:19 +0000
1#!/usr/bin/env python3
2"""Generate code flowcharts for GCO using pyflowchart + Playwright.
4For each ``(source_file, function)`` target in :data:`TARGETS`:
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.
17Outputs mirror the source tree under ``diagrams/code_diagrams/``:
19 lambda/analytics-presigned-url/handler.py::lambda_handler
20 -> diagrams/code_diagrams/lambda/analytics-presigned-url/handler.lambda_handler.{html,png}
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.
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"""
34from __future__ import annotations
36import argparse
37import hashlib
38import json
39import re
40import subprocess
41import sys
42from pathlib import Path
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))
48from diagrams.code_diagrams._renderer import (
49 RenderedTarget,
50 _output_stem_for,
51 prune_orphaned_artifacts,
52 render_all,
53 write_readme,
54)
55from diagrams.code_diagrams._source_marker import (
56 SENTINEL,
57 is_marker_source,
58 strip_all_markers,
59 strip_markers_from,
60 upsert_markers,
61)
62from diagrams.code_diagrams._targets import TARGETS, Target
63from diagrams.code_diagrams._timestamp import (
64 generation_source_commit,
65 generation_timestamp_utc,
66)
67from gco.lambda_shared_sources import LAMBDA_SHARED_SOURCE_TARGETS
69_MARKER_BYTES_RE = re.compile(
70 rb"(?:\r?\n)?# <" + re.escape(SENTINEL.encode()) + rb"> BEGIN[^\r\n]*\r?\n.*?"
71 rb"# <" + re.escape(SENTINEL.encode()) + rb"> END(?:\r?\n){2}",
72 re.DOTALL,
73)
75#: Committed next to the catalogue README. Records, per charted source, the
76#: SHA-256 of its marker-stripped bytes plus the timestamp and commit that
77#: produced its artifacts. Two properties follow from keeping provenance
78#: *per source* rather than catalogue-wide:
79#:
80#: 1. The contract check verifies working-tree sources against these digests
81#: instead of resolving ``source_commit`` from Git history — a squash-merged
82#: PR deletes its branch commits, so a recorded SHA is a human-readable
83#: label, never a lookup key that must resolve in every future clone.
84#: 2. Regeneration is incremental. Changing one charted file restamps only
85#: that file's artifacts, so a PR's diagram diff stays proportional to the
86#: code it actually touched instead of restamping all ~200 artifacts.
87PROVENANCE_MANIFEST_NAME = "provenance.json"
89#: Bumped when the manifest layout changes. v1 recorded one catalogue-wide
90#: ``generated_at`` / ``source_commit`` plus a flat ``source_digests`` map;
91#: v2 records a per-source ``sources`` mapping.
92PROVENANCE_SCHEMA_VERSION = 2
94#: Appended to every provenance failure so the fix never requires reading the
95#: generator. Regeneration is incremental, so the command below re-renders
96#: only the stale sources named in the message.
97REGENERATION_HINT = (
98 "To fix: commit the source change first, then regenerate — "
99 "``SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) "
100 "GCO_DIAGRAM_SOURCE_COMMIT=$(git rev-parse HEAD) "
101 "python diagrams/generate.py --code-only`` — and commit the result. "
102 "The run is incremental: only the sources named above are re-rendered "
103 "and restamped."
104)
107def _target_hint(sources: list[str]) -> str:
108 """Suggest the narrowest regeneration command for ``sources``."""
109 if not sources or len(sources) > 4:
110 return ""
111 targets = " ".join(f"--target {source}:<function>" for source in sources)
112 return (
113 " To re-render just these files, add "
114 f"``{targets}`` (see the source's own marker block for its charted "
115 "function names)."
116 )
119def _without_generated_marker(source: bytes) -> bytes:
120 """Remove only the generated marker bytes; preserve every other byte."""
121 return _MARKER_BYTES_RE.sub(b"", source)
124def source_content_digest(source: bytes) -> str:
125 """SHA-256 hex digest of a charted source with generated markers removed.
127 Marker blocks are excluded so that restamping timestamps/commits during
128 regeneration never changes a source's recorded digest — only substantive
129 code changes do.
130 """
131 return hashlib.sha256(_without_generated_marker(source)).hexdigest()
134def provenance_manifest_path(project_root: Path) -> Path:
135 """Absolute path of the committed provenance manifest."""
136 return project_root / "diagrams" / "code_diagrams" / PROVENANCE_MANIFEST_NAME
139def load_provenance_manifest(project_root: Path) -> dict[str, dict[str, str]]:
140 """Return ``{source: {digest, generated_at, source_commit}}``.
142 Raises :class:`RuntimeError` with an actionable message when the manifest
143 is absent or unreadable — the catalogue cannot be verified without it.
144 """
145 path = provenance_manifest_path(project_root)
146 try:
147 raw = json.loads(path.read_text(encoding="utf-8"))
148 except FileNotFoundError:
149 raise RuntimeError(
150 f"missing diagrams/code_diagrams/{PROVENANCE_MANIFEST_NAME}. {REGENERATION_HINT}"
151 ) from None
152 except (OSError, json.JSONDecodeError) as exc:
153 raise RuntimeError(
154 f"unreadable diagrams/code_diagrams/{PROVENANCE_MANIFEST_NAME}: {exc}. "
155 f"{REGENERATION_HINT}"
156 ) from exc
158 sources = raw.get("sources")
159 if not isinstance(sources, dict) or not sources:
160 raise RuntimeError(f"{PROVENANCE_MANIFEST_NAME} has no sources mapping")
161 entries: dict[str, dict[str, str]] = {}
162 for source, entry in sources.items():
163 if not isinstance(entry, dict) or not {
164 "digest",
165 "generated_at",
166 "source_commit",
167 } <= set(entry):
168 raise RuntimeError(
169 f"{PROVENANCE_MANIFEST_NAME} entry for {source} must record "
170 "digest, generated_at, and source_commit"
171 )
172 entries[source] = {
173 key: str(entry[key]) for key in ("digest", "generated_at", "source_commit")
174 }
175 return entries
178def newest_provenance_stamp(manifest: dict[str, dict[str, str]]) -> tuple[str, str]:
179 """Return the ``(generated_at, source_commit)`` of the newest entry.
181 The catalogue README carries one stamp describing the most recent
182 regeneration; per-source stamps live in the manifest and the markers.
183 """
184 newest = max(manifest.values(), key=lambda entry: entry["generated_at"])
185 return newest["generated_at"], newest["source_commit"]
188def write_provenance_manifest(
189 *,
190 project_root: Path,
191 output_dir: Path,
192 regenerated_targets: list[Target],
193 generated_at: str,
194 source_commit: str,
195 catalog: list[Target],
196) -> Path:
197 """Merge this run's regenerated sources into the manifest and write it.
199 Entries for sources this run did not regenerate are preserved verbatim,
200 which is what keeps an incremental run's diff small. Sources no longer in
201 ``catalog`` are dropped.
202 """
203 try:
204 existing = load_provenance_manifest(project_root)
205 except RuntimeError:
206 existing = {}
208 charted = {target.source for target in catalog}
209 merged = {source: entry for source, entry in existing.items() if source in charted}
210 for source in sorted({target.source for target in regenerated_targets}):
211 merged[source] = {
212 "digest": source_content_digest((project_root / source).read_bytes()),
213 "generated_at": generated_at,
214 "source_commit": source_commit,
215 }
217 manifest = {"schema_version": PROVENANCE_SCHEMA_VERSION, "sources": merged}
218 path = output_dir / PROVENANCE_MANIFEST_NAME
219 path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
220 return path
223def select_stale_targets(
224 *,
225 project_root: Path,
226 targets: list[Target],
227 output_dir: Path,
228) -> list[Target]:
229 """Return the targets an incremental run must re-render.
231 A source is stale when its marker-stripped bytes no longer match the
232 recorded digest (a substantive code change), when it has no recorded
233 provenance at all (newly charted), or when either committed artifact of
234 any of its targets is missing (for example, a function newly charted from
235 an already-charted source). Every target of a stale source is selected:
236 the source's marker lists all of its charted functions and one provenance
237 stamp covers all of their artifacts, so re-rendering only some of them
238 would drop the rest from the marker and restamp the source past their
239 artifacts. Everything else is already current and is left byte-for-byte
240 alone.
241 """
242 try:
243 manifest = load_provenance_manifest(project_root)
244 except RuntimeError:
245 return list(targets)
247 digest_cache: dict[str, str] = {}
248 stale_sources: set[str] = set()
249 for target in targets:
250 if target.source in stale_sources:
251 continue
252 entry = manifest.get(target.source)
253 if entry is None:
254 stale_sources.add(target.source)
255 continue
256 if target.source not in digest_cache:
257 digest_cache[target.source] = source_content_digest(
258 (project_root / target.source).read_bytes()
259 )
260 if digest_cache[target.source] != entry["digest"]:
261 stale_sources.add(target.source)
262 continue
263 stem = _output_stem_for(target, output_dir=output_dir)
264 if (
265 not stem.with_name(f"{stem.name}.html").is_file()
266 or not stem.with_name(f"{stem.name}.png").is_file()
267 ):
268 stale_sources.add(target.source)
269 return [target for target in targets if target.source in stale_sources]
272def _verify_targets_match_source_commit(
273 *, project_root: Path, targets: list[Target], source_commit: str
274) -> None:
275 """Require marker-excluded target bytes to equal a real Git commit."""
276 object_type = subprocess.run( # fixed Git command, hex-only ref
277 ["git", "cat-file", "-t", source_commit],
278 cwd=project_root,
279 capture_output=True,
280 check=False,
281 )
282 if object_type.returncode != 0:
283 detail = object_type.stderr.decode(errors="replace").strip()
284 raise RuntimeError(f"GCO_DIAGRAM_SOURCE_COMMIT {source_commit} does not resolve: {detail}")
285 if object_type.stdout.strip() != b"commit":
286 actual_type = object_type.stdout.decode(errors="replace").strip()
287 raise RuntimeError(
288 f"GCO_DIAGRAM_SOURCE_COMMIT {source_commit} is a {actual_type}, not a commit"
289 )
291 mismatches: list[str] = []
292 for source in sorted({target.source for target in targets}):
293 result = subprocess.run( # fixed Git command and catalog paths
294 ["git", "show", f"{source_commit}:{source}"],
295 cwd=project_root,
296 capture_output=True,
297 check=False,
298 )
299 if result.returncode != 0:
300 detail = result.stderr.decode(errors="replace").strip()
301 raise RuntimeError(
302 f"cannot read {source} from GCO_DIAGRAM_SOURCE_COMMIT {source_commit}: {detail}"
303 )
304 current = (project_root / source).read_bytes()
305 if _without_generated_marker(current) != _without_generated_marker(result.stdout):
306 mismatches.append(source)
307 if mismatches:
308 raise RuntimeError(
309 "charted source bytes differ from GCO_DIAGRAM_SOURCE_COMMIT after "
310 f"removing only generated markers: {mismatches}. Commit substantive "
311 "source changes first, then regenerate from that commit."
312 )
315def verify_targets_match_provenance_manifest(
316 *, project_root: Path, targets: list[Target]
317) -> dict[str, dict[str, str]]:
318 """Require charted source bytes to match the committed digest manifest.
320 This is the repository-side freshness contract, and it is deliberately
321 self-contained: it compares working-tree bytes (markers stripped) against
322 the SHA-256 digests recorded at generation time and never resolves a
323 recorded commit from Git history. The generation-time check
324 (:func:`_verify_targets_match_source_commit`) still anchors generation to a
325 real committed state on the machine running the generator — but once
326 committed, the catalogue must stay verifiable in any clone: a squash merge
327 deletes branch commits, so a recorded SHA can legitimately be unreachable
328 while the catalogue remains exactly current.
330 Returns the loaded manifest so the caller can cross-check per-source
331 marker and artifact stamps against it.
332 """
333 manifest = load_provenance_manifest(project_root)
335 charted = sorted({target.source for target in targets})
336 missing = sorted(set(charted) - set(manifest))
337 retired = sorted(set(manifest) - set(charted))
338 if missing or retired:
339 detail = []
340 if missing:
341 detail.append(f"newly charted sources with no recorded provenance: {missing}")
342 if retired:
343 detail.append(f"recorded sources no longer in _targets.py: {retired}")
344 raise RuntimeError(
345 f"{PROVENANCE_MANIFEST_NAME} is out of sync with the target catalogue "
346 f"({'; '.join(detail)}). {REGENERATION_HINT}"
347 )
349 mismatches = [
350 source
351 for source in charted
352 if source_content_digest((project_root / source).read_bytes()) != manifest[source]["digest"]
353 ]
354 if mismatches:
355 raise RuntimeError(
356 "these charted sources changed since their flowcharts were generated, so "
357 f"the committed diagrams no longer describe them: {mismatches}. "
358 f"{REGENERATION_HINT}{_target_hint(mismatches)}"
359 )
360 return manifest
363def main() -> None:
364 """CLI entry point."""
365 parser = argparse.ArgumentParser(
366 description="Generate GCO code flowcharts (HTML + PNG).",
367 )
368 parser.add_argument(
369 "--target",
370 action="append",
371 default=None,
372 metavar="PATH:FUNC",
373 help=(
374 "Only generate the named target(s). Repeatable. "
375 "Format: ``path/to/file.py:function_name``. "
376 "Default: all targets."
377 ),
378 )
379 parser.add_argument(
380 "--all",
381 dest="force_all",
382 action="store_true",
383 help=(
384 "Re-render every target instead of only the sources whose bytes "
385 "changed. Restamps the whole catalogue, so prefer the default "
386 "incremental run unless you changed the generator itself."
387 ),
388 )
389 parser.add_argument(
390 "--skip-png",
391 action="store_true",
392 help=(
393 "Skip Playwright PNG rendering, remove selected targets' stale "
394 "PNGs, and still write HTML."
395 ),
396 )
397 parser.add_argument(
398 "--require-png",
399 action="store_true",
400 help="Fail a canonical run if any selected PNG could not be rendered.",
401 )
402 parser.add_argument(
403 "--skip-marker",
404 action="store_true",
405 help="Don't insert ``# Flowchart:`` markers into source files.",
406 )
407 parser.add_argument(
408 "--strip-markers",
409 action="store_true",
410 help=(
411 "Remove every existing ``# <pyflowchart-code-diagram>`` "
412 "block from the source tree and exit. Useful when "
413 "refactoring the generator's placement rules or when "
414 "tearing down the feature entirely. Does not regenerate "
415 "flowcharts — combine with a normal run afterwards if "
416 "you want fresh markers."
417 ),
418 )
419 args = parser.parse_args()
420 if args.require_png and args.skip_png:
421 parser.error("--require-png cannot be combined with --skip-png")
423 project_root = Path(__file__).resolve().parent.parent.parent
424 output_dir = Path(__file__).resolve().parent
426 if args.strip_markers:
427 print("🧹 Stripping pyflowchart markers from source files")
428 print("=" * 50)
429 print(f" Project root : {project_root}")
430 modified = strip_all_markers(project_root)
431 print("\n" + "=" * 50)
432 print(f"✅ Stripped markers from {modified} file(s).")
433 return
435 selected = _filter_targets(TARGETS, args.target)
436 full_catalog = args.target is None
437 generated_at = generation_timestamp_utc()
438 source_commit = generation_source_commit()
440 # Incremental by default: re-render only the sources whose marker-stripped
441 # bytes differ from the recorded provenance (plus newly charted targets and
442 # any missing artifact). This is what keeps a PR's diagram diff
443 # proportional to the code it changed. ``--all`` and an explicit
444 # ``--target`` both bypass the staleness filter.
445 incremental = full_catalog and not args.force_all
446 targets = (
447 select_stale_targets(
448 project_root=project_root,
449 targets=selected,
450 output_dir=output_dir,
451 )
452 if incremental
453 else selected
454 )
456 print("🧭 GCO Code Flowchart Generator")
457 print("=" * 50)
458 print(f" Project root : {project_root}")
459 print(f" Output dir : {output_dir}")
460 print(f" Mode : {'incremental' if incremental else 'full'}")
461 print(f" Targets : {len(targets)} of {len(selected)} selected")
462 print(f" Generated at : {generated_at}")
463 print(f" Source commit: {source_commit}")
465 if not targets:
466 # Nothing changed. Leave every committed artifact, marker, README, and
467 # manifest byte untouched so a no-op regeneration is a no-op diff.
468 print("\n✅ Every charted source is already current; nothing to re-render.")
469 return
471 _verify_targets_match_source_commit(
472 project_root=project_root,
473 targets=targets,
474 source_commit=source_commit,
475 )
477 results = render_all(
478 targets=targets,
479 project_root=project_root,
480 output_dir=output_dir,
481 render_png=not args.skip_png,
482 generated_at=generated_at,
483 source_commit=source_commit,
484 )
485 if args.require_png:
486 missing_pngs = [
487 result.target.source + ":" + result.target.function
488 for result in results
489 if result.png_path is None
490 ]
491 if missing_pngs:
492 sys.exit(f"Canonical generation requires every PNG; missing: {missing_pngs}")
494 if not args.skip_marker:
495 # Refresh markers only in the sources we just re-rendered, so an
496 # incremental run leaves every other charted file byte-identical.
497 # Retired sources still get their stale markers pruned; the shared
498 # Lambda copies stay in the allowed set (see marker_allowed_sources).
499 prune_retired_markers(project_root, charted=marker_allowed_sources(TARGETS))
500 upsert_markers(results, project_root=project_root)
501 _sync_shared_lambda_copies(project_root)
503 if full_catalog:
504 prune_orphaned_artifacts(targets=TARGETS, output_dir=output_dir)
506 # Provenance and the index are refreshed on every run, including an
507 # explicit ``--target`` selection: a run that restamps a source's marker
508 # and artifacts without recording the new stamp would leave the repository
509 # contract failing. Entries for sources this run did not touch are
510 # preserved verbatim (manifest) or reconstructed from their recorded
511 # provenance (index), so a partial run stays a partial diff.
512 manifest_path = write_provenance_manifest(
513 project_root=project_root,
514 output_dir=output_dir,
515 regenerated_targets=targets,
516 generated_at=generated_at,
517 source_commit=source_commit,
518 catalog=TARGETS,
519 )
520 print(f"📝 Wrote {manifest_path}")
521 write_readme(
522 _catalog_readme_entries(
523 project_root=project_root,
524 output_dir=output_dir,
525 results=results,
526 ),
527 output_dir=output_dir,
528 )
530 print("\n" + "=" * 50)
531 print("✅ Code flowchart generation complete!")
532 print(f" Output directory: {output_dir.absolute()}")
535def marker_allowed_sources(targets: list[Target]) -> set[str]:
536 """Return every source permitted to carry a generated marker block.
538 That is the charted sources plus the checked-in copies of charted shared
539 Lambda sources — the copies are byte-identical to their canonical source
540 (a separate contract), so they carry its marker too. Omitting them makes
541 marker pruning strip the copies and desynchronise them, so this
542 computation is shared by the generator and the contract checker rather
543 than duplicated in both.
544 """
545 allowed = {target.source for target in targets}
546 for canonical, copies in LAMBDA_SHARED_SOURCE_TARGETS.items():
547 if canonical in allowed:
548 allowed.update(copies)
549 return allowed
552def prune_retired_markers(project_root: Path, *, charted: set[str]) -> int:
553 """Strip markers from files that are no longer charted targets.
555 A full ``strip_all_markers`` + reinsert pass would rewrite every charted
556 source on every run, which is exactly the catalogue-wide churn incremental
557 generation exists to avoid. Only genuinely retired sources are touched.
558 """
559 modified = 0
560 for source_path in sorted(project_root.rglob("*.py")):
561 relative = source_path.relative_to(project_root).as_posix()
562 if relative in charted or "-build" in relative:
563 continue
564 if not is_marker_source(relative):
565 continue
566 original = source_path.read_text(encoding="utf-8")
567 if SENTINEL not in original:
568 continue
569 stripped = strip_markers_from(original)
570 if stripped != original:
571 source_path.write_text(stripped, encoding="utf-8")
572 print(f" 🧹 stripped retired marker from {relative}")
573 modified += 1
574 return modified
577def _catalog_readme_entries(
578 *,
579 project_root: Path,
580 output_dir: Path,
581 results: list[RenderedTarget],
582) -> list[RenderedTarget]:
583 """Return one entry per catalogue target for the README index.
585 Freshly rendered targets contribute their real results; every other target
586 is reconstructed from its recorded provenance stamp and committed artifact
587 paths, so an incremental run still writes a complete index without
588 re-rendering (or restamping) the untouched majority.
589 """
590 rendered = {(result.target.source, result.target.function): result for result in results}
591 manifest = load_provenance_manifest(project_root)
592 entries: list[RenderedTarget] = []
593 for target in TARGETS:
594 existing = rendered.get((target.source, target.function))
595 if existing is not None:
596 entries.append(existing)
597 continue
598 entry = manifest[target.source]
599 stem = _output_stem_for(target, output_dir=output_dir)
600 png_path = stem.with_name(f"{stem.name}.png")
601 entries.append(
602 RenderedTarget(
603 target=target,
604 html_path=stem.with_name(f"{stem.name}.html"),
605 png_path=png_path if png_path.is_file() else None,
606 generated_at=entry["generated_at"],
607 source_commit=entry["source_commit"],
608 )
609 )
610 return entries
613def _sync_shared_lambda_copies(project_root: Path) -> None:
614 """Propagate refreshed canonical shared Lambda sources to their copies.
616 ``upsert_markers`` rewrites the pyflowchart header inside canonical
617 shared sources (``lambda/tls-shared/backend_tls.py``,
618 ``lambda/proxy-shared/proxy_utils.py``), but the checked-in per-function
619 copies are not diagram targets, so a regeneration used to leave them one
620 header behind. That drift is exactly what
621 ``tests/test_lambda_shared_sources.py`` rejects and what made every
622 deploy rewrite tracked files mid-run (``StackManager._sync_lambda_sources``
623 re-syncs at deploy time). Reuse the deploy path's own map so the two
624 sync points can never disagree about what a copy is.
625 """
626 synced = 0
627 for source_rel, target_rels in LAMBDA_SHARED_SOURCE_TARGETS.items():
628 source = project_root / source_rel
629 if not source.is_file():
630 continue
631 source_bytes = source.read_bytes()
632 for target_rel in target_rels:
633 target = project_root / target_rel
634 if not target.parent.is_dir():
635 continue
636 if target.is_file() and target.read_bytes() == source_bytes:
637 continue
638 target.write_bytes(source_bytes)
639 synced += 1
640 print(f" Synced shared copy: {target_rel} <- {source_rel}")
641 if synced:
642 print(f"🔁 Refreshed {synced} shared Lambda cop{'y' if synced == 1 else 'ies'}.")
645def _filter_targets(
646 all_targets: list[Target],
647 requested: list[str] | None,
648) -> list[Target]:
649 """Filter :data:`TARGETS` by optional ``PATH:FUNC`` arguments."""
650 if not requested:
651 return list(all_targets)
652 wanted = set(requested)
653 filtered = [t for t in all_targets if f"{t.source}:{t.function}" in wanted]
654 missing = wanted - {f"{t.source}:{t.function}" for t in filtered}
655 if missing:
656 sys.exit(f"Unknown target(s): {sorted(missing)}")
657 return filtered
660if __name__ == "__main__":
661 main()