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,115 @@
1
+ """Tier a1 — pure tier classification.
2
+
3
+ Word-boundary token matching (the substring-bug-fixed version) plus optional
4
+ body-aware promotion: any class with mutable instance state (``self.<attr>
5
+ = …``) is promoted to ``a2_mo_composites`` regardless of name signals.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any
12
+
13
+ _WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*")
14
+
15
+ _CONTRACT = frozenset({"constant", "const", "schema", "manifest", "token",
16
+ "invariant", "proof"})
17
+ _ATOM = frozenset({"button", "textarea", "input", "validator", "validate",
18
+ "format", "parse", "atom"})
19
+ _COMPOSITE = frozenset({"engine", "service", "client", "manager", "store",
20
+ "hook", "calculator", "molecule"})
21
+ _FEATURE = frozenset({"registry", "gateway", "vault", "module", "feature",
22
+ "workflow", "page", "screen", "organism"})
23
+ _ORCHESTRATION = frozenset({"server", "router", "orchestrator", "bridge",
24
+ "runtime", "system", "main", "mcp", "cli"})
25
+
26
+
27
+ def word_tokens(text: str) -> set[str]:
28
+ """Tokenize on word boundaries and split camelCase.
29
+
30
+ ``"AgenticSwarm"`` → ``{"agentic", "swarm"}``; ``"atomadic-v2"`` →
31
+ ``{"atomadic", "v2"}`` — no false-positive on ``"atom"``.
32
+ """
33
+ raw = _WORD_RE.findall(text)
34
+ out: set[str] = set()
35
+ for word in raw:
36
+ for piece in re.findall(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z0-9]+|[A-Z]+", word):
37
+ if piece:
38
+ out.add(piece.lower())
39
+ out.add(word.lower())
40
+ return out
41
+
42
+
43
+ def classify_tier(*, name: str, kind: str, path: str = "",
44
+ body_signals: dict[str, Any] | None = None) -> str:
45
+ """Return the canonical tier directory for a symbol.
46
+
47
+ ``body_signals`` (optional) carries ``has_self_assign`` /
48
+ ``has_class_attr_collections`` flags from the body extractor. When set,
49
+ a class with mutable instance state is forced to a2.
50
+ """
51
+ tokens = word_tokens(f"{path} {name} {kind}")
52
+ stateful = bool(body_signals and (
53
+ body_signals.get("has_self_assign")
54
+ or body_signals.get("has_class_attr_collections")
55
+ ))
56
+
57
+ if kind in ("class", "type") and stateful:
58
+ if tokens & _ORCHESTRATION:
59
+ return "a4_sy_orchestration"
60
+ if tokens & _FEATURE:
61
+ return "a3_og_features"
62
+ return "a2_mo_composites"
63
+
64
+ if tokens & _CONTRACT:
65
+ return "a0_qk_constants"
66
+ if tokens & _ATOM:
67
+ return "a1_at_functions"
68
+ if tokens & _COMPOSITE:
69
+ return "a2_mo_composites"
70
+ if tokens & _FEATURE:
71
+ return "a3_og_features"
72
+ if tokens & _ORCHESTRATION:
73
+ return "a4_sy_orchestration"
74
+
75
+ if kind == "function":
76
+ return "a1_at_functions"
77
+ if kind in ("class", "type"):
78
+ return "a2_mo_composites"
79
+ return "a1_at_functions"
80
+
81
+
82
+ _IO_ROOT_MODULES = frozenset({"urllib", "requests", "httpx", "socket",
83
+ "subprocess", "shutil", "os", "sqlite3",
84
+ "boto3", "smtplib"})
85
+ _IO_BUILTINS = frozenset({"open", "print", "input", "connect"})
86
+
87
+
88
+ def detect_effects(node: Any) -> list[str]:
89
+ """Cheap effect inference on an ``ast.AST`` node body."""
90
+ import ast
91
+
92
+ effects: list[str] = []
93
+ for child in ast.walk(node):
94
+ if isinstance(child, ast.Call):
95
+ f = child.func
96
+ if isinstance(f, ast.Name) and f.id in _IO_BUILTINS:
97
+ effects.append("io")
98
+ elif isinstance(f, ast.Attribute):
99
+ root = f
100
+ while isinstance(root, ast.Attribute):
101
+ root = root.value
102
+ if isinstance(root, ast.Name) and root.id in _IO_ROOT_MODULES:
103
+ effects.append("io")
104
+ if f.attr in ("write", "send", "post", "put", "delete",
105
+ "execute", "commit"):
106
+ effects.append("state")
107
+ if isinstance(child, ast.Global | ast.Nonlocal):
108
+ effects.append("state")
109
+ if not effects:
110
+ return ["pure"]
111
+ seen: list[str] = []
112
+ for e in effects:
113
+ if e not in seen:
114
+ seen.append(e)
115
+ return seen
@@ -0,0 +1,167 @@
1
+ """Tier a1 — pure discovery for the Commandsmith CLI registry.
2
+
3
+ Walks a directory tree of would-be command modules and returns a list of
4
+ :class:`RegisteredCommandCard` describing what each module exports.
5
+
6
+ Three recognised surfaces:
7
+
8
+ * **typer_app** — module exposes a top-level ``app: typer.Typer``.
9
+ * **register_fn** — module exposes ``register(app: typer.Typer) -> None``.
10
+ * **wrapped_class** — module exposes a public class plus a class-level
11
+ ``COMMAND_NAME`` attribute, indicating it should be wrapped by the
12
+ generator (see ``commandsmith_render``).
13
+
14
+ This module is pure: it only reads files and parses AST. It NEVER imports
15
+ the modules it describes — that lives in tier a2 / a3 to keep a1 import-safe.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ from collections.abc import Iterable
22
+ from pathlib import Path
23
+
24
+ from ..a0_qk_constants.commandsmith_types import (
25
+ CommandSignatureCard,
26
+ RegisteredCommandCard,
27
+ )
28
+
29
+ _PUBLIC = lambda name: not name.startswith("_") # noqa: E731 — short pure helper
30
+
31
+
32
+ def _module_for(path: Path, package_root: Path, package: str) -> str:
33
+ rel = path.relative_to(package_root)
34
+ parts = list(rel.with_suffix("").parts)
35
+ if parts and parts[-1] == "__init__":
36
+ parts.pop()
37
+ return ".".join([package, *parts]) if parts else package
38
+
39
+
40
+ def _docstring_first_line(node: ast.AST) -> str:
41
+ doc = ast.get_docstring(node) or ""
42
+ first = doc.strip().split("\n", 1)[0] if doc else ""
43
+ return first.strip()
44
+
45
+
46
+ def _collect_signature(fn: ast.FunctionDef) -> CommandSignatureCard:
47
+ params: list[str] = []
48
+ for arg in fn.args.args:
49
+ ann = ast.unparse(arg.annotation) if arg.annotation else "Any"
50
+ params.append(f"{arg.arg}: {ann}")
51
+ ret = ast.unparse(fn.returns) if fn.returns else "Any"
52
+ return CommandSignatureCard(
53
+ name=fn.name,
54
+ parameters=params,
55
+ return_type=ret,
56
+ docstring=_docstring_first_line(fn),
57
+ )
58
+
59
+
60
+ def _detect_surface(tree: ast.Module) -> tuple[str, str, bool, list[ast.AST], str]:
61
+ """Return (surface, help_text, hidden, sub_command_nodes, command_name)."""
62
+ has_typer_app = False
63
+ has_register_fn = False
64
+ has_command_name_const = False
65
+ command_name = ""
66
+ help_text = _docstring_first_line(tree)
67
+ hidden = False
68
+ classes: list[ast.ClassDef] = []
69
+ register_fn: ast.FunctionDef | None = None
70
+
71
+ for node in tree.body:
72
+ if isinstance(node, ast.Assign):
73
+ for target in node.targets:
74
+ if isinstance(target, ast.Name):
75
+ if target.id == "app":
76
+ has_typer_app = True
77
+ elif target.id == "COMMAND_NAME":
78
+ has_command_name_const = True
79
+ if isinstance(node.value, ast.Constant) and isinstance(
80
+ node.value.value, str
81
+ ):
82
+ command_name = node.value.value
83
+ elif target.id == "COMMAND_HELP":
84
+ if isinstance(node.value, ast.Constant) and isinstance(
85
+ node.value.value, str
86
+ ):
87
+ help_text = node.value.value
88
+ elif target.id == "COMMAND_HIDDEN":
89
+ if isinstance(node.value, ast.Constant) and isinstance(
90
+ node.value.value, bool
91
+ ):
92
+ hidden = node.value.value
93
+ elif isinstance(node, ast.FunctionDef) and node.name == "register":
94
+ has_register_fn = True
95
+ register_fn = node
96
+ elif isinstance(node, ast.ClassDef) and _PUBLIC(node.name):
97
+ classes.append(node)
98
+
99
+ if has_typer_app:
100
+ return ("typer_app", help_text, hidden, [], command_name)
101
+ if has_register_fn:
102
+ nodes: list[ast.AST] = [register_fn] if register_fn else []
103
+ return ("register_fn", help_text, hidden, nodes, command_name)
104
+ if has_command_name_const and classes:
105
+ classes[0]._commandsmith_name = command_name # type: ignore[attr-defined]
106
+ return ("wrapped_class", help_text, hidden, classes, command_name)
107
+ return ("", "", False, [], "")
108
+
109
+
110
+ def discover_command_modules(
111
+ package_root: Path,
112
+ package: str,
113
+ sub_dirs: Iterable[str] = ("commands",),
114
+ source_root: str = "atomadic_forge_seed",
115
+ ) -> list[RegisteredCommandCard]:
116
+ """Scan ``<package_root>/<sub_dir>`` files for command surfaces.
117
+
118
+ Returns a sorted list of :class:`RegisteredCommandCard`.
119
+ Files starting with ``_`` (incl. ``__init__.py``) are ignored.
120
+ """
121
+ cards: list[RegisteredCommandCard] = []
122
+ for sub in sub_dirs:
123
+ base = package_root / sub
124
+ if not base.exists():
125
+ continue
126
+ for py_file in sorted(base.rglob("*.py")):
127
+ if py_file.name.startswith("_"):
128
+ continue
129
+ try:
130
+ tree = ast.parse(py_file.read_text(encoding="utf-8"))
131
+ except (SyntaxError, OSError):
132
+ continue
133
+ surface, help_text, hidden, sub_nodes, explicit_name = _detect_surface(tree)
134
+ if not surface:
135
+ continue
136
+ sub_cmds: list[CommandSignatureCard] = []
137
+ for node in sub_nodes:
138
+ if isinstance(node, ast.FunctionDef):
139
+ sub_cmds.append(_collect_signature(node))
140
+ elif isinstance(node, ast.ClassDef):
141
+ for body in node.body:
142
+ if isinstance(body, ast.FunctionDef) and _PUBLIC(body.name):
143
+ sub_cmds.append(_collect_signature(body))
144
+ name = py_file.stem
145
+ if surface == "wrapped_class":
146
+ cls = next(
147
+ (c for c in sub_nodes if isinstance(c, ast.ClassDef)), None
148
+ )
149
+ custom = getattr(cls, "_commandsmith_name", "") if cls else ""
150
+ if custom:
151
+ name = custom
152
+ # Explicit COMMAND_NAME wins for any surface (e.g. wrappers strip
153
+ # the trailing ``_cli`` suffix to expose a cleaner verb).
154
+ if explicit_name:
155
+ name = explicit_name
156
+ cards.append(
157
+ RegisteredCommandCard(
158
+ name=name.replace("_", "-"),
159
+ module=_module_for(py_file, package_root, package),
160
+ surface=surface, # type: ignore[arg-type]
161
+ help_text=help_text,
162
+ hidden=hidden,
163
+ sub_commands=sub_cmds,
164
+ source_root=source_root,
165
+ )
166
+ )
167
+ return sorted(cards, key=lambda c: c["name"])
@@ -0,0 +1,267 @@
1
+ """Tier a1 — pure renderers for the Commandsmith CLI registry.
2
+
3
+ Three renderers:
4
+
5
+ * :func:`render_registry_module` — emits the ``atomadic_forge.commands._registry``
6
+ Python source that auto-imports each discovered command module and wires
7
+ it onto the supplied Typer app.
8
+
9
+ * :func:`render_command_doc` — emits a Markdown page for a single command
10
+ card.
11
+
12
+ * :func:`render_command_index` — emits ``docs/commands/INDEX.md`` listing
13
+ every registered command.
14
+
15
+ * :func:`render_wrapper_module` — emits a Python source file that wraps a
16
+ bare class (e.g. an assimilated ``CherryPicker``) in a Typer subcommand
17
+ group, so newly assimilated features can be invoked from the CLI without
18
+ hand-coded glue.
19
+
20
+ All four are pure: they take cards and return strings. No I/O.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from collections.abc import Iterable
26
+
27
+ from ..a0_qk_constants.commandsmith_types import (
28
+ CommandSignatureCard,
29
+ RegisteredCommandCard,
30
+ )
31
+
32
+ _REGISTRY_HEADER = '''"""
33
+ Auto-generated by Commandsmith — do NOT hand-edit.
34
+ Run ``atomadic-forge commandsmith sync`` to refresh.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import typer
40
+
41
+
42
+ '''
43
+
44
+
45
+ def render_registry_module(cards: Iterable[RegisteredCommandCard]) -> str:
46
+ """Render an importable module that registers every card on a Typer app."""
47
+ cards = list(cards)
48
+ imports: list[str] = []
49
+ body: list[str] = []
50
+ for card in cards:
51
+ mod = card["module"]
52
+ imports.append(f"from {mod} import * # noqa: F401,F403 — exported via {card['surface']}")
53
+ # We import the module by full name as alias so different surfaces work
54
+ # together; collisions are avoided because names are distinct verbs.
55
+ if not cards:
56
+ body.append(" pass # no commands discovered")
57
+ else:
58
+ body.append(" \"\"\"Register every Commandsmith-discovered command on ``app``.\"\"\"")
59
+ for card in cards:
60
+ mod = card["module"]
61
+ verb = card["name"]
62
+ help_text = card["help_text"].replace('"""', "'''")
63
+ hidden = card["hidden"]
64
+ if card["surface"] == "typer_app":
65
+ body.append(
66
+ f" from {mod} import app as _app_{verb.replace('-', '_')}"
67
+ )
68
+ body.append(
69
+ f" app.add_typer(_app_{verb.replace('-', '_')}, name={verb!r}, "
70
+ f"help={help_text!r}, hidden={hidden!r})"
71
+ )
72
+ elif card["surface"] == "register_fn":
73
+ body.append(f" from {mod} import register as _reg_{verb.replace('-', '_')}")
74
+ body.append(
75
+ f" _sub_{verb.replace('-', '_')} = typer.Typer(name={verb!r}, "
76
+ f"help={help_text!r})"
77
+ )
78
+ body.append(
79
+ f" _reg_{verb.replace('-', '_')}(_sub_{verb.replace('-', '_')})"
80
+ )
81
+ body.append(
82
+ f" app.add_typer(_sub_{verb.replace('-', '_')}, name={verb!r}, "
83
+ f"hidden={hidden!r})"
84
+ )
85
+ elif card["surface"] == "wrapped_class":
86
+ wrapper_mod = f"{mod}_cli"
87
+ body.append(
88
+ f" from {wrapper_mod} import app as _wrap_{verb.replace('-', '_')}"
89
+ )
90
+ body.append(
91
+ f" app.add_typer(_wrap_{verb.replace('-', '_')}, name={verb!r}, "
92
+ f"help={help_text!r}, hidden={hidden!r})"
93
+ )
94
+ src = _REGISTRY_HEADER
95
+ src += "def register_all(app: typer.Typer) -> None:\n"
96
+ src += "\n".join(body) + "\n"
97
+ return src
98
+
99
+
100
+ def render_command_doc(card: RegisteredCommandCard) -> str:
101
+ """Render a Markdown page describing one command."""
102
+ lines = [
103
+ f"# `atomadic-forge {card['name']}`",
104
+ "",
105
+ f"_{card['help_text']}_" if card["help_text"] else "",
106
+ "",
107
+ f"- **Module**: `{card['module']}`",
108
+ f"- **Surface**: `{card['surface']}`",
109
+ f"- **Source root**: `{card['source_root']}`",
110
+ f"- **Hidden**: `{card['hidden']}`",
111
+ "",
112
+ ]
113
+ if card["sub_commands"]:
114
+ lines.append("## Sub-commands")
115
+ lines.append("")
116
+ for sc in card["sub_commands"]:
117
+ lines.append(f"### `{sc['name']}`")
118
+ lines.append("")
119
+ if sc["docstring"]:
120
+ lines.append(sc["docstring"])
121
+ lines.append("")
122
+ if sc["parameters"]:
123
+ lines.append("Parameters:")
124
+ lines.append("")
125
+ for p in sc["parameters"]:
126
+ lines.append(f"- `{p}`")
127
+ lines.append("")
128
+ lines.append(f"Returns: `{sc['return_type']}`")
129
+ lines.append("")
130
+ return "\n".join(line for line in lines if line is not None) + "\n"
131
+
132
+
133
+ def render_command_index(cards: Iterable[RegisteredCommandCard]) -> str:
134
+ """Render docs/commands/INDEX.md."""
135
+ cards = sorted(cards, key=lambda c: c["name"])
136
+ lines = ["# ASS-ADE Commands", "", "Auto-generated index. One row per top-level CLI verb.", "",
137
+ "| Verb | Help | Source | Surface |",
138
+ "|------|------|--------|---------|"]
139
+ for c in cards:
140
+ help_short = c["help_text"].replace("|", "\\|") or "_(no help)_"
141
+ lines.append(
142
+ f"| `{c['name']}` | {help_short} | `{c['source_root']}` | `{c['surface']}` |"
143
+ )
144
+ return "\n".join(lines) + "\n"
145
+
146
+
147
+ def render_wrapper_module(
148
+ *,
149
+ target_module: str,
150
+ target_class: str,
151
+ command_name: str,
152
+ sub_commands: list[CommandSignatureCard],
153
+ help_text: str = "",
154
+ init_params: list[str] | None = None,
155
+ auto_scan: str | None = None,
156
+ ) -> str:
157
+ """Emit a wrapper Python module wrapping a class as a Typer subcommand group.
158
+
159
+ Generated app structure::
160
+
161
+ app = typer.Typer()
162
+ @app.callback() — accepts __init__ params, builds an instance, stashes it
163
+ on ``ctx.obj``. Optionally auto-invokes a "warm-up"
164
+ method (e.g. ``scan_repo``) before subcommands.
165
+ @app.command("verb") — pulls instance off ctx.obj, calls the method.
166
+ """
167
+ safe_help = help_text.replace('"""', "'''")
168
+ init_params = init_params or []
169
+ init_sig, init_fwd = _build_typer_args(init_params)
170
+ has_init_args = bool(init_fwd)
171
+
172
+ lines = [
173
+ '"""',
174
+ f"Auto-generated wrapper for {target_module}.{target_class}.",
175
+ "",
176
+ f"Run ``atomadic-forge {command_name} <method>`` to invoke methods on this class.",
177
+ "Re-emit with ``atomadic-forge commandsmith wrap`` after the source changes.",
178
+ '"""',
179
+ "",
180
+ "from __future__ import annotations",
181
+ "",
182
+ "import json",
183
+ "from pathlib import Path # noqa: F401 — used by some auto-generated annotations",
184
+ "import typer",
185
+ "",
186
+ f"from {target_module} import {target_class}",
187
+ "",
188
+ f"COMMAND_NAME = {command_name!r}",
189
+ f"COMMAND_HELP = {safe_help!r}",
190
+ "",
191
+ f'app = typer.Typer(no_args_is_help=True, help={safe_help!r})',
192
+ "",
193
+ ]
194
+
195
+ # Callback that builds the instance. Always emitted so subcommands can
196
+ # share the instance via ctx.obj.
197
+ cb_args = "ctx: typer.Context"
198
+ if init_sig:
199
+ cb_args += ", " + init_sig
200
+ lines.extend([
201
+ "@app.callback()",
202
+ f"def _build({cb_args}) -> None:",
203
+ f' """Construct an instance of {target_class} for this invocation."""',
204
+ ])
205
+ if has_init_args:
206
+ lines.append(f" instance = {target_class}({init_fwd})")
207
+ else:
208
+ lines.append(f" instance = {target_class}()")
209
+ if auto_scan:
210
+ lines.append(f" # auto-warm-up: call {auto_scan}() so subcommands see populated state")
211
+ lines.append(" try:")
212
+ lines.append(f" instance.{auto_scan}()")
213
+ lines.append(" except Exception:")
214
+ lines.append(" pass")
215
+ lines.append(" ctx.obj = instance")
216
+ lines.append("")
217
+
218
+ if not sub_commands:
219
+ lines.extend([
220
+ "@app.command('show')",
221
+ "def _show(ctx: typer.Context) -> None:",
222
+ f' """No public methods detected — show the {target_class} repr."""',
223
+ " typer.echo(repr(ctx.obj))",
224
+ ])
225
+
226
+ for sc in sub_commands:
227
+ if sc["name"] in {"__init__"}:
228
+ continue
229
+ verb = sc["name"].replace("_", "-")
230
+ py_args, fwd_args = _build_typer_args(sc["parameters"])
231
+ full_sig = "ctx: typer.Context"
232
+ if py_args:
233
+ full_sig += ", " + py_args
234
+ doc = sc["docstring"] or f"Invoke {target_class}.{sc['name']}."
235
+ lines.extend([
236
+ f"@app.command({verb!r})",
237
+ f"def _{sc['name']}({full_sig}) -> None:",
238
+ f' """{doc}"""',
239
+ f" result = ctx.obj.{sc['name']}({fwd_args})",
240
+ " if result is not None:",
241
+ " try:",
242
+ " typer.echo(json.dumps(result, indent=2, default=str))",
243
+ " except (TypeError, ValueError):",
244
+ " typer.echo(str(result))",
245
+ "",
246
+ ])
247
+ return "\n".join(lines) + "\n"
248
+
249
+
250
+ def _build_typer_args(parameters: list[str]) -> tuple[str, str]:
251
+ """Build (python signature args, forward-call args) from parameter strings.
252
+
253
+ Drops ``self``. Falls back to ``str`` for unknown annotations.
254
+ """
255
+ sig_parts: list[str] = []
256
+ fwd_parts: list[str] = []
257
+ for p in parameters:
258
+ name, _, ann = p.partition(":")
259
+ name = name.strip()
260
+ ann = ann.strip() or "str"
261
+ if name in ("self", "cls"):
262
+ continue
263
+ if ann not in ("int", "float", "bool", "str", "Path", "list", "dict"):
264
+ ann = "str"
265
+ sig_parts.append(f"{name}: {ann}")
266
+ fwd_parts.append(name)
267
+ return ", ".join(sig_parts), ", ".join(fwd_parts)
@@ -0,0 +1,94 @@
1
+ """Tier a1 — pure compiler-feedback prompt builder for the fix-round loop.
2
+
3
+ Golden Path Lane A W3 deliverable. Distinct from the regular iterate
4
+ turn:
5
+
6
+ * regular turn — wire + certify scores → LLM is asked to *improve
7
+ quality* (raise the score, fix violations,
8
+ etc.). Bounded by ``max_iterations``.
9
+ * fix-round turn — import_smoke FAIL (or pytest collection error)
10
+ → LLM is asked to *fix the broken code* without
11
+ adding features. Bounded by ``max_fix_rounds``
12
+ PER iterate turn.
13
+
14
+ A fix-round is much cheaper (one error trace, no quality scoring) and
15
+ should always run before the next regular turn — otherwise the
16
+ regular turn shows the LLM a ``score=0`` because the package didn't
17
+ even import, and the LLM wastes the round chasing scoring instead of
18
+ the actual blocker.
19
+
20
+ Pure: takes an ``ImportSmokeReport`` (or ImportSmoke-shaped dict) and
21
+ returns a string. No I/O, no LLM call. The forge_loop orchestrator
22
+ owns when to invoke this.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ _FIX_ROUND_HEADER = (
27
+ "FIX ROUND — your previous emission did not import. Do NOT add "
28
+ "features; do NOT touch unrelated files. Output ONLY the minimum "
29
+ "set of files needed to make the package importable.\n"
30
+ )
31
+
32
+ _FIX_ROUND_FOOTER = (
33
+ "\nRespond with the standard JSON array of `{path, content}` "
34
+ "objects. If you believe the package is already correct and the "
35
+ "error is environmental (e.g. a missing 3rd-party dep), output "
36
+ "the exact two characters `[]` and the loop will exit.\n"
37
+ )
38
+
39
+
40
+ def pack_compile_feedback(
41
+ smoke_report: dict,
42
+ *,
43
+ package: str,
44
+ fix_round_index: int,
45
+ max_fix_rounds: int,
46
+ ) -> str:
47
+ """Build the fix-round prompt from an ImportSmokeReport-shape dict.
48
+
49
+ Arguments:
50
+ smoke_report — dict with at least ``error_kind``,
51
+ ``error_message``, ``traceback_excerpt``.
52
+ package — the package name being iterated.
53
+ fix_round_index — 1-based current attempt within this turn.
54
+ max_fix_rounds — total budget so the LLM knows whether to
55
+ minimise the fix or give up cleanly.
56
+ """
57
+ if fix_round_index < 1:
58
+ raise ValueError("fix_round_index must be >= 1")
59
+ if max_fix_rounds < 1:
60
+ raise ValueError("max_fix_rounds must be >= 1")
61
+
62
+ kind = str(smoke_report.get("error_kind") or "ImportFailure")
63
+ message = str(smoke_report.get("error_message") or "(no message)")
64
+ trace = str(smoke_report.get("traceback_excerpt") or "").strip()
65
+
66
+ parts: list[str] = [
67
+ _FIX_ROUND_HEADER,
68
+ f" package: {package}",
69
+ f" fix-round attempt: {fix_round_index} of {max_fix_rounds}",
70
+ f" error_kind: {kind}",
71
+ f" error_message: {message}",
72
+ ]
73
+ if trace:
74
+ # Cap to avoid blowing the context window — the smoke
75
+ # collector already trims stderr to ~600 chars.
76
+ parts.append("\nTraceback excerpt:\n```\n" + trace[:1200] + "\n```")
77
+ parts.append(_FIX_ROUND_FOOTER)
78
+ return "\n".join(parts)
79
+
80
+
81
+ def should_fix_round(smoke_report: dict) -> bool:
82
+ """Return True iff the smoke report names a fixable runtime error.
83
+
84
+ 'Fixable' means: there's a concrete error_kind and non-empty
85
+ message we can show the LLM. Empty smoke (importable=True) returns
86
+ False; environmental failures with no useful trace also return
87
+ False so the orchestrator skips the round rather than re-prompting
88
+ the LLM with nothing.
89
+ """
90
+ if smoke_report.get("importable"):
91
+ return False
92
+ kind = smoke_report.get("error_kind") or ""
93
+ message = smoke_report.get("error_message") or ""
94
+ return bool(kind and message)