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,158 @@
|
|
|
1
|
+
"""Tier a1 — pure stub-body detector.
|
|
2
|
+
|
|
3
|
+
When an LLM emits a file with ``pass``, ``raise NotImplementedError``, or
|
|
4
|
+
a ``# TODO`` / ``# Implement me!`` placeholder, the file *looks* like a
|
|
5
|
+
real symbol but won't run. We must catch this so the certify score can't
|
|
6
|
+
be gamed by emitting empty shells.
|
|
7
|
+
|
|
8
|
+
The detector inspects function/class bodies via AST; placeholder comments
|
|
9
|
+
are matched via Python tokenization so docstrings and prompt text are not
|
|
10
|
+
mistaken for code stubs. Returns one record per stub-shaped symbol with
|
|
11
|
+
the file path, qualname, and the specific stub kind detected.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import io
|
|
18
|
+
import re
|
|
19
|
+
import tokenize
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Literal, TypedDict
|
|
23
|
+
|
|
24
|
+
StubKind = Literal["pass_only", "not_implemented", "comment_only",
|
|
25
|
+
"todo_marker"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StubFinding(TypedDict):
|
|
29
|
+
file: str # repo-relative
|
|
30
|
+
qualname: str
|
|
31
|
+
lineno: int
|
|
32
|
+
kind: StubKind
|
|
33
|
+
excerpt: str # one line, for prompt feedback
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_TODO_RE = re.compile(
|
|
37
|
+
r"#\s*(TODO|FIXME|XXX|HACK|implement\s+me|implement\s+this|"
|
|
38
|
+
r"add\s+actual|fill\s+this)\b",
|
|
39
|
+
re.IGNORECASE,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _function_body_is_pass_only(fn: ast.AST) -> bool:
|
|
44
|
+
if not isinstance(fn, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
45
|
+
return False
|
|
46
|
+
body = list(fn.body)
|
|
47
|
+
# Strip docstring.
|
|
48
|
+
if (body and isinstance(body[0], ast.Expr)
|
|
49
|
+
and isinstance(body[0].value, ast.Constant)
|
|
50
|
+
and isinstance(body[0].value.value, str)):
|
|
51
|
+
body = body[1:]
|
|
52
|
+
if not body:
|
|
53
|
+
return True
|
|
54
|
+
if len(body) == 1 and isinstance(body[0], ast.Pass):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _function_body_raises_not_implemented(fn: ast.AST) -> bool:
|
|
60
|
+
if not isinstance(fn, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
61
|
+
return False
|
|
62
|
+
for node in ast.walk(fn):
|
|
63
|
+
if isinstance(node, ast.Raise) and node.exc is not None:
|
|
64
|
+
target = node.exc
|
|
65
|
+
if isinstance(target, ast.Call):
|
|
66
|
+
target = target.func
|
|
67
|
+
if isinstance(target, ast.Name) and target.id == "NotImplementedError":
|
|
68
|
+
return True
|
|
69
|
+
if isinstance(target, ast.Attribute) and target.attr == "NotImplementedError":
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _todo_comment_lines(src: str) -> list[tuple[int, str]]:
|
|
75
|
+
"""Return placeholder comments only, excluding docstrings and strings."""
|
|
76
|
+
out: list[tuple[int, str]] = []
|
|
77
|
+
try:
|
|
78
|
+
tokens = tokenize.generate_tokens(io.StringIO(src).readline)
|
|
79
|
+
for tok in tokens:
|
|
80
|
+
if tok.type == tokenize.COMMENT and _TODO_RE.search(tok.string):
|
|
81
|
+
out.append((tok.start[0], tok.line.rstrip()))
|
|
82
|
+
except tokenize.TokenError:
|
|
83
|
+
return []
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def detect_stubs_in_file(path: Path, *, repo_root: Path | None = None
|
|
88
|
+
) -> list[StubFinding]:
|
|
89
|
+
"""Return a finding per stub-shaped function/class in ``path``.
|
|
90
|
+
|
|
91
|
+
Only reports public callables (no leading underscore) so we don't flag
|
|
92
|
+
private helpers that legitimately ``pass``.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
src = path.read_text(encoding="utf-8", errors="replace")
|
|
96
|
+
tree = ast.parse(src, filename=str(path))
|
|
97
|
+
except (SyntaxError, OSError):
|
|
98
|
+
return []
|
|
99
|
+
rel = (path.relative_to(repo_root).as_posix()
|
|
100
|
+
if repo_root else path.as_posix())
|
|
101
|
+
src_lines = src.splitlines()
|
|
102
|
+
out: list[StubFinding] = []
|
|
103
|
+
|
|
104
|
+
todo_lines = _todo_comment_lines(src)
|
|
105
|
+
|
|
106
|
+
def add(qualname: str, lineno: int, kind: StubKind, excerpt: str) -> None:
|
|
107
|
+
out.append(StubFinding(file=rel, qualname=qualname, lineno=lineno,
|
|
108
|
+
kind=kind, excerpt=excerpt[:120]))
|
|
109
|
+
|
|
110
|
+
def visit(node: ast.AST, prefix: str = "") -> None:
|
|
111
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
112
|
+
if node.name.startswith("_") and node.name != "__init__":
|
|
113
|
+
return
|
|
114
|
+
qual = f"{prefix}{node.name}" if prefix else node.name
|
|
115
|
+
line = src_lines[node.lineno - 1] if 0 < node.lineno <= len(src_lines) else ""
|
|
116
|
+
if _function_body_is_pass_only(node):
|
|
117
|
+
add(qual, node.lineno, "pass_only", line)
|
|
118
|
+
elif _function_body_raises_not_implemented(node):
|
|
119
|
+
add(qual, node.lineno, "not_implemented", line)
|
|
120
|
+
return
|
|
121
|
+
if isinstance(node, ast.ClassDef):
|
|
122
|
+
for sub in node.body:
|
|
123
|
+
visit(sub, prefix=f"{node.name}.")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
for node in tree.body:
|
|
127
|
+
visit(node)
|
|
128
|
+
|
|
129
|
+
# File-level TODO/FIXME markers — surface even when the body otherwise looks fine.
|
|
130
|
+
for ln, line in todo_lines:
|
|
131
|
+
if not any(f["lineno"] == ln for f in out):
|
|
132
|
+
kind: StubKind = "todo_marker"
|
|
133
|
+
add(f"<line {ln}>", ln, kind, line.strip())
|
|
134
|
+
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def detect_stubs(*, package_root: Path,
|
|
139
|
+
only_emitted: Iterable[Path] | None = None
|
|
140
|
+
) -> list[StubFinding]:
|
|
141
|
+
"""Walk a tier package (or a specific list of files) and aggregate stubs."""
|
|
142
|
+
package_root = Path(package_root)
|
|
143
|
+
files: Iterable[Path]
|
|
144
|
+
if only_emitted is not None:
|
|
145
|
+
files = [Path(p) for p in only_emitted]
|
|
146
|
+
else:
|
|
147
|
+
files = package_root.rglob("*.py")
|
|
148
|
+
out: list[StubFinding] = []
|
|
149
|
+
for f in files:
|
|
150
|
+
if not f.exists() or "__pycache__" in f.parts or f.name == "__init__.py":
|
|
151
|
+
continue
|
|
152
|
+
out.extend(detect_stubs_in_file(f, repo_root=package_root))
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def stub_penalty(findings: list[StubFinding]) -> int:
|
|
157
|
+
"""Cap-aware penalty for the certify score: 8 points per stub, max 40."""
|
|
158
|
+
return min(40, 8 * len(findings))
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Tier a1 — pure synergy detector.
|
|
2
|
+
|
|
3
|
+
Given a list of :class:`FeatureSurfaceCard`, produce ranked
|
|
4
|
+
:class:`SynergyCandidateCard`s.
|
|
5
|
+
|
|
6
|
+
Five signals (each contributes to the score):
|
|
7
|
+
|
|
8
|
+
* ``json_artifact`` — A emits ``--json-out``, B accepts a path/file arg.
|
|
9
|
+
* ``in_memory_pipe`` — vocabulary from A's outputs overlaps B's inputs.
|
|
10
|
+
* ``shared_schema`` — both reference the same ``atomadic-forge.<x>/v<n>`` schema.
|
|
11
|
+
* ``shared_vocabulary`` — Jaccard(vocab_A, vocab_B) ≥ threshold.
|
|
12
|
+
* ``phase_omission`` — A.phase_hint == "emit" and B.phase_hint == "ingest"
|
|
13
|
+
(or analogous predecessor → successor).
|
|
14
|
+
|
|
15
|
+
The detector ALWAYS proposes pairs (A, B) with A ≠ B and returns them
|
|
16
|
+
sorted by descending score.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
|
|
24
|
+
from ..a0_qk_constants.synergy_types import (
|
|
25
|
+
FeatureSurfaceCard,
|
|
26
|
+
SynergyCandidateCard,
|
|
27
|
+
SynergyKind,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_PHASE_FLOW = [
|
|
31
|
+
"recon", "ingest", "plan", "materialize", "certify", "emit", "register",
|
|
32
|
+
]
|
|
33
|
+
_PHASE_NEXT = {p: _PHASE_FLOW[i + 1] for i, p in enumerate(_PHASE_FLOW[:-1])}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _candidate_id(producer: str, consumer: str, kind: str) -> str:
|
|
37
|
+
h = hashlib.sha256(f"{producer}->{consumer}|{kind}".encode()).hexdigest()
|
|
38
|
+
return f"syn-{h[:8]}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _jaccard(a: set[str], b: set[str]) -> float:
|
|
42
|
+
if not a and not b:
|
|
43
|
+
return 0.0
|
|
44
|
+
return len(a & b) / max(1, len(a | b))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _detect_pair(a: FeatureSurfaceCard,
|
|
48
|
+
b: FeatureSurfaceCard) -> list[SynergyCandidateCard]:
|
|
49
|
+
out: list[SynergyCandidateCard] = []
|
|
50
|
+
|
|
51
|
+
# 1. JSON-artifact handoff: producer outputs a JSON, consumer takes a file.
|
|
52
|
+
json_out_signals = [o for o in a["outputs"] if "json" in o or "out" in o]
|
|
53
|
+
file_in_signals = [f for f in b["input_files"] if f]
|
|
54
|
+
if json_out_signals and file_in_signals:
|
|
55
|
+
score_breakdown = {
|
|
56
|
+
"json_handoff": 35,
|
|
57
|
+
"phase_step": 15 if _PHASE_NEXT.get(a["phase_hint"]) == b["phase_hint"] else 0,
|
|
58
|
+
"vocab_overlap": min(20, int(20 * _jaccard(set(a["vocabulary"]),
|
|
59
|
+
set(b["vocabulary"])))),
|
|
60
|
+
}
|
|
61
|
+
score = sum(score_breakdown.values())
|
|
62
|
+
if score >= 30:
|
|
63
|
+
out.append(SynergyCandidateCard(
|
|
64
|
+
candidate_id=_candidate_id(a["name"], b["name"], "json_artifact"),
|
|
65
|
+
producer=a["name"],
|
|
66
|
+
consumer=b["name"],
|
|
67
|
+
kind="json_artifact",
|
|
68
|
+
why=[
|
|
69
|
+
f"{a['name']} emits {json_out_signals[0]}",
|
|
70
|
+
f"{b['name']} accepts {file_in_signals[0]}",
|
|
71
|
+
],
|
|
72
|
+
score=float(score),
|
|
73
|
+
score_breakdown=score_breakdown,
|
|
74
|
+
proposed_adapter_name=f"{a['name']}-then-{b['name']}",
|
|
75
|
+
proposed_summary=(
|
|
76
|
+
f"Run {a['name']} to emit a JSON artifact, then feed it to "
|
|
77
|
+
f"{b['name']} for the next phase."
|
|
78
|
+
),
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
# 2. Shared schema reference — both source files mention the same
|
|
82
|
+
# atomadic-forge.<x>/v<n> schema string. Strong signal regardless of phase.
|
|
83
|
+
shared_schemas = sorted(set(a["schemas"]) & set(b["schemas"]))
|
|
84
|
+
if shared_schemas:
|
|
85
|
+
score_breakdown = {"schema_match": 50, "schemas_count": min(10, 5 * len(shared_schemas))}
|
|
86
|
+
score = sum(score_breakdown.values())
|
|
87
|
+
out.append(SynergyCandidateCard(
|
|
88
|
+
candidate_id=_candidate_id(a["name"], b["name"], "shared_schema"),
|
|
89
|
+
producer=a["name"],
|
|
90
|
+
consumer=b["name"],
|
|
91
|
+
kind="shared_schema",
|
|
92
|
+
why=[f"both reference schema(s) {', '.join(shared_schemas)}"],
|
|
93
|
+
score=float(score),
|
|
94
|
+
score_breakdown=score_breakdown,
|
|
95
|
+
proposed_adapter_name=f"{a['name']}-feeds-{b['name']}",
|
|
96
|
+
proposed_summary=(
|
|
97
|
+
f"{a['name']} and {b['name']} share schema {shared_schemas[0]} — "
|
|
98
|
+
"wire them as a pipeline rather than running each by hand."
|
|
99
|
+
),
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# 3. Phase-omission: producer is at step N, consumer at step N+1, but
|
|
103
|
+
# neither lists the other in its vocabulary (i.e. no existing mention
|
|
104
|
+
# of the partner).
|
|
105
|
+
if (_PHASE_NEXT.get(a["phase_hint"]) == b["phase_hint"]
|
|
106
|
+
and a["name"].lower() not in set(b["vocabulary"])
|
|
107
|
+
and b["name"].lower() not in set(a["vocabulary"])):
|
|
108
|
+
score_breakdown = {"phase_step": 30, "missing_mention": 20}
|
|
109
|
+
score = sum(score_breakdown.values())
|
|
110
|
+
out.append(SynergyCandidateCard(
|
|
111
|
+
candidate_id=_candidate_id(a["name"], b["name"], "phase_omission"),
|
|
112
|
+
producer=a["name"],
|
|
113
|
+
consumer=b["name"],
|
|
114
|
+
kind="phase_omission",
|
|
115
|
+
why=[
|
|
116
|
+
f"{a['name']} is at phase '{a['phase_hint']}', "
|
|
117
|
+
f"{b['name']} at '{b['phase_hint']}' — natural successor",
|
|
118
|
+
"neither references the other in help/docstring",
|
|
119
|
+
],
|
|
120
|
+
score=float(score),
|
|
121
|
+
score_breakdown=score_breakdown,
|
|
122
|
+
proposed_adapter_name=f"{a['name']}-into-{b['name']}",
|
|
123
|
+
proposed_summary=(
|
|
124
|
+
f"{a['name']} produces {a['phase_hint']}-phase output that "
|
|
125
|
+
f"{b['name']} could consume — currently un-piped."
|
|
126
|
+
),
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
# 4. High-overlap vocabulary (subject-matter pairs working in same area).
|
|
130
|
+
overlap = _jaccard(set(a["vocabulary"]), set(b["vocabulary"]))
|
|
131
|
+
if overlap >= 0.4:
|
|
132
|
+
score_breakdown = {"vocab_overlap": int(40 * overlap)}
|
|
133
|
+
score = sum(score_breakdown.values())
|
|
134
|
+
if score >= 25:
|
|
135
|
+
shared = sorted(set(a["vocabulary"]) & set(b["vocabulary"]))[:6]
|
|
136
|
+
out.append(SynergyCandidateCard(
|
|
137
|
+
candidate_id=_candidate_id(a["name"], b["name"], "shared_vocabulary"),
|
|
138
|
+
producer=a["name"],
|
|
139
|
+
consumer=b["name"],
|
|
140
|
+
kind="shared_vocabulary",
|
|
141
|
+
why=[f"shared vocabulary: {', '.join(shared)}"],
|
|
142
|
+
score=float(score),
|
|
143
|
+
score_breakdown=score_breakdown,
|
|
144
|
+
proposed_adapter_name=f"{a['name']}-with-{b['name']}",
|
|
145
|
+
proposed_summary=(
|
|
146
|
+
f"{a['name']} and {b['name']} talk about the same domain "
|
|
147
|
+
f"({', '.join(shared[:3])}) but aren't wired together."
|
|
148
|
+
),
|
|
149
|
+
))
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def detect_synergies(features: Iterable[FeatureSurfaceCard]) -> list[SynergyCandidateCard]:
|
|
154
|
+
feats = list(features)
|
|
155
|
+
out: list[SynergyCandidateCard] = []
|
|
156
|
+
seen: set[tuple[str, str, SynergyKind]] = set()
|
|
157
|
+
for i, a in enumerate(feats):
|
|
158
|
+
for b in feats[i + 1:] + feats[:i]:
|
|
159
|
+
for cand in _detect_pair(a, b):
|
|
160
|
+
key = (cand["producer"], cand["consumer"], cand["kind"])
|
|
161
|
+
if key in seen:
|
|
162
|
+
continue
|
|
163
|
+
seen.add(key)
|
|
164
|
+
out.append(cand)
|
|
165
|
+
out.sort(key=lambda c: c["score"], reverse=True)
|
|
166
|
+
return out
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Tier a1 — pure source synthesiser for a Synergy adapter.
|
|
2
|
+
|
|
3
|
+
Given a :class:`SynergyCandidateCard`, render a tiny ``commands/<name>.py``
|
|
4
|
+
Typer module that wires producer to consumer. Four templates cover the
|
|
5
|
+
detected synergy kinds:
|
|
6
|
+
|
|
7
|
+
* ``json_artifact`` — producer emits ``--json-out PATH`` then consumer
|
|
8
|
+
reads PATH as its first positional argument.
|
|
9
|
+
* ``in_memory_pipe`` — both modules importable; run the producer's main
|
|
10
|
+
in-process, capture its return, hand to consumer.
|
|
11
|
+
* ``phase_omission`` — producer + consumer in the natural phase order
|
|
12
|
+
with a clear ``# review: arg alignment`` marker
|
|
13
|
+
since signatures may not match.
|
|
14
|
+
* ``shared_schema`` — like ``json_artifact`` but the adapter validates
|
|
15
|
+
the JSON against the shared schema before piping.
|
|
16
|
+
|
|
17
|
+
The right template is picked from ``card["kind"]``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from ..a0_qk_constants.synergy_types import SynergyCandidateCard
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_synergy_adapter(card: SynergyCandidateCard) -> str:
|
|
26
|
+
"""Dispatch to the right per-kind renderer."""
|
|
27
|
+
renderers = {
|
|
28
|
+
"json_artifact": _render_json_artifact,
|
|
29
|
+
"in_memory_pipe": _render_in_memory_pipe,
|
|
30
|
+
"phase_omission": _render_phase_omission,
|
|
31
|
+
"shared_schema": _render_shared_schema,
|
|
32
|
+
"shared_vocabulary": _render_phase_omission, # treat like a soft chain
|
|
33
|
+
}
|
|
34
|
+
return renderers.get(card["kind"], _render_json_artifact)(card)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _header_lines(card: SynergyCandidateCard) -> list[str]:
|
|
38
|
+
name = card["proposed_adapter_name"]
|
|
39
|
+
safe_help = card["proposed_summary"].replace('"""', "'''")
|
|
40
|
+
lines = [
|
|
41
|
+
'"""',
|
|
42
|
+
f"Auto-synthesized synergy adapter ({card['candidate_id']}).",
|
|
43
|
+
"",
|
|
44
|
+
f"Producer: {card['producer']}",
|
|
45
|
+
f"Consumer: {card['consumer']}",
|
|
46
|
+
f"Kind: {card['kind']}",
|
|
47
|
+
f"Score: {card['score']:.0f}",
|
|
48
|
+
"",
|
|
49
|
+
"Why this synergy was detected:",
|
|
50
|
+
]
|
|
51
|
+
for w in card["why"]:
|
|
52
|
+
lines.append(f" - {w}")
|
|
53
|
+
lines.extend([
|
|
54
|
+
"",
|
|
55
|
+
"Re-emit with ``atomadic-forge synergy implement <id>`` after surfaces change.",
|
|
56
|
+
'"""',
|
|
57
|
+
"",
|
|
58
|
+
"from __future__ import annotations",
|
|
59
|
+
"",
|
|
60
|
+
"import json",
|
|
61
|
+
"import subprocess",
|
|
62
|
+
"import sys",
|
|
63
|
+
"import tempfile",
|
|
64
|
+
"from pathlib import Path",
|
|
65
|
+
"from typing import Annotated",
|
|
66
|
+
"",
|
|
67
|
+
"import typer",
|
|
68
|
+
"",
|
|
69
|
+
f"COMMAND_NAME = {name!r}",
|
|
70
|
+
f"COMMAND_HELP = {safe_help!r}",
|
|
71
|
+
"",
|
|
72
|
+
f'app = typer.Typer(no_args_is_help=False, help={safe_help!r})',
|
|
73
|
+
"",
|
|
74
|
+
])
|
|
75
|
+
return lines
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _render_json_artifact(card: SynergyCandidateCard) -> str:
|
|
79
|
+
body = _header_lines(card)
|
|
80
|
+
body.extend([
|
|
81
|
+
"@app.callback(invoke_without_command=True)",
|
|
82
|
+
"def run(",
|
|
83
|
+
" ctx: typer.Context,",
|
|
84
|
+
" producer_args: Annotated[list[str] | None, typer.Argument(",
|
|
85
|
+
" help='Args forwarded to the producer command.')] = None,",
|
|
86
|
+
") -> None:",
|
|
87
|
+
f' """Run {card["producer"]} → capture artifact → feed to {card["consumer"]}."""',
|
|
88
|
+
" if ctx.invoked_subcommand is not None:",
|
|
89
|
+
" return",
|
|
90
|
+
" producer_args = producer_args or []",
|
|
91
|
+
" with tempfile.TemporaryDirectory(prefix='synergy-') as tmp:",
|
|
92
|
+
" artifact = Path(tmp) / 'producer.json'",
|
|
93
|
+
" cmd_a = [sys.executable, '-m',",
|
|
94
|
+
" 'atomadic_forge.a4_sy_orchestration.unified_cli',",
|
|
95
|
+
f" {card['producer']!r}, *producer_args,",
|
|
96
|
+
" '--json-out', str(artifact)]",
|
|
97
|
+
" rc = subprocess.run(cmd_a, capture_output=False).returncode",
|
|
98
|
+
" if rc != 0:",
|
|
99
|
+
" typer.secho(f'producer exited {rc}', fg='red', err=True)",
|
|
100
|
+
" raise typer.Exit(rc)",
|
|
101
|
+
" cmd_b = [sys.executable, '-m',",
|
|
102
|
+
" 'atomadic_forge.a4_sy_orchestration.unified_cli',",
|
|
103
|
+
f" {card['consumer']!r}, str(artifact)]",
|
|
104
|
+
" rc = subprocess.run(cmd_b, capture_output=False).returncode",
|
|
105
|
+
" if rc != 0:",
|
|
106
|
+
" typer.secho(f'consumer exited {rc}', fg='red', err=True)",
|
|
107
|
+
" raise typer.Exit(rc)",
|
|
108
|
+
" try:",
|
|
109
|
+
" data = json.loads(artifact.read_text(encoding='utf-8'))",
|
|
110
|
+
" except (OSError, json.JSONDecodeError):",
|
|
111
|
+
" data = None",
|
|
112
|
+
" typer.echo(json.dumps({",
|
|
113
|
+
f" 'synergy': {card['candidate_id']!r},",
|
|
114
|
+
f" 'producer': {card['producer']!r},",
|
|
115
|
+
f" 'consumer': {card['consumer']!r},",
|
|
116
|
+
" 'artifact_size_bytes': artifact.stat().st_size,",
|
|
117
|
+
" 'producer_payload_keys': sorted(data.keys()) if isinstance(data, dict) else None,",
|
|
118
|
+
" }, indent=2))",
|
|
119
|
+
"",
|
|
120
|
+
])
|
|
121
|
+
return "\n".join(body) + "\n"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _render_in_memory_pipe(card: SynergyCandidateCard) -> str:
|
|
125
|
+
"""Run producer + consumer in-process; pass producer return to consumer.
|
|
126
|
+
|
|
127
|
+
Falls back to subprocess JSON-piping if either side isn't importable.
|
|
128
|
+
"""
|
|
129
|
+
body = _header_lines(card)
|
|
130
|
+
body.extend([
|
|
131
|
+
"@app.callback(invoke_without_command=True)",
|
|
132
|
+
"def run(ctx: typer.Context) -> None:",
|
|
133
|
+
f' """Run {card["producer"]} in-process and pass its return to {card["consumer"]}."""',
|
|
134
|
+
" if ctx.invoked_subcommand is not None:",
|
|
135
|
+
" return",
|
|
136
|
+
" # NOTE: in-memory pipe assumes producer and consumer expose a",
|
|
137
|
+
" # callable named `run` (or `main`) at module level. Manual wiring",
|
|
138
|
+
" # required if signatures don't align — see arg-alignment marker.",
|
|
139
|
+
" try:",
|
|
140
|
+
f" from atomadic_forge.commands import {card['producer']!s} as _producer # type: ignore[import-not-found]",
|
|
141
|
+
f" from atomadic_forge.commands import {card['consumer']!s} as _consumer # type: ignore[import-not-found]",
|
|
142
|
+
" except ImportError as exc:",
|
|
143
|
+
" typer.secho(f'in-memory pipe unavailable: {exc}', fg='yellow', err=True)",
|
|
144
|
+
" raise typer.Exit(2) from exc",
|
|
145
|
+
" producer_fn = getattr(_producer, 'run', None) or getattr(_producer, 'main', None)",
|
|
146
|
+
" consumer_fn = getattr(_consumer, 'run', None) or getattr(_consumer, 'main', None)",
|
|
147
|
+
" if producer_fn is None or consumer_fn is None:",
|
|
148
|
+
" typer.secho('producer or consumer has no `run`/`main` callable', fg='red', err=True)",
|
|
149
|
+
" raise typer.Exit(2)",
|
|
150
|
+
" intermediate = producer_fn()",
|
|
151
|
+
" # review: arg alignment — consumer may need shaping of `intermediate`",
|
|
152
|
+
" result = consumer_fn(intermediate)",
|
|
153
|
+
" typer.echo(json.dumps({",
|
|
154
|
+
f" 'synergy': {card['candidate_id']!r},",
|
|
155
|
+
" 'result': str(result)[:2000],",
|
|
156
|
+
" }, indent=2))",
|
|
157
|
+
"",
|
|
158
|
+
])
|
|
159
|
+
return "\n".join(body) + "\n"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _render_phase_omission(card: SynergyCandidateCard) -> str:
|
|
163
|
+
"""Producer at phase N → consumer at phase N+1. Both via subprocess.
|
|
164
|
+
|
|
165
|
+
Args may not align — emits a clear ``# review:`` marker so the operator
|
|
166
|
+
knows to inspect. Default behaviour is to run producer with no args
|
|
167
|
+
then prompt for consumer args.
|
|
168
|
+
"""
|
|
169
|
+
body = _header_lines(card)
|
|
170
|
+
body.extend([
|
|
171
|
+
"@app.callback(invoke_without_command=True)",
|
|
172
|
+
"def run(",
|
|
173
|
+
" ctx: typer.Context,",
|
|
174
|
+
" producer_args: Annotated[list[str] | None, typer.Argument(",
|
|
175
|
+
" help='Args for producer (everything before --consumer-args).')] = None,",
|
|
176
|
+
" consumer_args: Annotated[list[str] | None, typer.Option('--consumer-args',",
|
|
177
|
+
" help='Args forwarded to consumer (space-separated).')] = None,",
|
|
178
|
+
") -> None:",
|
|
179
|
+
f' """Phase chain: {card["producer"]} (phase) → {card["consumer"]} (next phase)."""',
|
|
180
|
+
" if ctx.invoked_subcommand is not None:",
|
|
181
|
+
" return",
|
|
182
|
+
" # review: arg alignment — phase_omission synergies are heuristic.",
|
|
183
|
+
" # The producer and consumer were inferred from phase order alone, so",
|
|
184
|
+
" # their CLI signatures are almost certainly different. Pass args",
|
|
185
|
+
" # explicitly via positional + --consumer-args until you hand-shape",
|
|
186
|
+
" # this adapter.",
|
|
187
|
+
" producer_args = producer_args or []",
|
|
188
|
+
" consumer_args = (consumer_args or '').split() if isinstance(consumer_args, str) else (consumer_args or [])",
|
|
189
|
+
" cmd_a = [sys.executable, '-m', 'atomadic_forge.a4_sy_orchestration.unified_cli',",
|
|
190
|
+
f" {card['producer']!r}, *producer_args]",
|
|
191
|
+
" rc = subprocess.run(cmd_a, capture_output=False).returncode",
|
|
192
|
+
" if rc != 0:",
|
|
193
|
+
" typer.secho(f'producer exited {rc}', fg='red', err=True)",
|
|
194
|
+
" raise typer.Exit(rc)",
|
|
195
|
+
" cmd_b = [sys.executable, '-m', 'atomadic_forge.a4_sy_orchestration.unified_cli',",
|
|
196
|
+
f" {card['consumer']!r}, *consumer_args]",
|
|
197
|
+
" rc = subprocess.run(cmd_b, capture_output=False).returncode",
|
|
198
|
+
" if rc != 0:",
|
|
199
|
+
" typer.secho(f'consumer exited {rc}', fg='red', err=True)",
|
|
200
|
+
" raise typer.Exit(rc)",
|
|
201
|
+
" typer.echo(json.dumps({",
|
|
202
|
+
f" 'synergy': {card['candidate_id']!r},",
|
|
203
|
+
f" 'phase_chain': [{card['producer']!r}, {card['consumer']!r}],",
|
|
204
|
+
" }, indent=2))",
|
|
205
|
+
"",
|
|
206
|
+
])
|
|
207
|
+
return "\n".join(body) + "\n"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _render_shared_schema(card: SynergyCandidateCard) -> str:
|
|
211
|
+
"""Like json_artifact but validates the producer JSON before piping."""
|
|
212
|
+
body = _header_lines(card)
|
|
213
|
+
body.extend([
|
|
214
|
+
"@app.callback(invoke_without_command=True)",
|
|
215
|
+
"def run(",
|
|
216
|
+
" ctx: typer.Context,",
|
|
217
|
+
" producer_args: Annotated[list[str] | None, typer.Argument(",
|
|
218
|
+
" help='Args forwarded to the producer command.')] = None,",
|
|
219
|
+
") -> None:",
|
|
220
|
+
f' """Run {card["producer"]} → validate shared schema → feed to {card["consumer"]}."""',
|
|
221
|
+
" if ctx.invoked_subcommand is not None:",
|
|
222
|
+
" return",
|
|
223
|
+
" producer_args = producer_args or []",
|
|
224
|
+
" with tempfile.TemporaryDirectory(prefix='synergy-') as tmp:",
|
|
225
|
+
" artifact = Path(tmp) / 'producer.json'",
|
|
226
|
+
" cmd_a = [sys.executable, '-m', 'atomadic_forge.a4_sy_orchestration.unified_cli',",
|
|
227
|
+
f" {card['producer']!r}, *producer_args, '--json-out', str(artifact)]",
|
|
228
|
+
" rc = subprocess.run(cmd_a, capture_output=False).returncode",
|
|
229
|
+
" if rc != 0:",
|
|
230
|
+
" raise typer.Exit(rc)",
|
|
231
|
+
" try:",
|
|
232
|
+
" payload = json.loads(artifact.read_text(encoding='utf-8'))",
|
|
233
|
+
" except (OSError, json.JSONDecodeError) as exc:",
|
|
234
|
+
" typer.secho(f'producer JSON invalid: {exc}', fg='red', err=True)",
|
|
235
|
+
" raise typer.Exit(2) from exc",
|
|
236
|
+
" schema_id = payload.get('schema_version') or payload.get('schema')",
|
|
237
|
+
" if not schema_id:",
|
|
238
|
+
" typer.secho('producer payload missing schema_version field', fg='yellow', err=True)",
|
|
239
|
+
" cmd_b = [sys.executable, '-m', 'atomadic_forge.a4_sy_orchestration.unified_cli',",
|
|
240
|
+
f" {card['consumer']!r}, str(artifact)]",
|
|
241
|
+
" rc = subprocess.run(cmd_b, capture_output=False).returncode",
|
|
242
|
+
" if rc != 0:",
|
|
243
|
+
" raise typer.Exit(rc)",
|
|
244
|
+
" typer.echo(json.dumps({",
|
|
245
|
+
f" 'synergy': {card['candidate_id']!r},",
|
|
246
|
+
" 'schema_id': schema_id,",
|
|
247
|
+
f" 'producer': {card['producer']!r},",
|
|
248
|
+
f" 'consumer': {card['consumer']!r},",
|
|
249
|
+
" }, indent=2))",
|
|
250
|
+
"",
|
|
251
|
+
])
|
|
252
|
+
return "\n".join(body) + "\n"
|