codegraph-ir 0.1.0__py3-none-any.whl

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 (63) hide show
  1. cgir/__init__.py +6 -0
  2. cgir/analyses/__init__.py +7 -0
  3. cgir/analyses/call_graph.py +114 -0
  4. cgir/analyses/cfg.py +320 -0
  5. cgir/analyses/effects.py +95 -0
  6. cgir/analyses/entrypoints.py +55 -0
  7. cgir/analyses/param_flow.py +75 -0
  8. cgir/analyses/pdg.py +75 -0
  9. cgir/analyses/purity.py +37 -0
  10. cgir/analyses/reaching_defs.py +115 -0
  11. cgir/analyses/symbols.py +106 -0
  12. cgir/api/__init__.py +1 -0
  13. cgir/api/mcp_server.py +172 -0
  14. cgir/api/server.py +107 -0
  15. cgir/cli.py +688 -0
  16. cgir/config.py +22 -0
  17. cgir/export/__init__.py +5 -0
  18. cgir/export/graphml.py +52 -0
  19. cgir/export/html_viz.py +1087 -0
  20. cgir/export/json_export.py +37 -0
  21. cgir/export/mermaid.py +57 -0
  22. cgir/export/neo4j.py +11 -0
  23. cgir/hooks.py +189 -0
  24. cgir/ir/__init__.py +16 -0
  25. cgir/ir/component_spec.py +100 -0
  26. cgir/ir/edges.py +31 -0
  27. cgir/ir/graph.py +132 -0
  28. cgir/ir/nodes.py +38 -0
  29. cgir/languages/__init__.py +25 -0
  30. cgir/languages/base.py +221 -0
  31. cgir/languages/cache.py +73 -0
  32. cgir/languages/python.py +1145 -0
  33. cgir/languages/registry.py +24 -0
  34. cgir/languages/typescript.py +853 -0
  35. cgir/manifest.py +77 -0
  36. cgir/pipeline.py +54 -0
  37. cgir/py.typed +0 -0
  38. cgir/regenerate/__init__.py +6 -0
  39. cgir/regenerate/prompt_pack.py +16 -0
  40. cgir/regenerate/regenerator.py +108 -0
  41. cgir/report/__init__.py +5 -0
  42. cgir/report/diff.py +226 -0
  43. cgir/report/flow.py +84 -0
  44. cgir/report/impact.py +234 -0
  45. cgir/report/lint.py +84 -0
  46. cgir/report/pack.py +271 -0
  47. cgir/report/stats.py +121 -0
  48. cgir/slicing/__init__.py +5 -0
  49. cgir/slicing/slicer.py +174 -0
  50. cgir/sources/__init__.py +6 -0
  51. cgir/sources/base.py +14 -0
  52. cgir/sources/codeql_source.py +13 -0
  53. cgir/sources/joern_source.py +13 -0
  54. cgir/sources/tree_sitter_source.py +270 -0
  55. cgir/trace/__init__.py +5 -0
  56. cgir/trace/trace_map.py +83 -0
  57. cgir/verify.py +173 -0
  58. cgir/watch.py +171 -0
  59. codegraph_ir-0.1.0.dist-info/METADATA +143 -0
  60. codegraph_ir-0.1.0.dist-info/RECORD +63 -0
  61. codegraph_ir-0.1.0.dist-info/WHEEL +4 -0
  62. codegraph_ir-0.1.0.dist-info/entry_points.txt +2 -0
  63. codegraph_ir-0.1.0.dist-info/licenses/LICENSE +21 -0
