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,154 @@
|
|
|
1
|
+
"""Tier a3 — pipeline-agnostic Emergent Scan overlay.
|
|
2
|
+
|
|
3
|
+
Every ASS-ADE phase that walks a catalog (scout, cherry-pick, assimilate,
|
|
4
|
+
rebuild, enhance, evolve) can call :func:`emergent_overlay_for_path` to
|
|
5
|
+
surface composition candidates that single-symbol heuristics would miss.
|
|
6
|
+
|
|
7
|
+
The overlay is a thin adapter:
|
|
8
|
+
|
|
9
|
+
path → harvest_signatures → find_chains → rank_chains → top-N
|
|
10
|
+
→ return as a list of EmergentCandidateCard plus a small summary
|
|
11
|
+
|
|
12
|
+
Output is shaped so it slots into existing report dicts under an
|
|
13
|
+
``"emergent"`` key without disturbing the schema.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from ..a0_qk_constants.emergent_types import (
|
|
23
|
+
EmergentCandidateCard,
|
|
24
|
+
SymbolSignatureCard,
|
|
25
|
+
)
|
|
26
|
+
from ..a1_at_functions.emergent_compose import find_chains
|
|
27
|
+
from ..a1_at_functions.emergent_rank import rank_chains
|
|
28
|
+
from ..a1_at_functions.emergent_signature_extract import harvest_signatures
|
|
29
|
+
|
|
30
|
+
_CONFIG_DEFAULTS: dict[str, dict[str, Any]] = {
|
|
31
|
+
"scout": {"max_depth": 3, "top_n": 10, "domain_jump_required": True,
|
|
32
|
+
"require_pure": False, "score_floor": 30},
|
|
33
|
+
"cherry-pick": {"max_depth": 3, "top_n": 15, "domain_jump_required": True,
|
|
34
|
+
"require_pure": False, "score_floor": 0},
|
|
35
|
+
"assimilate": {"max_depth": 3, "top_n": 25, "domain_jump_required": True,
|
|
36
|
+
"require_pure": False, "score_floor": 30},
|
|
37
|
+
"rebuild": {"max_depth": 3, "top_n": 25, "domain_jump_required": True,
|
|
38
|
+
"require_pure": False, "score_floor": 30},
|
|
39
|
+
"enhance": {"max_depth": 4, "top_n": 20, "domain_jump_required": False,
|
|
40
|
+
"require_pure": False, "score_floor": 20},
|
|
41
|
+
"evolve": {"max_depth": 4, "top_n": 30, "domain_jump_required": True,
|
|
42
|
+
"require_pure": True, "score_floor": 50},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _detect_package(src_root: Path) -> str:
|
|
47
|
+
"""Find the package name under ``src_root/<package>/__init__.py``.
|
|
48
|
+
|
|
49
|
+
Returns the first directory containing an ``__init__.py``. Falls back
|
|
50
|
+
to ``"atomadic_forge"`` if nothing is found.
|
|
51
|
+
"""
|
|
52
|
+
for child in sorted(src_root.iterdir()) if src_root.exists() else []:
|
|
53
|
+
if child.is_dir() and (child / "__init__.py").exists():
|
|
54
|
+
return child.name
|
|
55
|
+
return "atomadic_forge"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _src_root_of(repo: Path) -> Path:
|
|
59
|
+
"""Return ``<repo>/src`` if it exists, else ``repo``."""
|
|
60
|
+
src = repo / "src"
|
|
61
|
+
return src if src.exists() else repo
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def emergent_overlay_for_path(
|
|
65
|
+
repo_root: str | Path,
|
|
66
|
+
*,
|
|
67
|
+
phase: str = "scout",
|
|
68
|
+
catalog: Iterable[SymbolSignatureCard] | None = None,
|
|
69
|
+
package: str | None = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""Run an emergent scan over a repo (or pre-harvested catalog) for a phase.
|
|
72
|
+
|
|
73
|
+
``catalog`` lets phases that already harvested signatures pass them in
|
|
74
|
+
instead of re-walking the tree (assimilate / rebuild build the catalog
|
|
75
|
+
naturally as part of their flow).
|
|
76
|
+
"""
|
|
77
|
+
cfg = dict(_CONFIG_DEFAULTS.get(phase, _CONFIG_DEFAULTS["scout"]))
|
|
78
|
+
repo = Path(repo_root).resolve()
|
|
79
|
+
src_root = _src_root_of(repo)
|
|
80
|
+
pkg = package or _detect_package(src_root)
|
|
81
|
+
|
|
82
|
+
if catalog is None:
|
|
83
|
+
catalog = harvest_signatures(src_root, pkg)
|
|
84
|
+
catalog_list: list[SymbolSignatureCard] = list(catalog)
|
|
85
|
+
|
|
86
|
+
chains = find_chains(
|
|
87
|
+
catalog_list,
|
|
88
|
+
max_depth=cfg["max_depth"],
|
|
89
|
+
max_chains=2_000,
|
|
90
|
+
require_pure=cfg["require_pure"],
|
|
91
|
+
domain_jump_required=cfg["domain_jump_required"],
|
|
92
|
+
)
|
|
93
|
+
candidates = rank_chains(chains, catalog=catalog_list, top_n=cfg["top_n"])
|
|
94
|
+
floor = cfg["score_floor"]
|
|
95
|
+
candidates = [c for c in candidates if c["score"] >= floor]
|
|
96
|
+
|
|
97
|
+
by_domain: dict[str, int] = {}
|
|
98
|
+
for c in catalog_list:
|
|
99
|
+
by_domain[c["domain"]] = by_domain.get(c["domain"], 0) + 1
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"schema_version": "atomadic-forge.emergent.overlay/v1",
|
|
103
|
+
"phase": phase,
|
|
104
|
+
"src_root": str(src_root),
|
|
105
|
+
"package": pkg,
|
|
106
|
+
"catalog_size": len(catalog_list),
|
|
107
|
+
"chain_count_considered": len(chains),
|
|
108
|
+
"candidates": candidates,
|
|
109
|
+
"config": cfg,
|
|
110
|
+
"domain_inventory": by_domain,
|
|
111
|
+
"summary_line": _summary_line(candidates, len(catalog_list), pkg, phase),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _summary_line(candidates: list[EmergentCandidateCard],
|
|
116
|
+
catalog_size: int, package: str, phase: str) -> str:
|
|
117
|
+
if not candidates:
|
|
118
|
+
return (
|
|
119
|
+
f"emergent[{phase}]: 0 candidates over {package} ({catalog_size} symbols) — "
|
|
120
|
+
"no novel composition above score floor"
|
|
121
|
+
)
|
|
122
|
+
top = candidates[0]
|
|
123
|
+
return (
|
|
124
|
+
f"emergent[{phase}]: {len(candidates)} candidate(s) over {package} "
|
|
125
|
+
f"({catalog_size} symbols); top={top['name']} score={top['score']:.0f}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def boost_cherry_pick_targets(
|
|
130
|
+
cherry_targets: list[dict[str, Any]],
|
|
131
|
+
*,
|
|
132
|
+
overlay: dict[str, Any],
|
|
133
|
+
boost: float = 0.1,
|
|
134
|
+
) -> list[dict[str, Any]]:
|
|
135
|
+
"""Re-score cherry-pick targets using emergent participation.
|
|
136
|
+
|
|
137
|
+
Each target gains ``boost`` per emergent chain it participates in.
|
|
138
|
+
The augmented list is returned; the input is not mutated.
|
|
139
|
+
"""
|
|
140
|
+
chains = [c["chain"]["chain"] for c in overlay.get("candidates", [])]
|
|
141
|
+
flat = {q for chain in chains for q in chain}
|
|
142
|
+
out: list[dict[str, Any]] = []
|
|
143
|
+
for t in cherry_targets:
|
|
144
|
+
sym = t.get("symbol") or {}
|
|
145
|
+
qname = f"{sym.get('module','')}.{sym.get('qualname','')}"
|
|
146
|
+
bumped = dict(t)
|
|
147
|
+
if qname in flat:
|
|
148
|
+
current = float(bumped.get("confidence", 0.0))
|
|
149
|
+
bumped["confidence"] = min(1.0, current + boost)
|
|
150
|
+
bumped.setdefault("reasons", []).append(
|
|
151
|
+
"participates in emergent composition chain"
|
|
152
|
+
)
|
|
153
|
+
out.append(bumped)
|
|
154
|
+
return out
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Tier a3 — apply the ``forge enforce`` plan.
|
|
2
|
+
|
|
3
|
+
Composes a1.enforce_planner + a1.wire_check + filesystem moves into
|
|
4
|
+
one orchestrator that:
|
|
5
|
+
1. runs scan_violations(suggest_repairs=True)
|
|
6
|
+
2. plans actions via enforce_planner.plan_actions
|
|
7
|
+
3. when --apply, executes each auto_apply action atomically:
|
|
8
|
+
* mkdir -p <dest_tier>/
|
|
9
|
+
* git-rename or shutil.move
|
|
10
|
+
* RE-RUN scan_violations to confirm violations actually
|
|
11
|
+
dropped; if violations rose, ROLL BACK the move and mark
|
|
12
|
+
the action 'rolled_back'.
|
|
13
|
+
|
|
14
|
+
a3 because: combines a1 helpers + filesystem state. No upward imports.
|
|
15
|
+
The CLI surface (``commands/enforce.py``) is the only thing that
|
|
16
|
+
depends on this module.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import shutil
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from ..a1_at_functions.enforce_planner import (
|
|
25
|
+
EnforceAction,
|
|
26
|
+
plan_actions,
|
|
27
|
+
summarize_plan,
|
|
28
|
+
)
|
|
29
|
+
from ..a1_at_functions.wire_check import scan_violations
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_enforce(
|
|
33
|
+
package_root: Path,
|
|
34
|
+
*,
|
|
35
|
+
apply: bool = False,
|
|
36
|
+
dry_run_only: bool = False,
|
|
37
|
+
) -> dict:
|
|
38
|
+
"""Plan (and optionally apply) mechanical fixes for wire violations.
|
|
39
|
+
|
|
40
|
+
Returns a result dict that always includes:
|
|
41
|
+
schema_version — 'atomadic-forge.enforce/v1'
|
|
42
|
+
apply — whether file moves were attempted
|
|
43
|
+
pre_violations — violation_count before the run
|
|
44
|
+
post_violations — violation_count after (== pre when apply=False)
|
|
45
|
+
plan — summarize_plan() output (action list + counts)
|
|
46
|
+
applied — list of {action, status} per attempted action
|
|
47
|
+
rollbacks — list of action srcs that were rolled back
|
|
48
|
+
|
|
49
|
+
Caller is responsible for any version-control commit/discard. The
|
|
50
|
+
function never deletes, only moves; rollbacks restore the original
|
|
51
|
+
location.
|
|
52
|
+
"""
|
|
53
|
+
package_root = Path(package_root).resolve()
|
|
54
|
+
pre = scan_violations(package_root, suggest_repairs=True)
|
|
55
|
+
actions = plan_actions(pre, package_root=package_root)
|
|
56
|
+
|
|
57
|
+
applied: list[dict[str, Any]] = []
|
|
58
|
+
rollbacks: list[str] = []
|
|
59
|
+
|
|
60
|
+
if apply and not dry_run_only:
|
|
61
|
+
for a in actions:
|
|
62
|
+
if not a.get("auto_apply"):
|
|
63
|
+
applied.append({"action": a, "status": "skipped"})
|
|
64
|
+
continue
|
|
65
|
+
try:
|
|
66
|
+
_apply_one(package_root, a)
|
|
67
|
+
except OSError as exc:
|
|
68
|
+
applied.append({"action": a, "status": "io_error",
|
|
69
|
+
"detail": str(exc)})
|
|
70
|
+
continue
|
|
71
|
+
# Confirm the fix actually reduced violations; otherwise roll back.
|
|
72
|
+
mid = scan_violations(package_root)
|
|
73
|
+
if mid["violation_count"] > pre["violation_count"]:
|
|
74
|
+
_rollback_one(package_root, a)
|
|
75
|
+
rollbacks.append(a["src"])
|
|
76
|
+
applied.append({"action": a, "status": "rolled_back"})
|
|
77
|
+
else:
|
|
78
|
+
applied.append({"action": a, "status": "applied"})
|
|
79
|
+
|
|
80
|
+
post = scan_violations(package_root) if apply else pre
|
|
81
|
+
return {
|
|
82
|
+
"schema_version": "atomadic-forge.enforce/v1",
|
|
83
|
+
"apply": apply,
|
|
84
|
+
"pre_violations": pre["violation_count"],
|
|
85
|
+
"post_violations": post["violation_count"],
|
|
86
|
+
"plan": summarize_plan(actions),
|
|
87
|
+
"applied": applied,
|
|
88
|
+
"rollbacks": rollbacks,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _apply_one(package_root: Path, action: EnforceAction) -> None:
|
|
93
|
+
src = package_root / action["src"]
|
|
94
|
+
dest = package_root / action["dest"]
|
|
95
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
if dest.exists():
|
|
97
|
+
raise OSError(f"destination already exists: {dest}")
|
|
98
|
+
shutil.move(str(src), str(dest))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _rollback_one(package_root: Path, action: EnforceAction) -> None:
|
|
102
|
+
"""Best-effort: move the file back if we still own its destination."""
|
|
103
|
+
src = package_root / action["src"]
|
|
104
|
+
dest = package_root / action["dest"]
|
|
105
|
+
if dest.exists() and not src.exists():
|
|
106
|
+
src.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
shutil.move(str(dest), str(src))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Tier a3 — recursive self-improvement loop.
|
|
2
|
+
|
|
3
|
+
``forge evolve`` runs N rounds of ``iterate``, each round using the GROWING
|
|
4
|
+
catalog as the seed for the next. Round 0 starts from the user's seed (or
|
|
5
|
+
empty); each accepted artifact joins the catalog; subsequent rounds see a
|
|
6
|
+
richer compositional context via emergent + reuse signals.
|
|
7
|
+
|
|
8
|
+
Honest scope: this is **narrow self-improvement within a defined search
|
|
9
|
+
space** — same shape as AlphaEvolve, AutoML-Zero, Voyager. It is NOT a
|
|
10
|
+
path to AGI. The catalog grows, compositions multiply, the LLM gets
|
|
11
|
+
richer feedback. Whether the OUTPUT improves is bounded by the underlying
|
|
12
|
+
LLM's quality and the realism of the certify/wire signals.
|
|
13
|
+
|
|
14
|
+
Convergence rules:
|
|
15
|
+
* Round halts when iterate returns ``converged=True`` AND no new symbols
|
|
16
|
+
were added vs. previous round (catalog stable).
|
|
17
|
+
* Hard cap: ``rounds`` parameter (default 5).
|
|
18
|
+
* Optional ``stop_on_regression``: halt if score drops between rounds.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import datetime as _dt
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from ..a0_qk_constants.gen_language import (
|
|
28
|
+
Language,
|
|
29
|
+
normalize_language,
|
|
30
|
+
pkg_root_for,
|
|
31
|
+
)
|
|
32
|
+
from ..a1_at_functions.evolution_log import append_evolve_run
|
|
33
|
+
from ..a1_at_functions.llm_client import LLMClient, resolve_default_client
|
|
34
|
+
from ..a1_at_functions.scout_walk import harvest_repo
|
|
35
|
+
from ..a2_mo_composites.manifest_store import ManifestStore
|
|
36
|
+
from .forge_loop import run_iterate
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_evolve(
|
|
40
|
+
intent: str,
|
|
41
|
+
*,
|
|
42
|
+
output: Path,
|
|
43
|
+
package: str = "evolved",
|
|
44
|
+
seed_repo: Path | None = None,
|
|
45
|
+
llm: LLMClient | None = None,
|
|
46
|
+
rounds: int = 3,
|
|
47
|
+
iterations_per_round: int = 4,
|
|
48
|
+
target_score: float = 75.0,
|
|
49
|
+
stop_on_regression: bool = False,
|
|
50
|
+
stagnation_threshold: int = 3,
|
|
51
|
+
language: Language | str = "python",
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""Recursive iterate.
|
|
54
|
+
|
|
55
|
+
The growing package itself becomes the seed for the next round, so the
|
|
56
|
+
LLM sees its own prior output as building blocks the next time around.
|
|
57
|
+
|
|
58
|
+
``stagnation_threshold`` halts evolve when the score AND symbol count
|
|
59
|
+
are unchanged for that many consecutive rounds — saves tokens when the
|
|
60
|
+
LLM is stuck re-emitting the same files. Set to 0 to disable.
|
|
61
|
+
|
|
62
|
+
``language`` is forwarded to ``run_iterate`` and determines the output
|
|
63
|
+
layout (``"python"`` → ``output/src/<package>/aN_*/*.py``,
|
|
64
|
+
``"javascript"`` → ``output/<package>/aN_*/*.js``).
|
|
65
|
+
"""
|
|
66
|
+
output = Path(output).resolve()
|
|
67
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
llm = llm or resolve_default_client()
|
|
69
|
+
lang: Language = normalize_language(language)
|
|
70
|
+
pkg_root = output / pkg_root_for(lang, package)
|
|
71
|
+
|
|
72
|
+
rounds_log: list[dict[str, Any]] = []
|
|
73
|
+
last_symbol_count = 0
|
|
74
|
+
last_score = 0.0
|
|
75
|
+
current_seed = Path(seed_repo) if seed_repo else None
|
|
76
|
+
stagnant_streak = 0
|
|
77
|
+
halt_reason: str | None = None
|
|
78
|
+
|
|
79
|
+
for round_idx in range(rounds):
|
|
80
|
+
report = run_iterate(
|
|
81
|
+
intent,
|
|
82
|
+
output=output,
|
|
83
|
+
package=package,
|
|
84
|
+
seed_repo=current_seed,
|
|
85
|
+
llm=llm,
|
|
86
|
+
max_iterations=iterations_per_round,
|
|
87
|
+
target_score=target_score,
|
|
88
|
+
apply=True,
|
|
89
|
+
language=lang,
|
|
90
|
+
)
|
|
91
|
+
# Re-scout the generated package to grow the catalog.
|
|
92
|
+
catalog = harvest_repo(pkg_root) if pkg_root.exists() else {"symbols": [], "symbol_count": 0}
|
|
93
|
+
symbol_count = catalog["symbol_count"]
|
|
94
|
+
score = report.get("final_certify", {}).get("score", 0)
|
|
95
|
+
delta = symbol_count - last_symbol_count
|
|
96
|
+
|
|
97
|
+
round_record = {
|
|
98
|
+
"round": round_idx,
|
|
99
|
+
"iterations": report.get("iterations", 0),
|
|
100
|
+
"files_written": report.get("files_written_total", 0),
|
|
101
|
+
"symbol_count": symbol_count,
|
|
102
|
+
"delta_symbols": delta,
|
|
103
|
+
"score": score,
|
|
104
|
+
"delta_score": round(score - last_score, 1),
|
|
105
|
+
"wire_verdict": report.get("final_wire", {}).get("verdict", "?"),
|
|
106
|
+
"converged": report.get("converged", False),
|
|
107
|
+
}
|
|
108
|
+
rounds_log.append(round_record)
|
|
109
|
+
|
|
110
|
+
# Stagnation tracking — flat score AND flat catalog vs. previous round.
|
|
111
|
+
if round_idx > 0 and delta == 0 and abs(round_record["delta_score"]) < 0.5:
|
|
112
|
+
stagnant_streak += 1
|
|
113
|
+
else:
|
|
114
|
+
stagnant_streak = 0
|
|
115
|
+
round_record["stagnant_streak"] = stagnant_streak
|
|
116
|
+
|
|
117
|
+
# Convergence checks (in priority order)
|
|
118
|
+
if report.get("converged") and delta == 0:
|
|
119
|
+
halt_reason = "converged"
|
|
120
|
+
break
|
|
121
|
+
if stop_on_regression and round_idx > 0 and round_record["delta_score"] < 0:
|
|
122
|
+
halt_reason = "score regression"
|
|
123
|
+
round_record["halt_reason"] = halt_reason
|
|
124
|
+
break
|
|
125
|
+
if stagnation_threshold and stagnant_streak >= stagnation_threshold:
|
|
126
|
+
halt_reason = "stagnation"
|
|
127
|
+
round_record["halt_reason"] = halt_reason
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
# The growing package becomes the seed for the next round.
|
|
131
|
+
current_seed = pkg_root if pkg_root.exists() else current_seed
|
|
132
|
+
last_symbol_count = symbol_count
|
|
133
|
+
last_score = score
|
|
134
|
+
|
|
135
|
+
final_score = rounds_log[-1]["score"] if rounds_log else 0
|
|
136
|
+
final_symbols = rounds_log[-1]["symbol_count"] if rounds_log else 0
|
|
137
|
+
|
|
138
|
+
out: dict[str, Any] = {
|
|
139
|
+
"schema_version": "atomadic-forge.evolve/v1",
|
|
140
|
+
"intent": intent,
|
|
141
|
+
"package": package,
|
|
142
|
+
"language": lang,
|
|
143
|
+
"output_root": str(output),
|
|
144
|
+
"llm": llm.name,
|
|
145
|
+
"rounds": rounds_log,
|
|
146
|
+
"rounds_completed": len(rounds_log),
|
|
147
|
+
"rounds_requested": rounds,
|
|
148
|
+
"final_score": final_score,
|
|
149
|
+
"final_symbol_count": final_symbols,
|
|
150
|
+
"score_trajectory": [r["score"] for r in rounds_log],
|
|
151
|
+
"symbol_trajectory": [r["symbol_count"] for r in rounds_log],
|
|
152
|
+
"halt_reason": halt_reason or "rounds_exhausted",
|
|
153
|
+
"converged": rounds_log[-1].get("converged", False) if rounds_log else False,
|
|
154
|
+
"generated_at_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
155
|
+
}
|
|
156
|
+
ManifestStore(output).save("evolve", out)
|
|
157
|
+
|
|
158
|
+
# Auto-append to the evolution log so every run is documented permanently.
|
|
159
|
+
try:
|
|
160
|
+
append_evolve_run(
|
|
161
|
+
project_root=output,
|
|
162
|
+
package=package,
|
|
163
|
+
intent=intent,
|
|
164
|
+
llm_name=llm.name,
|
|
165
|
+
rounds_completed=len(rounds_log),
|
|
166
|
+
score_trajectory=out["score_trajectory"],
|
|
167
|
+
final_score=final_score,
|
|
168
|
+
converged=out["converged"],
|
|
169
|
+
halt_reason=out["halt_reason"],
|
|
170
|
+
extra={"final_symbol_count": final_symbols,
|
|
171
|
+
"rounds_requested": rounds},
|
|
172
|
+
)
|
|
173
|
+
except Exception: # noqa: BLE001 — logging must never break the run
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
return out
|