atomadic-forge 0.3.2__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 (131) hide show
  1. atomadic_forge/__init__.py +12 -0
  2. atomadic_forge/__main__.py +5 -0
  3. atomadic_forge/a0_qk_constants/__init__.py +1 -0
  4. atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
  5. atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
  6. atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
  7. atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
  8. atomadic_forge/a0_qk_constants/error_codes.py +296 -0
  9. atomadic_forge/a0_qk_constants/forge_types.py +89 -0
  10. atomadic_forge/a0_qk_constants/gen_language.py +116 -0
  11. atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
  12. atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
  13. atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
  14. atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
  15. atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
  16. atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
  17. atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
  18. atomadic_forge/a0_qk_constants/tier_names.py +47 -0
  19. atomadic_forge/a1_at_functions/__init__.py +1 -0
  20. atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
  21. atomadic_forge/a1_at_functions/agent_memory.py +139 -0
  22. atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
  23. atomadic_forge/a1_at_functions/agent_summary.py +277 -0
  24. atomadic_forge/a1_at_functions/body_extractor.py +306 -0
  25. atomadic_forge/a1_at_functions/card_renderer.py +210 -0
  26. atomadic_forge/a1_at_functions/certify_checks.py +445 -0
  27. atomadic_forge/a1_at_functions/chat_context.py +170 -0
  28. atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
  29. atomadic_forge/a1_at_functions/classify_tier.py +115 -0
  30. atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
  31. atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
  32. atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
  33. atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
  34. atomadic_forge/a1_at_functions/config_io.py +68 -0
  35. atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
  36. atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
  37. atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
  38. atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
  39. atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
  40. atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
  41. atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
  42. atomadic_forge/a1_at_functions/error_hints.py +105 -0
  43. atomadic_forge/a1_at_functions/evolution_log.py +94 -0
  44. atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
  45. atomadic_forge/a1_at_functions/generation_quality.py +322 -0
  46. atomadic_forge/a1_at_functions/import_repair.py +211 -0
  47. atomadic_forge/a1_at_functions/import_smoke.py +102 -0
  48. atomadic_forge/a1_at_functions/js_parser.py +539 -0
  49. atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
  50. atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
  51. atomadic_forge/a1_at_functions/llm_client.py +554 -0
  52. atomadic_forge/a1_at_functions/local_signer.py +134 -0
  53. atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
  54. atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
  55. atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
  56. atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
  57. atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
  58. atomadic_forge/a1_at_functions/policy_loader.py +107 -0
  59. atomadic_forge/a1_at_functions/preflight_change.py +227 -0
  60. atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
  61. atomadic_forge/a1_at_functions/provider_detect.py +157 -0
  62. atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
  63. atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
  64. atomadic_forge/a1_at_functions/recipes.py +186 -0
  65. atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
  66. atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
  67. atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
  68. atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
  69. atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
  70. atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
  71. atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
  72. atomadic_forge/a1_at_functions/scout_walk.py +309 -0
  73. atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
  74. atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
  75. atomadic_forge/a1_at_functions/stub_detector.py +158 -0
  76. atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
  77. atomadic_forge/a1_at_functions/synergy_render.py +252 -0
  78. atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
  79. atomadic_forge/a1_at_functions/test_runner.py +196 -0
  80. atomadic_forge/a1_at_functions/test_selector.py +122 -0
  81. atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
  82. atomadic_forge/a1_at_functions/tool_composer.py +130 -0
  83. atomadic_forge/a1_at_functions/transcript_log.py +70 -0
  84. atomadic_forge/a1_at_functions/wire_check.py +260 -0
  85. atomadic_forge/a2_mo_composites/__init__.py +1 -0
  86. atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
  87. atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
  88. atomadic_forge/a2_mo_composites/plan_store.py +164 -0
  89. atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
  90. atomadic_forge/a3_og_features/__init__.py +1 -0
  91. atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
  92. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
  93. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
  94. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
  95. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
  96. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
  97. atomadic_forge/a3_og_features/demo_runner.py +502 -0
  98. atomadic_forge/a3_og_features/emergent_feature.py +95 -0
  99. atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
  100. atomadic_forge/a3_og_features/forge_enforce.py +107 -0
  101. atomadic_forge/a3_og_features/forge_evolve.py +176 -0
  102. atomadic_forge/a3_og_features/forge_loop.py +528 -0
  103. atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
  104. atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
  105. atomadic_forge/a3_og_features/lsp_server.py +98 -0
  106. atomadic_forge/a3_og_features/mcp_server.py +160 -0
  107. atomadic_forge/a3_og_features/setup_wizard.py +337 -0
  108. atomadic_forge/a3_og_features/synergy_feature.py +65 -0
  109. atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
  110. atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
  111. atomadic_forge/commands/__init__.py +1 -0
  112. atomadic_forge/commands/_registry.py +36 -0
  113. atomadic_forge/commands/audit.py +142 -0
  114. atomadic_forge/commands/chat.py +133 -0
  115. atomadic_forge/commands/commandsmith.py +178 -0
  116. atomadic_forge/commands/config_cmd.py +145 -0
  117. atomadic_forge/commands/demo.py +142 -0
  118. atomadic_forge/commands/emergent.py +124 -0
  119. atomadic_forge/commands/emergent_then_synergy.py +70 -0
  120. atomadic_forge/commands/evolve.py +122 -0
  121. atomadic_forge/commands/evolve_then_iterate.py +70 -0
  122. atomadic_forge/commands/feature_then_emergent.py +111 -0
  123. atomadic_forge/commands/iterate.py +140 -0
  124. atomadic_forge/commands/synergy.py +96 -0
  125. atomadic_forge/commands/synergy_then_emergent.py +70 -0
  126. atomadic_forge-0.3.2.dist-info/METADATA +471 -0
  127. atomadic_forge-0.3.2.dist-info/RECORD +131 -0
  128. atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
  129. atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
  130. atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
  131. atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,324 @@