cgir/report/impact.py ADDED
@@ -0,0 +1,234 @@
1
+ """Change-impact / blast-radius analysis — the forward-looking companion
2
+ to :mod:`cgir.report.diff`.
3
+
4
+ ``diff`` is retrospective ("what changed?"). ``impact`` is predictive:
5
+ *before* you (or an agent) touch a component, what does changing it put at
6
+ risk? The answer is the transitive **upstream** (caller) closure — every
7
+ component whose behaviour depends on this one — plus the surface that
8
+ closure reaches: which **entrypoints** sit above it, and which **tests** to
9
+ run (union of ``covered_by`` over the target and everything affected).
10
+
11
+ ``compute_impact`` is the worst case: it assumes the change could affect
12
+ anything upstream. ``compute_typed_impact`` narrows that by *what actually
13
+ changed about the contract*, because not every change propagates the same
14
+ way:
15
+
16
+ * **body-only** (no contract field changed) — callers are contract-safe;
17
+ reach ``none``, only the target's own tests matter.
18
+ * **signature / outputs** — an interface break the *direct* call sites must
19
+ adapt to, but which does not inherently ripple past them; reach
20
+ ``direct``.
21
+ * **effects / purity / kind** — semantic taint that flows *up* the call
22
+ graph (a caller of something newly effectful is itself newly effectful);
23
+ reach ``transitive`` — the full closure.
24
+
25
+ The reach model is a deliberate, documented heuristic, not a proof: it
26
+ mirrors how the effect/purity analyses actually propagate. Pure over
27
+ ComponentSpecs, so it drives both the CLI (``cgir impact``) and the MCP
28
+ tool an agent calls before — and after — an edit.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from collections.abc import Iterable
34
+ from typing import Any
35
+
36
+ from cgir.ir.component_spec import ComponentSpec
37
+
38
+ # Contract fields whose change taints callers transitively vs. only directly.
39
+ _TRANSITIVE_FIELDS = frozenset({"effects", "purity", "kind"})
40
+ _DIRECT_FIELDS = frozenset({"signature", "outputs", "inputs"})
41
+
42
+
43
+ def _callers_map(
44
+ specs: list[ComponentSpec], by_id: dict[str, ComponentSpec]
45
+ ) -> dict[str, set[str]]:
46
+ """Reverse the CALLS relation: callee id -> ids that call it."""
47
+ callers: dict[str, set[str]] = {}
48
+ for spec in specs:
49
+ for callee in spec.calls:
50
+ if callee in by_id:
51
+ callers.setdefault(callee, set()).add(spec.id)
52
+ return callers
53
+
54
+
55
+ def _upstream_closure(callers: dict[str, set[str]], target_id: str) -> set[str]:
56
+ """Transitive caller closure of ``target_id`` (cycle-safe, target excluded)."""
57
+ affected: set[str] = set()
58
+ queue = [target_id]
59
+ while queue:
60
+ node = queue.pop()
61
+ for caller in callers.get(node, ()):
62
+ if caller != target_id and caller not in affected:
63
+ affected.add(caller)
64
+ queue.append(caller)
65
+ return affected
66
+
67
+
68
+ def _is_test_spec(spec: ComponentSpec) -> bool:
69
+ """Mirror of ``slicer._is_test_node`` at the spec level."""
70
+ if spec.id.rsplit(".", 1)[-1].startswith("test_"):
71
+ return True
72
+ if spec.trace:
73
+ path = spec.trace[0].rsplit(":", 1)[0].replace("\\", "/")
74
+ parts = path.split("/")
75
+ stem = parts[-1].rsplit(".", 1)[0]
76
+ return "tests" in parts[:-1] or stem.startswith("test_") or stem.endswith("_test")
77
+ return False
78
+
79
+
80
+ def _surface(
81
+ by_id: dict[str, ComponentSpec], target_id: str, affected: set[str]
82
+ ) -> tuple[list[str], list[dict[str, Any]], list[str]]:
83
+ """Partition + derive: (affected code, entrypoints at risk, tests to run).
84
+
85
+ Test components in the caller closure are not "affected code" — they are
86
+ literally tests to run, so they move into the tests list (this also
87
+ catches transitive test callers never linked via ``covered_by``).
88
+ """
89
+ code = {sid for sid in affected if not _is_test_spec(by_id[sid])}
90
+ scope = sorted(code | {target_id})
91
+ entrypoints = [
92
+ {"id": sid, "entrypoint": by_id[sid].entrypoint} for sid in scope if by_id[sid].entrypoint
93
+ ]
94
+ tests: set[str] = set(by_id[target_id].covered_by)
95
+ tests |= affected - code
96
+ for sid in code:
97
+ tests |= set(by_id[sid].covered_by)
98
+ return sorted(code), entrypoints, sorted(tests)
99
+
100
+
101
+ def compute_impact(specs: list[ComponentSpec], target_id: str) -> dict[str, Any]:
102
+ """Worst-case blast radius of changing ``target_id`` — pure, JSON-able."""
103
+ by_id = {s.id: s for s in specs}
104
+ if target_id not in by_id:
105
+ raise KeyError(target_id)
106
+ callers = _callers_map(specs, by_id)
107
+ affected = _upstream_closure(callers, target_id)
108
+ code, entrypoints, tests = _surface(by_id, target_id, affected)
109
+ return {
110
+ "target": target_id,
111
+ "direct_callers": sorted(callers.get(target_id, set())),
112
+ "affected": code,
113
+ "entrypoints": entrypoints,
114
+ "tests": tests,
115
+ }
116
+
117
+
118
+ def _reach(changed_fields: set[str]) -> str:
119
+ if changed_fields & _TRANSITIVE_FIELDS:
120
+ return "transitive"
121
+ if changed_fields & _DIRECT_FIELDS:
122
+ return "direct"
123
+ return "none"
124
+
125
+
126
+ def compute_typed_impact(
127
+ specs: list[ComponentSpec], target_id: str, changed_fields: Iterable[str]
128
+ ) -> dict[str, Any]:
129
+ """Blast radius narrowed by *which* contract fields changed.
130
+
131
+ ``changed_fields`` is the set of drifted contract fields — e.g. the keys
132
+ of a :func:`cgir.report.diff.compute_diff` change entry or a
133
+ ``VerifyResult.drift``. An empty set means a body-only edit.
134
+ """
135
+ by_id = {s.id: s for s in specs}
136
+ if target_id not in by_id:
137
+ raise KeyError(target_id)
138
+ delta = set(changed_fields)
139
+ reach = _reach(delta)
140
+ callers = _callers_map(specs, by_id)
141
+ direct = sorted(callers.get(target_id, set()))
142
+
143
+ if reach == "transitive":
144
+ affected = _upstream_closure(callers, target_id)
145
+ elif reach == "direct":
146
+ affected = set(direct)
147
+ else:
148
+ affected = set()
149
+
150
+ code, entrypoints, tests = _surface(by_id, target_id, affected)
151
+ return {
152
+ "target": target_id,
153
+ "changed_fields": sorted(delta),
154
+ "reach": reach,
155
+ "direct_callers": direct,
156
+ "affected": code,
157
+ "entrypoints": entrypoints,
158
+ "tests": tests,
159
+ }
160
+
161
+
162
+ _REACH_NOTE = {
163
+ "none": "body-only change — no contract drift, callers are unaffected",
164
+ "direct": "interface change — direct call sites must adapt",
165
+ "transitive": "effect/purity change — taint flows up the call graph",
166
+ }
167
+
168
+
169
+ def _render(data: dict[str, Any]) -> str:
170
+ target = data["target"]
171
+ affected = data["affected"]
172
+ entrypoints = data["entrypoints"]
173
+ tests = data["tests"]
174
+ reach = data.get("reach")
175
+
176
+ lines: list[str] = [f"# impact of changing {target}", ""]
177
+ if reach is not None:
178
+ fields = ", ".join(data["changed_fields"]) or "(none)"
179
+ lines.append(f"contract delta: {fields} → reach: {reach}")
180
+ lines.append(f" {_REACH_NOTE[reach]}")
181
+ lines.append("")
182
+ lines.append(
183
+ f"{len(affected)} component(s) affected · "
184
+ f"{len(entrypoints)} entrypoint(s) at risk · "
185
+ f"{len(tests)} test(s) to run"
186
+ )
187
+
188
+ lines.append("")
189
+ lines.append(f"affected components ({len(affected)}):")
190
+ if affected:
191
+ direct = set(data["direct_callers"])
192
+ for sid in affected:
193
+ lines.append(f" {'← direct' if sid in direct else ' ⋯ '} {sid}")
194
+ elif reach == "none" and data["direct_callers"]:
195
+ lines.append(f" (none contract-affected; {len(data['direct_callers'])} call it)")
196
+ else:
197
+ lines.append(" (none — nothing calls this)")
198
+
199
+ lines.append("")
200
+ lines.append(f"entrypoints at risk ({len(entrypoints)}):")
201
+ if entrypoints:
202
+ for e in entrypoints:
203
+ lines.append(f" ! {e['entrypoint']} ({e['id']})")
204
+ else:
205
+ lines.append(" (none reachable)")
206
+
207
+ lines.append("")
208
+ lines.append(f"tests to run ({len(tests)}):")
209
+ if tests:
210
+ lines.extend(f" • {t}" for t in tests)
211
+ else:
212
+ lines.append(" (no linked tests — this change is unguarded)")
213
+
214
+ return "\n".join(lines) + "\n"
215
+
216
+
217
+ def render_impact(specs: list[ComponentSpec], target_id: str) -> str:
218
+ """Human summary of the worst-case blast radius."""
219
+ return _render(compute_impact(specs, target_id))
220
+
221
+
222
+ def render_typed_impact(
223
+ specs: list[ComponentSpec], target_id: str, changed_fields: Iterable[str]
224
+ ) -> str:
225
+ """Human summary of the blast radius narrowed by the contract delta."""
226
+ return _render(compute_typed_impact(specs, target_id, changed_fields))
227
+
228
+
229
+ __all__ = [
230
+ "compute_impact",
231
+ "compute_typed_impact",
232
+ "render_impact",
233
+ "render_typed_impact",
234
+ ]
cgir/report/lint.py ADDED
@@ -0,0 +1,84 @@
1
+ """cgir lint — semantic architecture rules over the ComponentSpec index.
2
+
3
+ Import linters (Tach, import-linter) constrain *imports*. CGIR constrains
4
+ *meaning*: which components may carry which effects, what kind they must be,
5
+ and what they may call — checks an import graph can't express because it
6
+ doesn't know a function touches the network or routes to the DB layer.
7
+
8
+ A rule is scoped by an ``in`` id-glob and carries one predicate:
9
+
10
+ * ``forbid-effect``: matched components must not carry these effect tags
11
+ * ``require-kind``: matched components must be this component kind
12
+ * ``forbid-call``: matched components must not call components whose id
13
+ matches this glob (a semantic layer boundary over resolved CALLS)
14
+
15
+ Rules live in ``cgir.toml`` as a ``[[rule]]`` array; the checker itself is
16
+ pure over specs so it is trivially testable and CI-friendly.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import tomllib
22
+ from dataclasses import dataclass
23
+ from fnmatch import fnmatch
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from cgir.ir.component_spec import ComponentSpec
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class LintViolation:
32
+ rule: str
33
+ component: str
34
+ detail: str
35
+
36
+
37
+ def load_rules(config_path: Path) -> list[dict[str, Any]]:
38
+ """Read ``[[rule]]`` entries from a ``cgir.toml``-style config."""
39
+ data = tomllib.loads(config_path.read_text())
40
+ rules = data.get("rule", [])
41
+ return list(rules) if isinstance(rules, list) else []
42
+
43
+
44
+ def lint(specs: list[ComponentSpec], rules: list[dict[str, Any]]) -> list[LintViolation]:
45
+ violations: list[LintViolation] = []
46
+ known = {s.id for s in specs}
47
+ for rule in rules:
48
+ name = str(rule.get("name", "rule"))
49
+ scope = str(rule.get("in", "*"))
50
+ matched = [s for s in specs if fnmatch(s.id, scope)]
51
+
52
+ if "forbid-effect" in rule:
53
+ forbidden = set(rule["forbid-effect"])
54
+ for spec in matched:
55
+ hit = sorted(forbidden & set(spec.effects))
56
+ if hit:
57
+ violations.append(
58
+ LintViolation(name, spec.id, f"has forbidden effect(s): {', '.join(hit)}")
59
+ )
60
+ if "require-kind" in rule:
61
+ want = str(rule["require-kind"])
62
+ for spec in matched:
63
+ if spec.kind.value != want:
64
+ violations.append(
65
+ LintViolation(name, spec.id, f"is {spec.kind.value}, must be {want}")
66
+ )
67
+ if "forbid-call" in rule:
68
+ target_glob = str(rule["forbid-call"])
69
+ for spec in matched:
70
+ for callee in spec.calls:
71
+ if callee in known and fnmatch(callee, target_glob):
72
+ violations.append(
73
+ LintViolation(name, spec.id, f"calls forbidden target: {callee}")
74
+ )
75
+ return violations
76
+
77
+
78
+ def render_lint(violations: list[LintViolation]) -> str:
79
+ if not violations:
80
+ return "no architecture-rule violations\n"
81
+ lines = [f"{len(violations)} architecture-rule violation(s):"]
82
+ for v in violations:
83
+ lines.append(f" ! [{v.rule}] {v.component} {v.detail}")
84
+ return "\n".join(lines) + "\n"
cgir/report/pack.py ADDED
@@ -0,0 +1,271 @@
1
+ """Context packer — the minimal bundle for working on one component.
2
+
3
+ This is the product's core loop (Code-IR.md: rewrite/audit "without
4
+ holding the whole repo in context"): given a target component, assemble
5
+ what an LLM needs and nothing else, in priority order:
6
+
7
+ 1. the target — spec fields + source text when available
8
+ 2. its callees, as *interfaces only* (signature, kind, effects, returns)
9
+ 3. its callers — how it's used, with entrypoints ("how the outside
10
+ world reaches this")
11
+ 4. the types it constructs
12
+
13
+ ``budget`` is an approximate token budget (chars / 4). Lower-priority
14
+ sections are dropped whole to fit, and every drop is recorded under
15
+ ``omitted`` — the bundle never silently lies about completeness.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from typing import Any
22
+
23
+ from cgir.ir.component_spec import ComponentSpec
24
+
25
+ DEFAULT_BUDGET = 4000
26
+
27
+ # Dropped in this order to fit budget: types, tests & the target contract
28
+ # are the last to go (they're what makes a context-free rewrite possible).
29
+ _SECTION_PRIORITY = ("constructs", "callers", "callees", "context", "tests", "types")
30
+
31
+ _BUILTIN_TYPES = frozenset(
32
+ {
33
+ "int",
34
+ "float",
35
+ "str",
36
+ "bool",
37
+ "bytes",
38
+ "None",
39
+ "Any",
40
+ "list",
41
+ "dict",
42
+ "set",
43
+ "tuple",
44
+ "frozenset",
45
+ "object",
46
+ "Optional",
47
+ "Union",
48
+ "Iterable",
49
+ "Iterator",
50
+ "Sequence",
51
+ "Mapping",
52
+ "Callable",
53
+ "Type",
54
+ "Awaitable",
55
+ "Coroutine",
56
+ "AsyncIterator",
57
+ }
58
+ )
59
+ _TYPE_TOKEN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
60
+
61
+
62
+ def referenced_type_names(spec: ComponentSpec) -> set[str]:
63
+ """Non-builtin type identifiers the component's contract names.
64
+
65
+ Pulled from the return type, constructed classes, and parameter
66
+ annotations — the types an implementer must know the shape of.
67
+ """
68
+ names: set[str] = set()
69
+ for output in spec.outputs:
70
+ names |= _type_tokens(output)
71
+ for construct in spec.constructs:
72
+ names.add(construct.rsplit(".", 1)[-1])
73
+ for _, annotation in _param_items(spec.signature):
74
+ if annotation:
75
+ names |= _type_tokens(annotation)
76
+ return {n for n in names if n not in _BUILTIN_TYPES}
77
+
78
+
79
+ def _type_tokens(annotation: str) -> set[str]:
80
+ return set(_TYPE_TOKEN.findall(annotation))
81
+
82
+
83
+ def _param_items(signature: str | None) -> list[tuple[str, str | None]]:
84
+ if not signature or "(" not in signature or ")" not in signature:
85
+ return []
86
+ inner = signature[signature.index("(") + 1 : signature.rindex(")")]
87
+ items: list[tuple[str, str | None]] = []
88
+ depth, current = 0, ""
89
+ for ch in inner + ",":
90
+ if ch in "([{":
91
+ depth += 1
92
+ elif ch in ")]}":
93
+ depth -= 1
94
+ if ch == "," and depth == 0:
95
+ name, _, annotation = current.partition(":")
96
+ annotation = annotation.split("=", 1)[0].strip()
97
+ items.append((name.strip().lstrip("*"), annotation or None))
98
+ current = ""
99
+ else:
100
+ current += ch
101
+ return items
102
+
103
+
104
+ def build_pack(
105
+ specs: list[ComponentSpec],
106
+ target_id: str,
107
+ source: str | None = None,
108
+ budget: int = DEFAULT_BUDGET,
109
+ types: dict[str, str] | None = None,
110
+ tests: dict[str, str] | None = None,
111
+ context: dict[str, str] | None = None,
112
+ receivers: dict[str, str] | None = None,
113
+ ) -> dict[str, Any]:
114
+ by_id = {s.id: s for s in specs}
115
+ target = by_id[target_id] # KeyError for unknown targets, by design
116
+
117
+ callees = []
118
+ for callee in target.calls:
119
+ if callee not in by_id:
120
+ continue
121
+ entry = _interface(by_id[callee])
122
+ # A DI callee is reached through an injected field; naming it lets a
123
+ # rewriter reproduce the call (and thus the effect contract) instead
124
+ # of guessing the field name.
125
+ if receivers and callee in receivers:
126
+ entry["receiver"] = receivers[callee]
127
+ callees.append(entry)
128
+ callers = [_interface(s) for s in specs if target_id in s.calls]
129
+ type_defs = [{"name": name, "source": src} for name, src in sorted((types or {}).items())]
130
+ test_defs = [{"id": tid, "source": src} for tid, src in sorted((tests or {}).items())]
131
+ context_defs = [{"name": n, "source": src} for n, src in sorted((context or {}).items())]
132
+ pack: dict[str, Any] = {
133
+ "target": {
134
+ "id": target.id,
135
+ "kind": target.kind.value,
136
+ "signature": target.signature,
137
+ "inputs": target.inputs,
138
+ "outputs": target.outputs,
139
+ "effects": target.effects,
140
+ "purity": target.purity,
141
+ "entrypoint": target.entrypoint,
142
+ "doc": target.doc,
143
+ "raises": target.raises,
144
+ "trace": target.trace,
145
+ "source": source,
146
+ },
147
+ "types": type_defs,
148
+ "context": context_defs,
149
+ "tests": test_defs,
150
+ "callees": callees,
151
+ "callers": callers,
152
+ "constructs": list(target.constructs),
153
+ "omitted": [],
154
+ }
155
+
156
+ for section in _SECTION_PRIORITY:
157
+ if _estimate_tokens(pack) <= budget:
158
+ break
159
+ if pack[section]:
160
+ pack[section] = []
161
+ pack["omitted"].append(section)
162
+ return pack
163
+
164
+
165
+ def _interface(spec: ComponentSpec) -> dict[str, Any]:
166
+ return {
167
+ "id": spec.id,
168
+ "kind": spec.kind.value,
169
+ "signature": spec.signature,
170
+ "outputs": spec.outputs,
171
+ "effects": spec.effects,
172
+ "entrypoint": spec.entrypoint,
173
+ }
174
+
175
+
176
+ def _estimate_tokens(pack: dict[str, Any]) -> int:
177
+ return len(str(pack)) // 4
178
+
179
+
180
+ def render_pack(pack: dict[str, Any]) -> str:
181
+ target = pack["target"]
182
+ lines: list[str] = [f"# {target['id']}", ""]
183
+ lines.append(
184
+ f"`{target['signature']}` — {target['kind']}"
185
+ + (f", purity {target['purity']}" if target["purity"] is not None else "")
186
+ )
187
+ if target["entrypoint"]:
188
+ lines.append(f"Entrypoint: **{target['entrypoint']}**")
189
+ if target["effects"]:
190
+ lines.append(f"Effects: {', '.join(target['effects'])}")
191
+ if target["outputs"]:
192
+ lines.append(f"Returns: {', '.join(target['outputs'])}")
193
+ if target.get("raises"):
194
+ lines.append(f"Raises: {', '.join(target['raises'])}")
195
+ if target["trace"]:
196
+ lines.append(f"Source location: {target['trace'][0]}")
197
+ if target.get("doc"):
198
+ lines.append("")
199
+ lines.append(f"> {target['doc'].strip().splitlines()[0]}")
200
+ if target["source"]:
201
+ lines.append("")
202
+ lines.append("```python")
203
+ lines.append(target["source"].rstrip("\n"))
204
+ lines.append("```")
205
+
206
+ if pack.get("types"):
207
+ lines.append("")
208
+ lines.append("## Types")
209
+ lines.append("Shapes the target's contract references — match these exactly.")
210
+ for tdef in pack["types"]:
211
+ lines.append("")
212
+ lines.append("```python")
213
+ lines.append(tdef["source"].rstrip("\n"))
214
+ lines.append("```")
215
+
216
+ if pack.get("context"):
217
+ lines.append("")
218
+ lines.append("## Referenced module definitions")
219
+ lines.append("Constants and helpers the body uses — use them, don't redefine.")
220
+ for cdef in pack["context"]:
221
+ lines.append("")
222
+ lines.append("```python")
223
+ lines.append(cdef["source"].rstrip("\n"))
224
+ lines.append("```")
225
+
226
+ if pack.get("tests"):
227
+ lines.append("")
228
+ lines.append("## Tests (behavior contract)")
229
+ lines.append("The implementation must satisfy these.")
230
+ for tdef in pack["tests"]:
231
+ lines.append("")
232
+ lines.append(f"```python # {tdef['id']}")
233
+ lines.append(tdef["source"].rstrip("\n"))
234
+ lines.append("```")
235
+
236
+ if pack["callees"]:
237
+ lines.append("")
238
+ lines.append("## Callees (interfaces)")
239
+ lines.append("Do not modify these; call them as specified.")
240
+ lines.append("")
241
+ for callee in pack["callees"]:
242
+ lines.append(_interface_line(callee))
243
+ if pack["callers"]:
244
+ lines.append("")
245
+ lines.append("## Callers")
246
+ lines.append("These depend on the target's current contract.")
247
+ lines.append("")
248
+ for caller in pack["callers"]:
249
+ lines.append(_interface_line(caller))
250
+ if pack["constructs"]:
251
+ lines.append("")
252
+ lines.append("## Constructs")
253
+ lines.extend(f"- {type_name}" for type_name in pack["constructs"])
254
+ if pack["omitted"]:
255
+ lines.append("")
256
+ lines.append(f"_Omitted for budget: {', '.join(pack['omitted'])}_")
257
+ return "\n".join(lines) + "\n"
258
+
259
+
260
+ def _interface_line(entry: dict[str, Any]) -> str:
261
+ receiver = entry.get("receiver")
262
+ call = f"{receiver}.{entry['signature']}" if receiver else entry["signature"]
263
+ parts = [f"- `{call}`", f"({entry['id']}, {entry['kind']}"]
264
+ if entry["effects"]:
265
+ parts[-1] += f", effects: {','.join(entry['effects'])}"
266
+ parts[-1] += ")"
267
+ if entry["outputs"]:
268
+ parts.append(f"-> {entry['outputs'][0]}")
269
+ if entry["entrypoint"]:
270
+ parts.append(f"[{entry['entrypoint']}]")
271
+ return " ".join(parts)