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.
- atomadic_forge/__init__.py +12 -0
- atomadic_forge/__main__.py +5 -0
- atomadic_forge/a0_qk_constants/__init__.py +1 -0
- atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
- atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
- atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
- atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
- atomadic_forge/a0_qk_constants/error_codes.py +296 -0
- atomadic_forge/a0_qk_constants/forge_types.py +89 -0
- atomadic_forge/a0_qk_constants/gen_language.py +116 -0
- atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
- atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
- atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
- atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
- atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
- atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
- atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
- atomadic_forge/a0_qk_constants/tier_names.py +47 -0
- atomadic_forge/a1_at_functions/__init__.py +1 -0
- atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
- atomadic_forge/a1_at_functions/agent_memory.py +139 -0
- atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
- atomadic_forge/a1_at_functions/agent_summary.py +277 -0
- atomadic_forge/a1_at_functions/body_extractor.py +306 -0
- atomadic_forge/a1_at_functions/card_renderer.py +210 -0
- atomadic_forge/a1_at_functions/certify_checks.py +445 -0
- atomadic_forge/a1_at_functions/chat_context.py +170 -0
- atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
- atomadic_forge/a1_at_functions/classify_tier.py +115 -0
- atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
- atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
- atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
- atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
- atomadic_forge/a1_at_functions/config_io.py +68 -0
- atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
- atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
- atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
- atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
- atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
- atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
- atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
- atomadic_forge/a1_at_functions/error_hints.py +105 -0
- atomadic_forge/a1_at_functions/evolution_log.py +94 -0
- atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
- atomadic_forge/a1_at_functions/generation_quality.py +322 -0
- atomadic_forge/a1_at_functions/import_repair.py +211 -0
- atomadic_forge/a1_at_functions/import_smoke.py +102 -0
- atomadic_forge/a1_at_functions/js_parser.py +539 -0
- atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
- atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
- atomadic_forge/a1_at_functions/llm_client.py +554 -0
- atomadic_forge/a1_at_functions/local_signer.py +134 -0
- atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
- atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
- atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
- atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
- atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
- atomadic_forge/a1_at_functions/policy_loader.py +107 -0
- atomadic_forge/a1_at_functions/preflight_change.py +227 -0
- atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
- atomadic_forge/a1_at_functions/provider_detect.py +157 -0
- atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
- atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
- atomadic_forge/a1_at_functions/recipes.py +186 -0
- atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
- atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
- atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
- atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
- atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
- atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
- atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
- atomadic_forge/a1_at_functions/scout_walk.py +309 -0
- atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
- atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
- atomadic_forge/a1_at_functions/stub_detector.py +158 -0
- atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
- atomadic_forge/a1_at_functions/synergy_render.py +252 -0
- atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
- atomadic_forge/a1_at_functions/test_runner.py +196 -0
- atomadic_forge/a1_at_functions/test_selector.py +122 -0
- atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
- atomadic_forge/a1_at_functions/tool_composer.py +130 -0
- atomadic_forge/a1_at_functions/transcript_log.py +70 -0
- atomadic_forge/a1_at_functions/wire_check.py +260 -0
- atomadic_forge/a2_mo_composites/__init__.py +1 -0
- atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
- atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
- atomadic_forge/a2_mo_composites/plan_store.py +164 -0
- atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
- atomadic_forge/a3_og_features/__init__.py +1 -0
- atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
- atomadic_forge/a3_og_features/demo_runner.py +502 -0
- atomadic_forge/a3_og_features/emergent_feature.py +95 -0
- atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
- atomadic_forge/a3_og_features/forge_enforce.py +107 -0
- atomadic_forge/a3_og_features/forge_evolve.py +176 -0
- atomadic_forge/a3_og_features/forge_loop.py +528 -0
- atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
- atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
- atomadic_forge/a3_og_features/lsp_server.py +98 -0
- atomadic_forge/a3_og_features/mcp_server.py +160 -0
- atomadic_forge/a3_og_features/setup_wizard.py +337 -0
- atomadic_forge/a3_og_features/synergy_feature.py +65 -0
- atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
- atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
- atomadic_forge/commands/__init__.py +1 -0
- atomadic_forge/commands/_registry.py +36 -0
- atomadic_forge/commands/audit.py +142 -0
- atomadic_forge/commands/chat.py +133 -0
- atomadic_forge/commands/commandsmith.py +178 -0
- atomadic_forge/commands/config_cmd.py +145 -0
- atomadic_forge/commands/demo.py +142 -0
- atomadic_forge/commands/emergent.py +124 -0
- atomadic_forge/commands/emergent_then_synergy.py +70 -0
- atomadic_forge/commands/evolve.py +122 -0
- atomadic_forge/commands/evolve_then_iterate.py +70 -0
- atomadic_forge/commands/feature_then_emergent.py +111 -0
- atomadic_forge/commands/iterate.py +140 -0
- atomadic_forge/commands/synergy.py +96 -0
- atomadic_forge/commands/synergy_then_emergent.py +70 -0
- atomadic_forge-0.3.2.dist-info/METADATA +471 -0
- atomadic_forge-0.3.2.dist-info/RECORD +131 -0
- atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
- atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
- atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
- atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Tier a1 — pure signature harvesting for the Emergent Scan.
|
|
2
|
+
|
|
3
|
+
Walks a tier-organized package and emits a :class:`SymbolSignatureCard` per
|
|
4
|
+
public callable. ``inputs`` and ``output`` are the type annotation texts as
|
|
5
|
+
they appear in source — we keep them as strings so the matcher can do
|
|
6
|
+
loose, normalized compatibility comparisons later (no real type system).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..a0_qk_constants.emergent_types import SymbolSignatureCard
|
|
17
|
+
|
|
18
|
+
_TIERS = (
|
|
19
|
+
"a0_qk_constants", "a1_at_functions", "a2_mo_composites",
|
|
20
|
+
"a3_og_features", "a4_sy_orchestration",
|
|
21
|
+
)
|
|
22
|
+
_PUBLIC = lambda name: not name.startswith("_") or name == "__init__" # noqa: E731
|
|
23
|
+
|
|
24
|
+
# Effects we treat as "impure" for the heuristic. Any function whose body
|
|
25
|
+
# touches one of these is marked ``is_pure=False``.
|
|
26
|
+
_IMPURE_HINTS = {
|
|
27
|
+
"open", "print", "input", "exec", "eval",
|
|
28
|
+
"Path", "subprocess", "socket", "requests", "urllib",
|
|
29
|
+
"logging", "os.system", "os.environ",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _module_for(file: Path, src_root: Path, package: str) -> str:
|
|
34
|
+
rel = file.relative_to(src_root).with_suffix("")
|
|
35
|
+
parts = list(rel.parts)
|
|
36
|
+
if parts and parts[-1] == "__init__":
|
|
37
|
+
parts.pop()
|
|
38
|
+
return ".".join([package, *parts]) if parts else package
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _tier_of(file: Path, src_root: Path) -> str:
|
|
42
|
+
parts = file.relative_to(src_root).parts
|
|
43
|
+
for p in parts:
|
|
44
|
+
if p in _TIERS:
|
|
45
|
+
return p
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _domain_of(stem: str) -> str:
|
|
50
|
+
"""Heuristic domain tag: pull the last meaningful slug from the file stem.
|
|
51
|
+
|
|
52
|
+
``a1_source_atomadic_v2_cherrypicker`` → ``cherrypicker``.
|
|
53
|
+
``commandsmith_render`` → ``commandsmith``. ``ingest`` → ``ingest``.
|
|
54
|
+
"""
|
|
55
|
+
cleaned = re.sub(r"^a\d_(?:source_)?", "", stem)
|
|
56
|
+
cleaned = re.sub(r"^(atomadic_forge_seed_|atomadic_v2_)", "", cleaned)
|
|
57
|
+
parts = cleaned.split("_")
|
|
58
|
+
return parts[0].lower() if parts else "misc"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ann_text(node: ast.AST | None) -> str:
|
|
62
|
+
if node is None:
|
|
63
|
+
return "Any"
|
|
64
|
+
try:
|
|
65
|
+
return ast.unparse(node)
|
|
66
|
+
except Exception:
|
|
67
|
+
return "Any"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_pure(fn: ast.AST) -> bool:
|
|
71
|
+
for child in ast.walk(fn):
|
|
72
|
+
if isinstance(child, ast.Call):
|
|
73
|
+
f = child.func
|
|
74
|
+
if isinstance(f, ast.Name) and f.id in _IMPURE_HINTS:
|
|
75
|
+
return False
|
|
76
|
+
if isinstance(f, ast.Attribute):
|
|
77
|
+
root = f
|
|
78
|
+
while isinstance(root, ast.Attribute):
|
|
79
|
+
root = root.value
|
|
80
|
+
if isinstance(root, ast.Name) and root.id in _IMPURE_HINTS:
|
|
81
|
+
return False
|
|
82
|
+
if isinstance(child, ast.Global | ast.Nonlocal):
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _docstring_first_line(node: ast.AST) -> str:
|
|
88
|
+
doc = ast.get_docstring(node) or ""
|
|
89
|
+
return (doc.strip().split("\n", 1)[0] if doc else "").strip()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _harvest_function(fn: ast.AST, *, module: str, tier: str, domain: str,
|
|
93
|
+
class_qualifier: str = "") -> SymbolSignatureCard | None:
|
|
94
|
+
if not isinstance(fn, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
95
|
+
return None
|
|
96
|
+
if not _PUBLIC(fn.name):
|
|
97
|
+
return None
|
|
98
|
+
name = fn.name if not class_qualifier else f"{class_qualifier}.{fn.name}"
|
|
99
|
+
inputs: list[tuple[str, str]] = []
|
|
100
|
+
for arg in fn.args.args:
|
|
101
|
+
if arg.arg in ("self", "cls"):
|
|
102
|
+
continue
|
|
103
|
+
inputs.append((arg.arg, _ann_text(arg.annotation)))
|
|
104
|
+
for arg in fn.args.kwonlyargs:
|
|
105
|
+
inputs.append((arg.arg, _ann_text(arg.annotation)))
|
|
106
|
+
output = _ann_text(fn.returns)
|
|
107
|
+
return SymbolSignatureCard(
|
|
108
|
+
name=name,
|
|
109
|
+
qualname=f"{module}.{name}",
|
|
110
|
+
module=module,
|
|
111
|
+
tier=tier,
|
|
112
|
+
domain=domain,
|
|
113
|
+
inputs=inputs,
|
|
114
|
+
output=output,
|
|
115
|
+
is_pure=_is_pure(fn),
|
|
116
|
+
docstring=_docstring_first_line(fn),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _harvest_one_file(py_file: Path, *, module: str, tier: str,
|
|
121
|
+
domain: str) -> list[SymbolSignatureCard]:
|
|
122
|
+
cards: list[SymbolSignatureCard] = []
|
|
123
|
+
try:
|
|
124
|
+
tree = ast.parse(py_file.read_text(encoding="utf-8"))
|
|
125
|
+
except (SyntaxError, OSError):
|
|
126
|
+
return cards
|
|
127
|
+
for node in tree.body:
|
|
128
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
129
|
+
card = _harvest_function(node, module=module, tier=tier, domain=domain)
|
|
130
|
+
if card:
|
|
131
|
+
cards.append(card)
|
|
132
|
+
elif isinstance(node, ast.ClassDef) and _PUBLIC(node.name):
|
|
133
|
+
for sub in node.body:
|
|
134
|
+
if isinstance(sub, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
135
|
+
card = _harvest_function(
|
|
136
|
+
sub, module=module, tier=tier, domain=domain,
|
|
137
|
+
class_qualifier=node.name,
|
|
138
|
+
)
|
|
139
|
+
if card:
|
|
140
|
+
cards.append(card)
|
|
141
|
+
return cards
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _flat_tier_for(py_file: Path) -> str:
|
|
145
|
+
"""Best-effort tier guess for repos without a*_ folders.
|
|
146
|
+
|
|
147
|
+
Uses pure naming heuristics from :func:`atomadic_forge.a1_at_functions.ingest.\
|
|
148
|
+
classify_tier` shape — but works on the file alone (good enough for the
|
|
149
|
+
composition graph; mis-tier doesn't break compatibility).
|
|
150
|
+
"""
|
|
151
|
+
name = py_file.stem.lower()
|
|
152
|
+
if any(t in name for t in ("constant", "schema", "manifest", "type", "enum")):
|
|
153
|
+
return "a0_qk_constants"
|
|
154
|
+
if any(t in name for t in ("util", "helper", "validator", "parse", "format")):
|
|
155
|
+
return "a1_at_functions"
|
|
156
|
+
if any(t in name for t in ("client", "store", "registry", "manager", "core")):
|
|
157
|
+
return "a2_mo_composites"
|
|
158
|
+
if any(t in name for t in ("feature", "service", "pipeline", "gate", "tool")):
|
|
159
|
+
return "a3_og_features"
|
|
160
|
+
if any(t in name for t in ("cli", "main", "runner", "cmd", "orchestrat")):
|
|
161
|
+
return "a4_sy_orchestration"
|
|
162
|
+
return "a1_at_functions"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def harvest_signatures(
|
|
166
|
+
src_root: Path,
|
|
167
|
+
package: str = "atomadic_forge",
|
|
168
|
+
tiers: Iterable[str] = _TIERS,
|
|
169
|
+
*,
|
|
170
|
+
skip_generated: bool = True,
|
|
171
|
+
) -> list[SymbolSignatureCard]:
|
|
172
|
+
"""Walk every ``a*/`` tier under ``src_root`` and harvest signatures.
|
|
173
|
+
|
|
174
|
+
Falls back to **flat mode** if no tier folder is found under
|
|
175
|
+
``src_root/package/`` — so emergent-scan also works on legacy / non-ASS-ADE
|
|
176
|
+
repositories (atomadic-v2-style flat layouts, the ``foo/bar.py`` shape, …).
|
|
177
|
+
In flat mode each file's tier is guessed from its name and the catalogue
|
|
178
|
+
contains every public callable under ``src_root``.
|
|
179
|
+
|
|
180
|
+
``skip_generated``: when True, skip files whose stems start with
|
|
181
|
+
``a*_source_*`` (verbatim assimilator output).
|
|
182
|
+
"""
|
|
183
|
+
src_root = Path(src_root)
|
|
184
|
+
cards: list[SymbolSignatureCard] = []
|
|
185
|
+
pkg_root = src_root / package
|
|
186
|
+
have_any_tier = pkg_root.exists() and any(
|
|
187
|
+
(pkg_root / t).exists() for t in tiers
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if have_any_tier:
|
|
191
|
+
for tier in tiers:
|
|
192
|
+
tdir = pkg_root / tier
|
|
193
|
+
if not tdir.exists():
|
|
194
|
+
continue
|
|
195
|
+
for py_file in sorted(tdir.glob("*.py")):
|
|
196
|
+
if py_file.name.startswith("_"):
|
|
197
|
+
continue
|
|
198
|
+
if skip_generated and py_file.stem.startswith((
|
|
199
|
+
"a0_source_", "a1_source_", "a2_source_",
|
|
200
|
+
"a3_source_", "a4_source_")):
|
|
201
|
+
continue
|
|
202
|
+
module = _module_for(py_file, src_root, package)
|
|
203
|
+
domain = _domain_of(py_file.stem)
|
|
204
|
+
cards.extend(_harvest_one_file(py_file, module=module,
|
|
205
|
+
tier=tier, domain=domain))
|
|
206
|
+
return cards
|
|
207
|
+
|
|
208
|
+
# ── Flat mode — walk every .py, infer tier from filename heuristics.
|
|
209
|
+
walk_root = src_root
|
|
210
|
+
package_for_module = package
|
|
211
|
+
if not pkg_root.exists():
|
|
212
|
+
# Caller passed a repo root, not a src layout. Use the repo as the
|
|
213
|
+
# walk root and fabricate a one-level package alias from the directory.
|
|
214
|
+
walk_root = src_root
|
|
215
|
+
package_for_module = src_root.name.replace("-", "_")
|
|
216
|
+
for py_file in sorted(walk_root.rglob("*.py")):
|
|
217
|
+
if py_file.name.startswith("_"):
|
|
218
|
+
continue
|
|
219
|
+
# Use parts RELATIVE to walk_root so a hidden parent like
|
|
220
|
+
# ``.pytest_basetemp`` doesn't accidentally exclude every test fixture.
|
|
221
|
+
try:
|
|
222
|
+
rel_parts = py_file.relative_to(walk_root).parts
|
|
223
|
+
except ValueError:
|
|
224
|
+
rel_parts = py_file.parts
|
|
225
|
+
if any(part.startswith((".", "__pycache__")) for part in rel_parts):
|
|
226
|
+
continue
|
|
227
|
+
if any(part in {"tests", "test", "build", "dist", ".venv", "venv"}
|
|
228
|
+
for part in rel_parts):
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
rel = py_file.relative_to(walk_root).with_suffix("")
|
|
232
|
+
module_parts = list(rel.parts)
|
|
233
|
+
if module_parts and module_parts[-1] == "__init__":
|
|
234
|
+
module_parts.pop()
|
|
235
|
+
module = ".".join([package_for_module, *module_parts]) if module_parts else package_for_module
|
|
236
|
+
except ValueError:
|
|
237
|
+
module = package_for_module
|
|
238
|
+
tier = _flat_tier_for(py_file)
|
|
239
|
+
domain = _domain_of(py_file.stem)
|
|
240
|
+
cards.extend(_harvest_one_file(py_file, module=module,
|
|
241
|
+
tier=tier, domain=domain))
|
|
242
|
+
return cards
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tier a1 — pure source synthesiser for an :class:`EmergentCandidateCard`.
|
|
2
|
+
|
|
3
|
+
Renders a Python module that wires the chain's components together as a new
|
|
4
|
+
feature. The generator is conservative: it imports each step by qualname,
|
|
5
|
+
calls them in order, and returns a typed result dict. Manual review is
|
|
6
|
+
expected — this is scaffolding, not a finished feature.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ..a0_qk_constants.emergent_types import EmergentCandidateCard
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _imports_for(chain_qualnames: list[str]) -> list[str]:
|
|
15
|
+
out: list[str] = []
|
|
16
|
+
seen: set[str] = set()
|
|
17
|
+
for q in chain_qualnames:
|
|
18
|
+
module, _, name = q.rpartition(".")
|
|
19
|
+
# qualnames may include a class-qualified method (``Mod.Class.method``).
|
|
20
|
+
if "." in name: # method case
|
|
21
|
+
continue
|
|
22
|
+
line = f"from {module} import {name}"
|
|
23
|
+
if line in seen:
|
|
24
|
+
continue
|
|
25
|
+
seen.add(line)
|
|
26
|
+
out.append(line)
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_emergent_feature(card: EmergentCandidateCard) -> str:
|
|
31
|
+
chain = card["chain"]
|
|
32
|
+
qualnames = chain["chain"]
|
|
33
|
+
bridges = chain["bridges"]
|
|
34
|
+
name = card["name"].replace("-", "_")
|
|
35
|
+
imports = _imports_for(qualnames)
|
|
36
|
+
summary = card["summary"].replace('"""', "'''")
|
|
37
|
+
novelty = "; ".join(card["novelty_signals"]) or "(none)"
|
|
38
|
+
score = card["score"]
|
|
39
|
+
breakdown = ", ".join(f"{k}={v:g}" for k, v in card["score_breakdown"].items())
|
|
40
|
+
|
|
41
|
+
lines: list[str] = [
|
|
42
|
+
'"""',
|
|
43
|
+
f"Auto-synthesized by ``atomadic-forge emergent synthesize {card['candidate_id']}``.",
|
|
44
|
+
"",
|
|
45
|
+
f"Suggested name: {card['name']}",
|
|
46
|
+
f"Suggested tier: {card['suggested_tier']}",
|
|
47
|
+
f"Score: {score:.0f} ({breakdown})",
|
|
48
|
+
f"Novelty: {novelty}",
|
|
49
|
+
"",
|
|
50
|
+
"Composition chain:",
|
|
51
|
+
]
|
|
52
|
+
for i, q in enumerate(qualnames):
|
|
53
|
+
lines.append(f" {i+1}. {q}")
|
|
54
|
+
if i < len(bridges):
|
|
55
|
+
lines.append(f" -- output: {bridges[i]} -->")
|
|
56
|
+
lines.extend([
|
|
57
|
+
'"""',
|
|
58
|
+
"",
|
|
59
|
+
"from __future__ import annotations",
|
|
60
|
+
"",
|
|
61
|
+
"from typing import Any",
|
|
62
|
+
"",
|
|
63
|
+
])
|
|
64
|
+
lines.extend(imports)
|
|
65
|
+
lines.append("")
|
|
66
|
+
lines.append(f"def run_{name}(seed: Any) -> dict[str, Any]:")
|
|
67
|
+
lines.append(f' """Wired composition for {card["name"]}.')
|
|
68
|
+
lines.append("")
|
|
69
|
+
lines.append(f" Summary: {summary}")
|
|
70
|
+
lines.append(' """')
|
|
71
|
+
lines.append(" trace: list[Any] = []")
|
|
72
|
+
lines.append(" value = seed")
|
|
73
|
+
for i, q in enumerate(qualnames):
|
|
74
|
+
_module, _, callable_name = q.rpartition(".")
|
|
75
|
+
if "." in callable_name:
|
|
76
|
+
# method case — fall back to a noop trace step rather than guessing
|
|
77
|
+
# the receiver. Manual wiring required.
|
|
78
|
+
lines.append(
|
|
79
|
+
f" # NOTE: step {i+1} is a method ({callable_name}); "
|
|
80
|
+
"wire the receiver manually."
|
|
81
|
+
)
|
|
82
|
+
lines.append(f" trace.append(({q!r}, 'unwired-method'))")
|
|
83
|
+
continue
|
|
84
|
+
lines.append(f" value = {callable_name}(value)")
|
|
85
|
+
lines.append(f" trace.append(({q!r}, value))")
|
|
86
|
+
lines.append(" return {'final': value, 'trace': trace}")
|
|
87
|
+
lines.append("")
|
|
88
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Tier a1 — pure planner for ``forge enforce``.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane A W6 deliverable. Consumes a wire report with W5
|
|
4
|
+
F-codes attached and emits a list of mechanical fix actions. Pure:
|
|
5
|
+
the planner reads the wire report and inspects file paths only — it
|
|
6
|
+
does NOT mutate anything. Application is a separate concern (see
|
|
7
|
+
``a3/forge_enforce.py``).
|
|
8
|
+
|
|
9
|
+
Action shape:
|
|
10
|
+
{
|
|
11
|
+
"f_code": "F0042",
|
|
12
|
+
"action": "move_file_up",
|
|
13
|
+
"src": "a1_at_functions/helper.py",
|
|
14
|
+
"dest": "a3_og_features/helper.py",
|
|
15
|
+
"violations": [<wire violation dict>, ...],
|
|
16
|
+
"auto_apply": true,
|
|
17
|
+
"warnings": [str, ...], # populated when the move has risks
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
The planner today implements one action: ``move_file_up`` for the six
|
|
21
|
+
canonical upward-import F-codes (F0041–F0046). F0040 (a0 importing
|
|
22
|
+
anything) and F0049 (unknown-shape) are emitted as ``review_manually``
|
|
23
|
+
actions with no auto_apply.
|
|
24
|
+
|
|
25
|
+
Pre-flight risks the planner detects (and surfaces as warnings, not
|
|
26
|
+
exceptions):
|
|
27
|
+
* the destination path already exists (would clobber)
|
|
28
|
+
* the source file has multiple distinct destination tiers across
|
|
29
|
+
its violations (ambiguous; safest is review)
|
|
30
|
+
* the source file is itself imported by other files in the package
|
|
31
|
+
(those callers' imports would need rewriting — out of scope for
|
|
32
|
+
W6 v1; W7 will own this)
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import ast
|
|
37
|
+
from collections import defaultdict
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import TypedDict
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EnforceAction(TypedDict, total=False):
|
|
43
|
+
"""One proposed mechanical fix. See module docstring."""
|
|
44
|
+
f_code: str
|
|
45
|
+
action: str # 'move_file_up' | 'review_manually'
|
|
46
|
+
src: str # repo-relative posix path
|
|
47
|
+
dest: str # repo-relative posix path (move target)
|
|
48
|
+
violations: list[dict]
|
|
49
|
+
auto_apply: bool
|
|
50
|
+
warnings: list[str]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _file_imports_in_package(
|
|
54
|
+
package_root: Path,
|
|
55
|
+
target_relpath: str,
|
|
56
|
+
) -> list[str]:
|
|
57
|
+
"""Return repo-relative paths of files that import the target.
|
|
58
|
+
|
|
59
|
+
Heuristic: looks for ``from <…>.target_module_name import``
|
|
60
|
+
or ``from <…>.target_module_name`` patterns where target_module_name
|
|
61
|
+
is the file stem of ``target_relpath``. Cheap regex-quality
|
|
62
|
+
proxy for a real symbol-resolution pass; the W6 acceptance is
|
|
63
|
+
"smoke covers 7 fix paths", not "full inbound-edge analysis".
|
|
64
|
+
|
|
65
|
+
Used purely to populate ``warnings`` when a move could break
|
|
66
|
+
inbound callers — the planner does not block on this signal.
|
|
67
|
+
"""
|
|
68
|
+
target = Path(target_relpath)
|
|
69
|
+
stem = target.stem
|
|
70
|
+
if not stem or stem.startswith("_"):
|
|
71
|
+
return []
|
|
72
|
+
out: list[str] = []
|
|
73
|
+
for f in package_root.rglob("*.py"):
|
|
74
|
+
if f.is_dir():
|
|
75
|
+
continue
|
|
76
|
+
rel = f.relative_to(package_root).as_posix()
|
|
77
|
+
if rel == target_relpath:
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
81
|
+
except OSError:
|
|
82
|
+
continue
|
|
83
|
+
if f"import {stem}" not in text and f".{stem}" not in text:
|
|
84
|
+
continue
|
|
85
|
+
# Confirm via AST so we don't trip on string literals.
|
|
86
|
+
try:
|
|
87
|
+
tree = ast.parse(text, filename=str(f))
|
|
88
|
+
except SyntaxError:
|
|
89
|
+
continue
|
|
90
|
+
for node in ast.walk(tree):
|
|
91
|
+
if isinstance(node, ast.ImportFrom):
|
|
92
|
+
module = (node.module or "").split(".")
|
|
93
|
+
if stem in module:
|
|
94
|
+
out.append(rel)
|
|
95
|
+
break
|
|
96
|
+
if any(alias.name == stem for alias in node.names):
|
|
97
|
+
out.append(rel)
|
|
98
|
+
break
|
|
99
|
+
elif isinstance(node, ast.Import):
|
|
100
|
+
if any(alias.name.endswith("." + stem) or alias.name == stem
|
|
101
|
+
for alias in node.names):
|
|
102
|
+
out.append(rel)
|
|
103
|
+
break
|
|
104
|
+
return sorted(set(out))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def plan_actions(
|
|
108
|
+
wire_report: dict,
|
|
109
|
+
*,
|
|
110
|
+
package_root: Path | None = None,
|
|
111
|
+
) -> list[EnforceAction]:
|
|
112
|
+
"""Group violations by file and emit one EnforceAction per file.
|
|
113
|
+
|
|
114
|
+
A file with multiple violations to the same higher tier yields a
|
|
115
|
+
single move action covering them all. A file with violations to
|
|
116
|
+
DIFFERENT tiers gets a review_manually action with warnings.
|
|
117
|
+
|
|
118
|
+
``package_root`` (optional): when provided, the planner inspects
|
|
119
|
+
inbound imports across the package and adds warnings; without it
|
|
120
|
+
the action's warnings list stays empty.
|
|
121
|
+
"""
|
|
122
|
+
by_file: dict[str, list[dict]] = defaultdict(list)
|
|
123
|
+
for v in wire_report.get("violations", []) or []:
|
|
124
|
+
by_file[v["file"]].append(v)
|
|
125
|
+
|
|
126
|
+
actions: list[EnforceAction] = []
|
|
127
|
+
for file_path, viols in sorted(by_file.items()):
|
|
128
|
+
f_codes = {v.get("f_code", "") for v in viols}
|
|
129
|
+
to_tiers = {v["to_tier"] for v in viols}
|
|
130
|
+
# F0040 / F0049: not auto-fixable.
|
|
131
|
+
if "F0040" in f_codes or "F0049" in f_codes or not f_codes:
|
|
132
|
+
actions.append(EnforceAction(
|
|
133
|
+
f_code=next(iter(f_codes)) if f_codes else "F0049",
|
|
134
|
+
action="review_manually",
|
|
135
|
+
src=file_path,
|
|
136
|
+
dest="",
|
|
137
|
+
violations=viols,
|
|
138
|
+
auto_apply=False,
|
|
139
|
+
warnings=[
|
|
140
|
+
"violation requires manual review (a0 special-case "
|
|
141
|
+
"or non-canonical tier shape)",
|
|
142
|
+
],
|
|
143
|
+
))
|
|
144
|
+
continue
|
|
145
|
+
# Multiple distinct destination tiers: ambiguous.
|
|
146
|
+
if len(to_tiers) > 1:
|
|
147
|
+
actions.append(EnforceAction(
|
|
148
|
+
f_code=sorted(f_codes)[0],
|
|
149
|
+
action="review_manually",
|
|
150
|
+
src=file_path,
|
|
151
|
+
dest="",
|
|
152
|
+
violations=viols,
|
|
153
|
+
auto_apply=False,
|
|
154
|
+
warnings=[
|
|
155
|
+
f"file imports from multiple higher tiers ({sorted(to_tiers)}); "
|
|
156
|
+
"no single mechanical destination — review manually",
|
|
157
|
+
],
|
|
158
|
+
))
|
|
159
|
+
continue
|
|
160
|
+
# Single canonical move.
|
|
161
|
+
target_tier = next(iter(to_tiers))
|
|
162
|
+
src = file_path
|
|
163
|
+
dest = f"{target_tier}/{Path(src).name}"
|
|
164
|
+
warnings: list[str] = []
|
|
165
|
+
if package_root is not None:
|
|
166
|
+
try:
|
|
167
|
+
inbound = _file_imports_in_package(package_root, src)
|
|
168
|
+
except OSError:
|
|
169
|
+
inbound = []
|
|
170
|
+
if inbound:
|
|
171
|
+
warnings.append(
|
|
172
|
+
f"{len(inbound)} other file(s) import this module — "
|
|
173
|
+
"their imports will need rewriting after the move: "
|
|
174
|
+
+ ", ".join(inbound[:5])
|
|
175
|
+
+ ("…" if len(inbound) > 5 else "")
|
|
176
|
+
)
|
|
177
|
+
dest_path = package_root / dest
|
|
178
|
+
if dest_path.exists():
|
|
179
|
+
warnings.append(
|
|
180
|
+
f"destination {dest} already exists; move would clobber"
|
|
181
|
+
)
|
|
182
|
+
actions.append(EnforceAction(
|
|
183
|
+
f_code=sorted(f_codes)[0],
|
|
184
|
+
action="move_file_up",
|
|
185
|
+
src=src,
|
|
186
|
+
dest=dest,
|
|
187
|
+
violations=viols,
|
|
188
|
+
auto_apply=not warnings,
|
|
189
|
+
warnings=warnings,
|
|
190
|
+
))
|
|
191
|
+
return actions
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def summarize_plan(actions: list[EnforceAction]) -> dict:
|
|
195
|
+
"""Reduce a plan to summary stats. Used by CLI human + JSON output."""
|
|
196
|
+
auto = sum(1 for a in actions if a.get("auto_apply"))
|
|
197
|
+
review = sum(1 for a in actions if not a.get("auto_apply"))
|
|
198
|
+
by_fcode: dict[str, int] = defaultdict(int)
|
|
199
|
+
for a in actions:
|
|
200
|
+
by_fcode[a.get("f_code", "F????")] += 1
|
|
201
|
+
return {
|
|
202
|
+
"schema_version": "atomadic-forge.enforce.plan/v1",
|
|
203
|
+
"action_count": len(actions),
|
|
204
|
+
"auto_apply_count": auto,
|
|
205
|
+
"review_count": review,
|
|
206
|
+
"by_fcode": dict(sorted(by_fcode.items())),
|
|
207
|
+
"actions": list(actions),
|
|
208
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tier a1 — named error-message templates with concrete recovery commands.
|
|
2
|
+
|
|
3
|
+
Audit pain point (Lane B4): every CLI error should end with a suggested
|
|
4
|
+
next step. ``GEMINI_API_KEY not set`` becomes ``GEMINI_API_KEY not set
|
|
5
|
+
— here are three ways to recover``. The point is: a developer who
|
|
6
|
+
just installed Forge should never have to alt-tab to Stack Overflow.
|
|
7
|
+
|
|
8
|
+
This module is pure: it returns formatted strings. The CLI layer
|
|
9
|
+
decides whether to ``typer.echo`` them, fold them into a
|
|
10
|
+
``typer.BadParameter``, or pipe them through structured JSON output.
|
|
11
|
+
|
|
12
|
+
Templates are looked up by stable name (e.g. ``provider_missing_key``)
|
|
13
|
+
so call sites can pass a dict of substitutions and we centralize wording
|
|
14
|
+
without touching every command. Adding a hint:
|
|
15
|
+
|
|
16
|
+
HINT_TEMPLATES["my_new_hint"] = '''… {var} …'''
|
|
17
|
+
format_hint("my_new_hint", var="x")
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
HINT_TEMPLATES: dict[str, str] = {
|
|
22
|
+
# ----- LLM provider errors -------------------------------------------
|
|
23
|
+
"provider_missing_key": (
|
|
24
|
+
"{provider!s} is selected but its API key is not set.\n"
|
|
25
|
+
"\n"
|
|
26
|
+
"Recovery options:\n"
|
|
27
|
+
" 1. Set the key: export {env_var!s}=<your-key>\n"
|
|
28
|
+
" 2. Get a free Gemini key: https://aistudio.google.com/apikey\n"
|
|
29
|
+
" 3. Use local Ollama: export FORGE_OLLAMA=1 && "
|
|
30
|
+
"ollama pull qwen2.5-coder:7b\n"
|
|
31
|
+
" 4. Use the offline stub: --provider stub (for tests / CI)\n"
|
|
32
|
+
"\n"
|
|
33
|
+
"Verify with: forge config test --provider {provider!s}"
|
|
34
|
+
),
|
|
35
|
+
# ----- Wire / tier errors --------------------------------------------
|
|
36
|
+
"no_tier_dirs": (
|
|
37
|
+
"No tier directories were found at {path!s}.\n"
|
|
38
|
+
"\n"
|
|
39
|
+
"Expected at least three of:\n"
|
|
40
|
+
" a0_qk_constants/ a1_at_functions/ a2_mo_composites/\n"
|
|
41
|
+
" a3_og_features/ a4_sy_orchestration/\n"
|
|
42
|
+
"\n"
|
|
43
|
+
"Did you forget to materialize? Try:\n"
|
|
44
|
+
" forge auto <source-repo> {path!s} --apply --package <name>\n"
|
|
45
|
+
),
|
|
46
|
+
"wire_fail_with_violations": (
|
|
47
|
+
"Wire scan found {count} upward-import violation(s).\n"
|
|
48
|
+
"\n"
|
|
49
|
+
"Recovery options:\n"
|
|
50
|
+
" 1. Get repair suggestions: forge wire {path!s} --suggest-repairs\n"
|
|
51
|
+
" 2. Get a JSON report: forge wire {path!s} --json > wire.json\n"
|
|
52
|
+
" 3. Gate this in CI: forge wire {path!s} --fail-on-violations\n"
|
|
53
|
+
),
|
|
54
|
+
# ----- Certify errors -------------------------------------------------
|
|
55
|
+
"certify_below_threshold": (
|
|
56
|
+
"Certify score {score}/100 is below the {threshold}/100 gate.\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"Inspect what failed and the cheapest path to recover:\n"
|
|
59
|
+
" forge certify {path!s} --json | python -m json.tool\n"
|
|
60
|
+
"\n"
|
|
61
|
+
"Common quick wins:\n"
|
|
62
|
+
" - documentation: add a README.md (free 25 points)\n"
|
|
63
|
+
" - tests: add tests/test_*.py (free 25 points)\n"
|
|
64
|
+
" - imports: forge wire {path!s} --suggest-repairs\n"
|
|
65
|
+
),
|
|
66
|
+
"fail_under_out_of_range": (
|
|
67
|
+
"--fail-under must be between 0 and 100. Got: {value!r}\n"
|
|
68
|
+
"\n"
|
|
69
|
+
"Common values:\n"
|
|
70
|
+
" --fail-under 75 # team-grade target\n"
|
|
71
|
+
" --fail-under 90 # release-grade target\n"
|
|
72
|
+
),
|
|
73
|
+
# ----- Manifest / file errors ----------------------------------------
|
|
74
|
+
"not_a_forge_manifest": (
|
|
75
|
+
"{path!s}: not a Forge JSON manifest.\n"
|
|
76
|
+
"\n"
|
|
77
|
+
"Expected a JSON object whose top-level `schema_version` field "
|
|
78
|
+
"starts with `atomadic-forge.` (e.g. `atomadic-forge.scout/v1`, "
|
|
79
|
+
"`atomadic-forge.wire/v1`, etc.).\n"
|
|
80
|
+
"\n"
|
|
81
|
+
"Forge manifests are written under .atomadic-forge/ when you run\n"
|
|
82
|
+
" forge auto / forge recon / forge cherry / forge finalize\n"
|
|
83
|
+
"with --apply or pass through ManifestStore."
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def format_hint(name: str, /, **fmt: object) -> str:
|
|
89
|
+
"""Return the named hint template with ``fmt`` substituted in.
|
|
90
|
+
|
|
91
|
+
Raises KeyError on unknown hint names so typos surface in tests
|
|
92
|
+
rather than at runtime in front of users.
|
|
93
|
+
"""
|
|
94
|
+
if name not in HINT_TEMPLATES:
|
|
95
|
+
raise KeyError(f"unknown hint template: {name!r}")
|
|
96
|
+
return HINT_TEMPLATES[name].format(**fmt)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def hint_lines(name: str, /, **fmt: object) -> list[str]:
|
|
100
|
+
"""Same as ``format_hint`` but returns the lines as a list.
|
|
101
|
+
|
|
102
|
+
Useful when a caller wants to prepend `` `` to indent the hint
|
|
103
|
+
inside a larger error block.
|
|
104
|
+
"""
|
|
105
|
+
return format_hint(name, **fmt).splitlines()
|