1
+ """Tier a1 — pure agent_plan/v1 emitter.
2
+
3
+ Consumes one or more Forge reports and emits an ordered
4
+ ``AgentPlan``. Codex's prescription:
5
+
6
+ > 'observe → propose cards → agent chooses/edits → apply bounded
7
+ > change → certify → next card'
8
+
9
+ This module owns the 'propose cards' step. The agent owns 'choose
10
+ / edit / apply'. Forge provides the bounded-change verbs (auto,
11
+ enforce, certify, etc.) the cards point at.
12
+
13
+ Ranking: same deterministic order as ``agent_summary``:
14
+ 1. operational + applyable + low risk (cheapest wins first)
15
+ 2. architectural + auto_fixable (forge enforce path)
16
+ 3. release-blocking certify axes
17
+ 4. high-confidence synthesis (synergy adapters)
18
+ 5. composition opportunities (emergent chains)
19
+ 6. medium-risk architectural (review_manually)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import datetime as _dt
24
+
25
+ from ..a0_qk_constants.agent_plan_schema import (
26
+ REQUIRED_PLAN_FIELDS,
27
+ SCHEMA_VERSION_AGENT_ACTION_V1,
28
+ SCHEMA_VERSION_AGENT_PLAN_V1,
29
+ AgentActionCard,
30
+ AgentPlan,
31
+ )
32
+ from ..a0_qk_constants.error_codes import (
33
+ fcode_for_certify_axis,
34
+ fcode_for_tier_violation,
35
+ get_fcode,
36
+ )
37
+
38
+ _KIND_RANK = {
39
+ "operational": 0,
40
+ "architectural": 1,
41
+ "release": 2,
42
+ "synthesis": 3,
43
+ "composition": 4,
44
+ }
45
+ _RISK_RANK = {"low": 0, "medium": 1, "high": 2}
46
+
47
+
48
+ def _now_utc_iso() -> str:
49
+ return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
50
+
51
+
52
+ def _slug(*parts: str) -> str:
53
+ """Build a stable card id from its parts. Lowercased; non-alnum
54
+ collapsed to '-'."""
55
+ raw = "-".join(p for p in parts if p)
56
+ out: list[str] = []
57
+ prev_dash = False
58
+ for ch in raw.lower():
59
+ if ch.isalnum() or ch == ".":
60
+ out.append(ch)
61
+ prev_dash = False
62
+ elif not prev_dash:
63
+ out.append("-")
64
+ prev_dash = True
65
+ return "".join(out).strip("-")[:80] or "action"
66
+
67
+
68
+ def _operational_cards(certify_report: dict | None,
69
+ *, package: str | None) -> list[AgentActionCard]:
70
+ cards: list[AgentActionCard] = []
71
+ if not certify_report:
72
+ return cards
73
+ pkg_label = package or "your_pkg"
74
+
75
+ if not certify_report.get("documentation_complete", True):
76
+ cards.append(AgentActionCard(
77
+ schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
78
+ id=_slug("docs", pkg_label),
79
+ kind="operational",
80
+ title="Add a README so the documentation axis passes",
81
+ why="forge certify documentation_complete=False blocks any "
82
+ "score above 75/100; a one-paragraph README is worth +25.",
83
+ risk="low",
84
+ # forge plan-apply has a bounded F0050 handler that writes
85
+ # a stub README; the agent should replace it with real
86
+ # content before shipping.
87
+ applyable=True,
88
+ write_scope=["README.md"],
89
+ commands=[f"forge certify . --package {pkg_label}"],
90
+ related_fcodes=[fcode_for_certify_axis("documentation_complete")],
91
+ next_command=f"echo '# {pkg_label}' > README.md",
92
+ sample_path=None,
93
+ score_delta_estimate=25,
94
+ ))
95
+ if not certify_report.get("tests_present", True):
96
+ cards.append(AgentActionCard(
97
+ schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
98
+ id=_slug("tests", pkg_label),
99
+ kind="operational",
100
+ title="Add a tests/ directory so the tests axis passes",
101
+ why="forge certify tests_present=False; even one smoke test "
102
+ "(import-only) clears the axis and unlocks +25.",
103
+ risk="low",
104
+ applyable=False,
105
+ write_scope=["tests/test_smoke.py", "tests/__init__.py"],
106
+ commands=[f"forge certify . --package {pkg_label}",
107
+ "python -m pytest"],
108
+ related_fcodes=[fcode_for_certify_axis("tests_present")],
109
+ next_command=(
110
+ f"mkdir -p tests && printf 'import {pkg_label}\\n' > "
111
+ "tests/test_smoke.py"
112
+ ),
113
+ sample_path=None,
114
+ score_delta_estimate=25,
115
+ ))
116
+ return cards
117
+
118
+
119
+ def _architectural_cards(wire_report: dict | None,
120
+ *, package_root: str | None) -> list[AgentActionCard]:
121
+ cards: list[AgentActionCard] = []
122
+ if not wire_report:
123
+ return cards
124
+ violations = wire_report.get("violations") or []
125
+ if not violations:
126
+ return cards
127
+ by_fcode_file: dict[tuple[str, str], list[dict]] = {}
128
+ for v in violations:
129
+ code = v.get("f_code") or fcode_for_tier_violation(
130
+ v.get("from_tier", ""), v.get("to_tier", ""))
131
+ key = (code, v.get("file", ""))
132
+ by_fcode_file.setdefault(key, []).append(v)
133
+ for (code, file_path), group in by_fcode_file.items():
134
+ entry = get_fcode(code)
135
+ applyable = bool(entry and entry.get("auto_fixable"))
136
+ if applyable:
137
+ risk = "low"
138
+ cmd = (f"forge enforce {package_root or 'src/your_pkg'} "
139
+ "--apply # rolls back if violations rise")
140
+ why = (
141
+ f"{file_path} carries {len(group)} {code} violation(s). "
142
+ "auto_fixable=True; forge enforce can move the file up "
143
+ "to the higher tier and atomically roll back if the "
144
+ "move increases violations."
145
+ )
146
+ else:
147
+ risk = "medium"
148
+ cmd = (f"# review {file_path}: invert the import direction "
149
+ "or extract the symbol down to a lower tier.")
150
+ why = (
151
+ f"{file_path} carries {len(group)} {code} violation(s). "
152
+ "Mechanical fix is unsafe (a0 special-case or "
153
+ "non-canonical tier shape); requires agent judgement."
154
+ )
155
+ cards.append(AgentActionCard(
156
+ schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
157
+ id=_slug("fix", code, file_path),
158
+ kind="architectural",
159
+ title=(entry["title"] if entry else f"{code} on {file_path}"),
160
+ why=why,
161
+ risk=risk,
162
+ applyable=applyable,
163
+ write_scope=[file_path],
164
+ commands=[
165
+ f"forge wire {package_root or 'src/your_pkg'} "
166
+ "--suggest-repairs",
167
+ "python -m pytest",
168
+ ],
169
+ related_fcodes=[code],
170
+ next_command=cmd,
171
+ sample_path=file_path,
172
+ score_delta_estimate=10 if applyable else 5,
173
+ ))
174
+ return cards
175
+
176
+
177
+ def _synthesis_cards(synergy_report: dict | None,
178
+ *, top_n: int = 3) -> list[AgentActionCard]:
179
+ cards: list[AgentActionCard] = []
180
+ if not synergy_report:
181
+ return cards
182
+ candidates = synergy_report.get("candidates") or []
183
+ for c in candidates[:top_n]:
184
+ cid = c.get("id") or _slug("syn",
185
+ c.get("producer", ""),
186
+ c.get("consumer", ""))
187
+ cards.append(AgentActionCard(
188
+ schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
189
+ id=_slug("synergy", cid),
190
+ kind="synthesis",
191
+ title=f"Synergy adapter: {c.get('producer', '?')} → "
192
+ f"{c.get('consumer', '?')}",
193
+ why=(c.get("reason")
194
+ or "forge synergy detected a high-confidence "
195
+ "feature-pair compose-by-law match."),
196
+ risk="medium",
197
+ applyable=True,
198
+ write_scope=[],
199
+ commands=[f"forge synergy implement {cid}",
200
+ "python -m pytest"],
201
+ related_fcodes=[],
202
+ next_command=f"forge synergy implement {cid}",
203
+ sample_path=None,
204
+ score_delta_estimate=0,
205
+ ))
206
+ return cards
207
+
208
+
209
+ def _composition_cards(emergent_report: dict | None,
210
+ *, top_n: int = 3) -> list[AgentActionCard]:
211
+ cards: list[AgentActionCard] = []
212
+ if not emergent_report:
213
+ return cards
214
+ candidates = emergent_report.get("candidates") or []
215
+ for c in candidates[:top_n]:
216
+ cid = c.get("id") or _slug("emg", *(s for s in c.get("chain", [])))
217
+ chain = c.get("chain") or []
218
+ cards.append(AgentActionCard(
219
+ schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
220
+ id=_slug("emergent", cid),
221
+ kind="composition",
222
+ title=f"Compose: {' → '.join(chain) if chain else cid}",
223
+ why=(c.get("reason")
224
+ or "forge emergent surfaced a hidden composition chain."),
225
+ risk="medium",
226
+ applyable=True,
227
+ write_scope=[],
228
+ commands=[f"forge emergent synthesize {cid}",
229
+ "python -m pytest"],
230
+ related_fcodes=[],
231
+ next_command=f"forge emergent synthesize {cid}",
232
+ sample_path=None,
233
+ score_delta_estimate=0,
234
+ ))
235
+ return cards
236
+
237
+
238
+ def _rank(card: AgentActionCard) -> tuple[int, int, int, str]:
239
+ """Stable card rank — see module docstring."""
240
+ return (
241
+ 0 if card.get("applyable") else 1,
242
+ _KIND_RANK.get(card.get("kind", "operational"), 99),
243
+ _RISK_RANK.get(card.get("risk", "high"), 99),
244
+ card.get("id", ""),
245
+ )
246
+
247
+
248
+ def emit_agent_plan(
249
+ *,
250
+ project_root: str,
251
+ goal: str,
252
+ mode: str = "improve",
253
+ wire_report: dict | None = None,
254
+ certify_report: dict | None = None,
255
+ emergent_report: dict | None = None,
256
+ synergy_report: dict | None = None,
257
+ package: str | None = None,
258
+ top_n: int = 7,
259
+ ) -> AgentPlan:
260
+ """Build an agent_plan/v1 from any combination of Forge reports.
261
+
262
+ Caller is responsible for running the source scans (cheap to do
263
+ in parallel; a3 ``run_auto_plan`` wraps the orchestration).
264
+ """
265
+ if top_n < 1:
266
+ raise ValueError("top_n must be >= 1")
267
+ if mode not in ("improve", "absorb"):
268
+ raise ValueError(f"mode must be 'improve' | 'absorb', got {mode!r}")
269
+
270
+ cards: list[AgentActionCard] = []
271
+ cards.extend(_operational_cards(certify_report, package=package))
272
+ cards.extend(_architectural_cards(wire_report,
273
+ package_root=package or project_root))
274
+ cards.extend(_synthesis_cards(synergy_report))
275
+ cards.extend(_composition_cards(emergent_report))
276
+ cards.sort(key=_rank)
277
+
278
+ applyable = sum(1 for c in cards if c.get("applyable"))
279
+ score = float((certify_report or {}).get("score", 0.0))
280
+ if wire_report and wire_report.get("verdict") == "PASS" and score >= 100:
281
+ verdict = "PASS"
282
+ elif not cards:
283
+ verdict = "PASS"
284
+ else:
285
+ verdict = "FAIL"
286
+
287
+ next_command = (cards[0]["next_command"] if cards
288
+ else "# already PASSing — no next action.")
289
+
290
+ sources: dict[str, str] = {}
291
+ if wire_report:
292
+ sources["wire"] = wire_report.get("schema_version", "")
293
+ if certify_report:
294
+ sources["certify"] = certify_report.get("schema_version", "")
295
+ if emergent_report:
296
+ sources["emergent"] = emergent_report.get("schema_version", "")
297
+ if synergy_report:
298
+ sources["synergy"] = synergy_report.get("schema_version", "")
299
+
300
+ plan: AgentPlan = AgentPlan(
301
+ schema_version=SCHEMA_VERSION_AGENT_PLAN_V1,
302
+ generated_at_utc=_now_utc_iso(),
303
+ verdict=verdict, # type: ignore[typeddict-item]
304
+ goal=goal,
305
+ mode=mode,
306
+ project_root=str(project_root),
307
+ top_actions=list(cards[:top_n]),
308
+ action_count=len(cards),
309
+ applyable_count=applyable,
310
+ next_command=next_command,
311
+ # Codex feedback (round 3): surface the certify score on the
312
+ # plan envelope so MCP _summary digests inherit the real number.
313
+ score=(float(certify_report["score"])
314
+ if certify_report and "score" in certify_report else None),
315
+ sources=sources,
316
+ )
317
+ # Defensive — TypedDict isn't enforced at runtime.
318
+ for f in REQUIRED_PLAN_FIELDS:
319
+ if f not in plan:
320
+ raise RuntimeError(
321
+ f"agent_plan emitter built a plan missing required "
322
+ f"field {f!r} — schema/emitter drift"
323
+ )
324
+ return plan
@@ -0,0 +1,277 @@
1
+ """Tier a1 — agent-native compact-blocker summaries.
2
+
3
+ Direct response to field feedback from Codex (the Atomadic-Lang
4
+ agent) after using earlier Forge releases:
5
+
6
+ > "Make tool outputs compact and explicitly actionable, because
7
+ > agents thrive on 'here are the 2 things blocking release' more
8
+ > than huge manifests."
9
+
10
+ Every Forge report (wire / certify / enforce) is structurally
11
+ correct but verbose. An agent consuming them via ``forge mcp serve``
12
+ or ``--json`` has to wade through dozens of KB to find the next
13
+ action. This module condenses any combination of those reports
14
+ into a 3-key shape:
15
+
16
+ {
17
+ "schema_version": "atomadic-forge.summary/v1",
18
+ "verdict": "PASS" | "FAIL" | ...,
19
+ "score": 0-100,
20
+ "blockers": [
21
+ {"f_code": "F0050", "title": "...",
22
+ "next_command": "echo '# my-pkg' > README.md",
23
+ "severity": "error", "auto_fixable": true},
24
+ ...
25
+ ],
26
+ "next_command": "<the single fastest unblock>",
27
+ "blocker_count": N,
28
+ "auto_fixable_count": M,
29
+ }
30
+
31
+ Ranking is deterministic (so the same input always yields the same
32
+ ordered blockers): auto_fixable first (cheap wins), then by F-code
33
+ severity (error > warn > info), then by frequency in the report,
34
+ then by F-code value (lexicographic) as a tiebreak.
35
+
36
+ Pure: no I/O, no LLM call. The caller decides whether to print the
37
+ summary, embed it in an MCP response, or fold it into a Receipt's
38
+ notes field.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ from typing import Any, TypedDict
43
+
44
+ from ..a0_qk_constants.error_codes import (
45
+ fcode_for_tier_violation,
46
+ get_fcode,
47
+ )
48
+
49
+ _SEVERITY_RANK = {"error": 0, "warn": 1, "info": 2}
50
+
51
+
52
+ class Blocker(TypedDict, total=False):
53
+ f_code: str
54
+ title: str
55
+ next_command: str
56
+ severity: str
57
+ auto_fixable: bool
58
+ occurrences: int # how many wire violations / issues fed this
59
+ sample_path: str | None # representative file (when applicable)
60
+
61
+
62
+ def _certify_blockers(
63
+ certify_report: dict | None,
64
+ *,
65
+ package_root: str | None,
66
+ ) -> list[Blocker]:
67
+ if not certify_report:
68
+ return []
69
+ out: list[Blocker] = []
70
+ if not certify_report.get("documentation_complete", True):
71
+ out.append(_blocker(
72
+ "F0050",
73
+ next_command=(
74
+ f"echo '# {package_root or 'your_package'}' > README.md"
75
+ if package_root else "add a README.md with at least an H1 + a one-paragraph intro"
76
+ ),
77
+ ))
78
+ if not certify_report.get("tests_present", True):
79
+ out.append(_blocker(
80
+ "F0051",
81
+ next_command="mkdir -p tests && cat > tests/test_smoke.py "
82
+ "<<'PY'\nimport <pkg>\nPY",
83
+ ))
84
+ if not certify_report.get("tier_layout_present", True):
85
+ out.append(_blocker(
86
+ "F0052",
87
+ next_command=(
88
+ f"forge auto . ./out --apply --package "
89
+ f"{package_root or 'your_pkg'}"
90
+ ),
91
+ ))
92
+ if not certify_report.get("no_upward_imports", True):
93
+ out.append(_blocker(
94
+ "F0053",
95
+ next_command=(
96
+ f"forge wire {package_root or 'src/your_pkg'} "
97
+ "--suggest-repairs && forge enforce "
98
+ f"{package_root or 'src/your_pkg'} --apply"
99
+ ),
100
+ ))
101
+ return out
102
+
103
+
104
+ def _wire_blockers(
105
+ wire_report: dict | None,
106
+ *,
107
+ package_root: str | None,
108
+ ) -> list[Blocker]:
109
+ if not wire_report:
110
+ return []
111
+ violations = wire_report.get("violations") or []
112
+ if not violations:
113
+ return []
114
+ # Group by F-code so duplicate violations of the same kind collapse
115
+ # into one blocker with a sample path + occurrence count.
116
+ by_fcode: dict[str, list[dict]] = {}
117
+ for v in violations:
118
+ code = v.get("f_code") or fcode_for_tier_violation(
119
+ v.get("from_tier", ""), v.get("to_tier", ""))
120
+ by_fcode.setdefault(code, []).append(v)
121
+ out: list[Blocker] = []
122
+ for code, group in by_fcode.items():
123
+ sample = group[0]
124
+ sample_file = sample.get("file", "")
125
+ if get_fcode(code) and get_fcode(code).get("auto_fixable"): # type: ignore[union-attr]
126
+ cmd = (
127
+ f"forge enforce {package_root or 'src/your_pkg'} --apply "
128
+ f"# resolves {code}"
129
+ )
130
+ else:
131
+ cmd = (
132
+ f"# review manually: {sample_file} cannot move mechanically; "
133
+ "either invert the import direction or extract the symbol "
134
+ "down to a lower tier."
135
+ )
136
+ out.append(_blocker(
137
+ code,
138
+ next_command=cmd,
139
+ occurrences=len(group),
140
+ sample_path=sample_file,
141
+ ))
142
+ return out
143
+
144
+
145
+ def _blocker(
146
+ f_code: str,
147
+ *,
148
+ next_command: str,
149
+ occurrences: int = 1,
150
+ sample_path: str | None = None,
151
+ ) -> Blocker:
152
+ entry = get_fcode(f_code)
153
+ if entry is None:
154
+ return Blocker(
155
+ f_code=f_code,
156
+ title=f"unregistered F-code: {f_code}",
157
+ next_command=next_command,
158
+ severity="error",
159
+ auto_fixable=False,
160
+ occurrences=occurrences,
161
+ sample_path=sample_path,
162
+ )
163
+ return Blocker(
164
+ f_code=f_code,
165
+ title=entry["title"],
166
+ next_command=next_command,
167
+ severity=entry["severity"],
168
+ auto_fixable=entry["auto_fixable"],
169
+ occurrences=occurrences,
170
+ sample_path=sample_path,
171
+ )
172
+
173
+
174
+ def _rank(blocker: Blocker) -> tuple[int, int, int, str]:
175
+ """Stable ranking: auto_fixable first, then severity, then frequency
176
+ (descending), then F-code lex order."""
177
+ return (
178
+ 0 if blocker.get("auto_fixable") else 1,
179
+ _SEVERITY_RANK.get(blocker.get("severity", "error"), 0),
180
+ -int(blocker.get("occurrences", 1)),
181
+ blocker.get("f_code", ""),
182
+ )
183
+
184
+
185
+ def summarize_blockers(
186
+ *,
187
+ wire_report: dict | None = None,
188
+ certify_report: dict | None = None,
189
+ package_root: str | None = None,
190
+ top_n: int = 5,
191
+ ) -> dict[str, Any]:
192
+ """Compact-blocker summary for agent consumption (Codex feedback).
193
+
194
+ Either ``wire_report`` or ``certify_report`` (or both) may be
195
+ None; the function returns whatever blockers it can derive.
196
+ ``top_n`` (default 5) caps the returned blocker list — the
197
+ blocker_count field always reflects the FULL count regardless.
198
+ """
199
+ if top_n < 1:
200
+ raise ValueError("top_n must be >= 1")
201
+
202
+ blockers = _certify_blockers(certify_report, package_root=package_root)
203
+ blockers.extend(_wire_blockers(wire_report, package_root=package_root))
204
+ blockers.sort(key=_rank)
205
+
206
+ auto = sum(1 for b in blockers if b.get("auto_fixable"))
207
+ # Codex feedback: bare-wire callers have no certify score; render
208
+ # 'no score' instead of a misleading 0/100. None signals 'unknown'.
209
+ score = (certify_report or {}).get("score") if certify_report is not None else None
210
+ if certify_report is None and wire_report is not None:
211
+ # Bare-wire callers: derive a coarse verdict from wire alone.
212
+ verdict = wire_report.get("verdict", "FAIL")
213
+ elif wire_report is not None and (certify_report or {}).get("score", 0) >= 100 \
214
+ and wire_report.get("verdict") == "PASS":
215
+ verdict = "PASS"
216
+ elif certify_report is not None and not blockers:
217
+ verdict = "PASS"
218
+ else:
219
+ verdict = "FAIL"
220
+
221
+ next_command = blockers[0]["next_command"] if blockers else (
222
+ "# already PASSing — re-run on the next change."
223
+ )
224
+
225
+ return {
226
+ "schema_version": "atomadic-forge.summary/v1",
227
+ "verdict": verdict,
228
+ "score": score,
229
+ "blocker_count": len(blockers),
230
+ "auto_fixable_count": auto,
231
+ "blockers": list(blockers[:top_n]),
232
+ "next_command": next_command,
233
+ }
234
+
235
+
236
+ def render_summary_text(summary: dict, *, width: int = 60) -> str:
237
+ """Single-screen plain-text rendering of the summary.
238
+
239
+ Designed for human terminals AND for agent consumption: every
240
+ line is short, every blocker fits on 2 lines, the next-command
241
+ is the very last visible row.
242
+ """
243
+ if width < 30:
244
+ raise ValueError("width must be >= 30")
245
+ lines: list[str] = []
246
+ verdict = summary.get("verdict", "?")
247
+ score = summary.get("score", 0)
248
+ n = summary.get("blocker_count", 0)
249
+ auto = summary.get("auto_fixable_count", 0)
250
+ glyph = {"PASS": "✓", "FAIL": "✗", "REFINE": "↻", "QUARANTINE": "⏸"}.get(
251
+ verdict, "?")
252
+ score_part = (f"score {score:.0f}/100 "
253
+ if isinstance(score, int | float) else "")
254
+ lines.append(f"{glyph} {verdict} {score_part}"
255
+ f"{n} blocker{'' if n == 1 else 's'} "
256
+ f"({auto} auto-fixable)")
257
+ lines.append("─" * width)
258
+ if not summary.get("blockers"):
259
+ lines.append("(no blockers)")
260
+ for i, b in enumerate(summary.get("blockers", []), 1):
261
+ tag = "AUTO" if b.get("auto_fixable") else "REVIEW"
262
+ title = b.get("title", "")
263
+ prefix = f" {i}. [{b.get('f_code', 'F????')}] [{tag}] "
264
+ # Cap the WHOLE row at width, not just the title.
265
+ budget = max(0, width - len(prefix))
266
+ if len(title) > budget:
267
+ title = title[: max(0, budget - 1)] + "…"
268
+ lines.append(prefix + title)
269
+ cmd = b.get("next_command", "").strip()
270
+ if cmd:
271
+ short = cmd if len(cmd) < width - 6 else cmd[: width - 7] + "…"
272
+ lines.append(f" → {short}")
273
+ lines.append("─" * width)
274
+ nc = summary.get("next_command", "").strip()
275
+ if nc:
276
+ lines.append(f"NEXT: {nc[: width - 6]}")
277
+ return "\n".join(lines)