atomadic-forge 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. atomadic_forge/__init__.py +12 -0
  2. atomadic_forge/__main__.py +5 -0
  3. atomadic_forge/a0_qk_constants/__init__.py +1 -0
  4. atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
  5. atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
  6. atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
  7. atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
  8. atomadic_forge/a0_qk_constants/error_codes.py +296 -0
  9. atomadic_forge/a0_qk_constants/forge_types.py +89 -0
  10. atomadic_forge/a0_qk_constants/gen_language.py +116 -0
  11. atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
  12. atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
  13. atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
  14. atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
  15. atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
  16. atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
  17. atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
  18. atomadic_forge/a0_qk_constants/tier_names.py +47 -0
  19. atomadic_forge/a1_at_functions/__init__.py +1 -0
  20. atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
  21. atomadic_forge/a1_at_functions/agent_memory.py +139 -0
  22. atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
  23. atomadic_forge/a1_at_functions/agent_summary.py +277 -0
  24. atomadic_forge/a1_at_functions/body_extractor.py +306 -0
  25. atomadic_forge/a1_at_functions/card_renderer.py +210 -0
  26. atomadic_forge/a1_at_functions/certify_checks.py +445 -0
  27. atomadic_forge/a1_at_functions/chat_context.py +170 -0
  28. atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
  29. atomadic_forge/a1_at_functions/classify_tier.py +115 -0
  30. atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
  31. atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
  32. atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
  33. atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
  34. atomadic_forge/a1_at_functions/config_io.py +68 -0
  35. atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
  36. atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
  37. atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
  38. atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
  39. atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
  40. atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
  41. atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
  42. atomadic_forge/a1_at_functions/error_hints.py +105 -0
  43. atomadic_forge/a1_at_functions/evolution_log.py +94 -0
  44. atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
  45. atomadic_forge/a1_at_functions/generation_quality.py +322 -0
  46. atomadic_forge/a1_at_functions/import_repair.py +211 -0
  47. atomadic_forge/a1_at_functions/import_smoke.py +102 -0
  48. atomadic_forge/a1_at_functions/js_parser.py +539 -0
  49. atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
  50. atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
  51. atomadic_forge/a1_at_functions/llm_client.py +554 -0
  52. atomadic_forge/a1_at_functions/local_signer.py +134 -0
  53. atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
  54. atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
  55. atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
  56. atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
  57. atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
  58. atomadic_forge/a1_at_functions/policy_loader.py +107 -0
  59. atomadic_forge/a1_at_functions/preflight_change.py +227 -0
  60. atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
  61. atomadic_forge/a1_at_functions/provider_detect.py +157 -0
  62. atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
  63. atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
  64. atomadic_forge/a1_at_functions/recipes.py +186 -0
  65. atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
  66. atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
  67. atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
  68. atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
  69. atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
  70. atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
  71. atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
  72. atomadic_forge/a1_at_functions/scout_walk.py +309 -0
  73. atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
  74. atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
  75. atomadic_forge/a1_at_functions/stub_detector.py +158 -0
  76. atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
  77. atomadic_forge/a1_at_functions/synergy_render.py +252 -0
  78. atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
  79. atomadic_forge/a1_at_functions/test_runner.py +196 -0
  80. atomadic_forge/a1_at_functions/test_selector.py +122 -0
  81. atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
  82. atomadic_forge/a1_at_functions/tool_composer.py +130 -0
  83. atomadic_forge/a1_at_functions/transcript_log.py +70 -0
  84. atomadic_forge/a1_at_functions/wire_check.py +260 -0
  85. atomadic_forge/a2_mo_composites/__init__.py +1 -0
  86. atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
  87. atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
  88. atomadic_forge/a2_mo_composites/plan_store.py +164 -0
  89. atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
  90. atomadic_forge/a3_og_features/__init__.py +1 -0
  91. atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
  92. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
  93. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
  94. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
  95. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
  96. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
  97. atomadic_forge/a3_og_features/demo_runner.py +502 -0
  98. atomadic_forge/a3_og_features/emergent_feature.py +95 -0
  99. atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
  100. atomadic_forge/a3_og_features/forge_enforce.py +107 -0
  101. atomadic_forge/a3_og_features/forge_evolve.py +176 -0
  102. atomadic_forge/a3_og_features/forge_loop.py +528 -0
  103. atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
  104. atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
  105. atomadic_forge/a3_og_features/lsp_server.py +98 -0
  106. atomadic_forge/a3_og_features/mcp_server.py +160 -0
  107. atomadic_forge/a3_og_features/setup_wizard.py +337 -0
  108. atomadic_forge/a3_og_features/synergy_feature.py +65 -0
  109. atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
  110. atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
  111. atomadic_forge/commands/__init__.py +1 -0
  112. atomadic_forge/commands/_registry.py +36 -0
  113. atomadic_forge/commands/audit.py +142 -0
  114. atomadic_forge/commands/chat.py +133 -0
  115. atomadic_forge/commands/commandsmith.py +178 -0
  116. atomadic_forge/commands/config_cmd.py +145 -0
  117. atomadic_forge/commands/demo.py +142 -0
  118. atomadic_forge/commands/emergent.py +124 -0
  119. atomadic_forge/commands/emergent_then_synergy.py +70 -0
  120. atomadic_forge/commands/evolve.py +122 -0
  121. atomadic_forge/commands/evolve_then_iterate.py +70 -0
  122. atomadic_forge/commands/feature_then_emergent.py +111 -0
  123. atomadic_forge/commands/iterate.py +140 -0
  124. atomadic_forge/commands/synergy.py +96 -0
  125. atomadic_forge/commands/synergy_then_emergent.py +70 -0
  126. atomadic_forge-0.3.2.dist-info/METADATA +471 -0
  127. atomadic_forge-0.3.2.dist-info/RECORD +131 -0
  128. atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
  129. atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
  130. atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
  131. atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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"