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,205 @@
1
+ """Tier a1 — pure renderer that builds a stunning README from a generated package.
2
+
3
+ After evolve converges, Forge walks the produced package and synthesises a
4
+ showcase-quality README from the actual code, tests, and certify report.
5
+ This replaces the bare scaffolded README with one that reflects what was
6
+ actually built — function names, class signatures, working CLI invocation,
7
+ score breakdown, evidence.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import ast
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ def _collect_public_symbols(package_root: Path) -> dict[str, list[dict[str, str]]]:
18
+ """Return ``{tier: [{name, kind, signature, doc}]}`` for the package."""
19
+ out: dict[str, list[dict[str, str]]] = {}
20
+ for tier_dir in sorted(package_root.iterdir()):
21
+ if not tier_dir.is_dir():
22
+ continue
23
+ if not tier_dir.name.startswith(("a0_", "a1_", "a2_", "a3_", "a4_")):
24
+ continue
25
+ items: list[dict[str, str]] = []
26
+ for py in sorted(tier_dir.glob("*.py")):
27
+ if py.name == "__init__.py":
28
+ continue
29
+ try:
30
+ tree = ast.parse(py.read_text(encoding="utf-8"))
31
+ except (SyntaxError, OSError):
32
+ continue
33
+ for node in tree.body:
34
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
35
+ if node.name.startswith("_"):
36
+ continue
37
+ sig = _function_signature(node)
38
+ items.append({
39
+ "name": node.name, "kind": "function", "signature": sig,
40
+ "doc": (ast.get_docstring(node) or "").split("\n")[0],
41
+ "module": py.stem,
42
+ })
43
+ elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
44
+ items.append({
45
+ "name": node.name, "kind": "class",
46
+ "signature": f"class {node.name}",
47
+ "doc": (ast.get_docstring(node) or "").split("\n")[0],
48
+ "module": py.stem,
49
+ })
50
+ if items:
51
+ out[tier_dir.name] = items
52
+ return out
53
+
54
+
55
+ def _function_signature(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
56
+ args: list[str] = []
57
+ for arg in fn.args.args:
58
+ if arg.arg in ("self", "cls"):
59
+ continue
60
+ ann = ast.unparse(arg.annotation) if arg.annotation else None
61
+ args.append(f"{arg.arg}: {ann}" if ann else arg.arg)
62
+ ret = ast.unparse(fn.returns) if fn.returns else "Any"
63
+ return f"def {fn.name}({', '.join(args)}) -> {ret}"
64
+
65
+
66
+ _TIER_LABEL = {
67
+ "a0_qk_constants": "a0 — constants",
68
+ "a1_at_functions": "a1 — pure functions",
69
+ "a2_mo_composites": "a2 — stateful classes",
70
+ "a3_og_features": "a3 — feature orchestrators",
71
+ "a4_sy_orchestration": "a4 — CLI / entry points",
72
+ }
73
+
74
+
75
+ def render_showcase_readme(*, package: str, intent: str,
76
+ output_root: Path,
77
+ certify_report: dict[str, Any] | None = None,
78
+ cli_demo: dict[str, Any] | None = None,
79
+ llm_name: str = "?") -> str:
80
+ """Build a stunning README for a generated package.
81
+
82
+ ``cli_demo`` (optional): ``{"command": [...], "stdout": "...", "rc": int}``
83
+ captured from a successful invocation of the generated CLI.
84
+ """
85
+ pkg_root = output_root / "src" / package
86
+ symbols = _collect_public_symbols(pkg_root) if pkg_root.exists() else {}
87
+
88
+ # Build a hero score badge from the certify report.
89
+ score_line = ""
90
+ if certify_report:
91
+ score = certify_report.get("score", 0)
92
+ comps = certify_report.get("score_components", {})
93
+ score_line = (
94
+ f"![score](https://img.shields.io/badge/forge_certify-"
95
+ f"{int(score)}%2F100-brightgreen)"
96
+ if score >= 80 else
97
+ f"![score](https://img.shields.io/badge/forge_certify-"
98
+ f"{int(score)}%2F100-yellow)"
99
+ )
100
+
101
+ lines: list[str] = [
102
+ f"# `{package}`",
103
+ "",
104
+ score_line,
105
+ "",
106
+ "_Generated by [Atomadic Forge](https://atomadic.tech) — absorb · enforce · emerge._",
107
+ "",
108
+ "## Intent",
109
+ "",
110
+ f"> {intent.strip()}",
111
+ "",
112
+ ]
113
+
114
+ if certify_report:
115
+ score = certify_report.get("score", 0)
116
+ comps = certify_report.get("score_components", {})
117
+ (certify_report.get("detail") or {}).get("import_smoke") or {}
118
+ test_run = (certify_report.get("detail") or {}).get("test_run") or {}
119
+ lines.extend([
120
+ "## Forge certification",
121
+ "",
122
+ "| Component | Result |",
123
+ "|-----------|--------|",
124
+ f"| Documentation present | {'✅' if certify_report.get('documentation_complete') else '❌'} |",
125
+ f"| Tests present | {'✅' if certify_report.get('tests_present') else '❌'} |",
126
+ f"| 5-tier layout | {'✅' if certify_report.get('tier_layout_present') else '❌'} |",
127
+ f"| Wire (no upward imports) | {'✅' if certify_report.get('no_upward_imports') else '❌'} |",
128
+ f"| Package importable | {'✅' if certify_report.get('package_importable') else '❌'} |",
129
+ f"| No stub bodies | {'✅' if certify_report.get('no_stub_bodies') else '❌'} |",
130
+ f"| Tests pass-ratio | {certify_report.get('test_pass_ratio', 0):.0%} ({test_run.get('passed', 0)}/{test_run.get('total', 0)}) |",
131
+ "",
132
+ f"**Score: {score:.0f}/100** "
133
+ + (f"(structural {comps.get('structural', 0)} · "
134
+ f"runtime {comps.get('runtime', 0)} · "
135
+ f"behavioral {comps.get('behavioral', 0)})"
136
+ if comps else ""),
137
+ "",
138
+ ])
139
+
140
+ # Surface the generated symbols, organised by tier.
141
+ if symbols:
142
+ lines.append("## What was built")
143
+ lines.append("")
144
+ for tier in ("a0_qk_constants", "a1_at_functions", "a2_mo_composites",
145
+ "a3_og_features", "a4_sy_orchestration"):
146
+ if tier not in symbols:
147
+ continue
148
+ lines.append(f"### {_TIER_LABEL[tier]}")
149
+ lines.append("")
150
+ for s in symbols[tier]:
151
+ doc = s["doc"] or ""
152
+ trailer = f" — _{doc}_" if doc else ""
153
+ lines.append(f"- **`{s['signature']}`**{trailer} "
154
+ f"<sub>(`{tier}/{s['module']}.py`)</sub>")
155
+ lines.append("")
156
+
157
+ if cli_demo:
158
+ lines.extend([
159
+ "## Live invocation",
160
+ "",
161
+ "```bash",
162
+ "$ " + " ".join(cli_demo.get("command", []))[:200],
163
+ (cli_demo.get("stdout") or "").strip()[:500] or "(no stdout)",
164
+ "```",
165
+ "",
166
+ f"Exit code: `{cli_demo.get('rc', '?')}`",
167
+ "",
168
+ ])
169
+
170
+ lines.extend([
171
+ "## Architecture",
172
+ "",
173
+ "This package follows the Atomadic 5-tier monadic standard. Every",
174
+ "file lives in exactly one tier and tiers compose **upward only**.",
175
+ "Every public symbol is reachable via the tier package's "
176
+ "auto-generated `__init__.py` re-exports.",
177
+ "",
178
+ "```",
179
+ f"src/{package}/",
180
+ "├── a0_qk_constants/ constants, enums, TypedDicts",
181
+ "├── a1_at_functions/ pure stateless functions",
182
+ "├── a2_mo_composites/ stateful classes",
183
+ "├── a3_og_features/ feature orchestrators",
184
+ "└── a4_sy_orchestration/ CLI / entry points",
185
+ "```",
186
+ "",
187
+ "## Install",
188
+ "",
189
+ "```bash",
190
+ "pip install -e .",
191
+ "```",
192
+ "",
193
+ "## Verify with Forge",
194
+ "",
195
+ "```bash",
196
+ f"forge wire src/{package}",
197
+ f"forge certify . --package {package}",
198
+ "```",
199
+ "",
200
+ "---",
201
+ "",
202
+ f"_Generated by Atomadic Forge with `{llm_name}` as the generation engine._",
203
+ "",
204
+ ])
205
+ return "\n".join(line for line in lines if line is not None)
@@ -0,0 +1,192 @@
1
+ """Tier a1 — pure composition discovery.
2
+
3
+ Given a list of :class:`SymbolSignatureCard`, find chains where the output of
4
+ one symbol is type-compatible with an input of another.
5
+
6
+ Type compatibility is intentionally loose — we work on annotation text:
7
+
8
+ * exact identical text → match
9
+ * ``T`` ↔ ``Optional[T]`` / ``T | None`` → match
10
+ * ``Iterable[T]`` ↔ ``list[T]`` / ``Sequence[T]`` / ``Collection[T]`` → match (T equal)
11
+ * ``Any`` ↔ anything → match
12
+ * generic vs concrete (``list`` ↔ ``list[str]``) → match (looser side wins)
13
+
14
+ Chain enumeration is bounded to depth ``max_depth`` (default 3) so the search
15
+ space stays tractable even on a 600-symbol catalog.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from collections.abc import Iterable
22
+
23
+ from ..a0_qk_constants.emergent_types import (
24
+ CompositionChain,
25
+ SymbolSignatureCard,
26
+ )
27
+
28
+ _OPTIONAL_RE = re.compile(r"^Optional\[(.+)\]$|^(.+)\s*\|\s*None$|^None\s*\|\s*(.+)$")
29
+ _GENERIC_RE = re.compile(r"^(\w+)\[(.+)\]$")
30
+ _COLLECTION_FAMILY = {"list", "List", "Sequence", "Iterable", "Collection",
31
+ "tuple", "Tuple", "set", "Set", "frozenset"}
32
+
33
+
34
+ def _strip_optional(t: str) -> str:
35
+ t = t.strip()
36
+ m = _OPTIONAL_RE.match(t)
37
+ if m:
38
+ return next(g for g in m.groups() if g is not None).strip()
39
+ return t
40
+
41
+
42
+ def _outer(t: str) -> tuple[str, str | None]:
43
+ """Return (outer name, inner) for ``Outer[Inner]``, else (t, None)."""
44
+ m = _GENERIC_RE.match(t.strip())
45
+ if not m:
46
+ return t.strip(), None
47
+ return m.group(1), m.group(2).strip()
48
+
49
+
50
+ def _normalize(t: str) -> str:
51
+ return _strip_optional(t).replace(" ", "")
52
+
53
+
54
+ def types_compatible(produced: str, consumed: str,
55
+ *, strict: bool = False) -> bool:
56
+ """Return True if the producer's output text can flow into the consumer.
57
+
58
+ ``strict`` (default False keeps behaviour permissive for backwards-compat):
59
+ when True, treat ``Any`` as a non-match unless the partner is also ``Any``,
60
+ and refuse generic-collection variance unless inner types align. Strict
61
+ mode is what :func:`find_chains` uses to filter out the noisy ``dict[str,
62
+ Any] → dict[str, Any]`` chains that dominate the catalog.
63
+ """
64
+ p = _normalize(produced)
65
+ c = _normalize(consumed)
66
+ if not p or not c:
67
+ return False
68
+ if p == c:
69
+ return True
70
+ if "Any" in (p, c):
71
+ if strict:
72
+ return False
73
+ return True
74
+ p_outer, p_inner = _outer(p)
75
+ c_outer, c_inner = _outer(c)
76
+ if p_outer in _COLLECTION_FAMILY and c_outer in _COLLECTION_FAMILY:
77
+ if p_inner is None and c_inner is None:
78
+ return True
79
+ if p_inner is None or c_inner is None:
80
+ return not strict
81
+ return types_compatible(p_inner, c_inner, strict=strict)
82
+ if p_outer == c_outer and (p_inner is None or c_inner is None):
83
+ return not strict
84
+ return False
85
+
86
+
87
+ def is_anyish(t: str) -> bool:
88
+ """Is this type spec a noisy 'Any-shaped' carrier?"""
89
+ n = _normalize(t)
90
+ if n == "Any":
91
+ return True
92
+ outer, inner = _outer(n)
93
+ if inner and "Any" in inner:
94
+ return True
95
+ if outer in {"dict", "Dict", "Mapping"} and (inner is None or "Any" in (inner or "")):
96
+ return True
97
+ return False
98
+
99
+
100
+ def _consumer_inputs(card: SymbolSignatureCard) -> list[str]:
101
+ return [t for _, t in card["inputs"]]
102
+
103
+
104
+ def find_chains(
105
+ cards: Iterable[SymbolSignatureCard],
106
+ *,
107
+ max_depth: int = 3,
108
+ max_chains: int = 5_000,
109
+ require_pure: bool = False,
110
+ domain_jump_required: bool = False,
111
+ strict_types: bool = True,
112
+ drop_anyish_seeds: bool = True,
113
+ ) -> list[CompositionChain]:
114
+ """Enumerate type-compatible chains across the catalog.
115
+
116
+ A chain is a sequence ``[c1, c2, …]`` where ``c_{i+1}`` has at least one
117
+ input compatible with ``c_i``'s output, and no card appears twice.
118
+
119
+ Parameters bound the search:
120
+
121
+ * ``max_depth`` — chain length cap.
122
+ * ``max_chains`` — early-stop hard cap on returned chains.
123
+ * ``require_pure`` — only purely-inferred symbols can participate.
124
+ * ``domain_jump_required`` — at least two distinct domains in the chain.
125
+ * ``strict_types`` — refuse ``Any``-as-bridge matches. Cuts the noise
126
+ from ``dict[str, Any] → dict[str, Any]`` chains that dominate the
127
+ catalog when most signatures are loosely typed.
128
+ * ``drop_anyish_seeds`` — skip seeding chains from symbols whose output
129
+ is a generic ``dict``/``Mapping``/``Any`` carrier. Such symbols are
130
+ bottlenecks (they connect to half the catalog) and produce many
131
+ look-alike candidates.
132
+ """
133
+ catalog = [c for c in cards if not (require_pure and not c["is_pure"])]
134
+ by_qual = {c["qualname"]: c for c in catalog}
135
+ out: list[CompositionChain] = []
136
+
137
+ def consumers_of(produced: str) -> list[SymbolSignatureCard]:
138
+ return [
139
+ c for c in catalog
140
+ if any(types_compatible(produced, ti, strict=strict_types)
141
+ for ti in _consumer_inputs(c))
142
+ ]
143
+
144
+ def extend(prefix: list[SymbolSignatureCard]) -> None:
145
+ if len(out) >= max_chains:
146
+ return
147
+ if len(prefix) >= max_depth:
148
+ _record(prefix)
149
+ return
150
+ last = prefix[-1]
151
+ nexts = consumers_of(last["output"])
152
+ used = {c["qualname"] for c in prefix}
153
+ progressed = False
154
+ for nxt in nexts:
155
+ if nxt["qualname"] in used:
156
+ continue
157
+ progressed = True
158
+ extend(prefix + [nxt])
159
+ if len(out) >= max_chains:
160
+ return
161
+ if not progressed:
162
+ _record(prefix)
163
+
164
+ def _record(prefix: list[SymbolSignatureCard]) -> None:
165
+ if len(prefix) < 2:
166
+ return
167
+ domains = [p["domain"] for p in prefix]
168
+ tiers = [p["tier"] for p in prefix]
169
+ if domain_jump_required and len(set(domains)) < 2:
170
+ return
171
+ bridges: list[str] = []
172
+ for i in range(len(prefix) - 1):
173
+ bridges.append(prefix[i]["output"])
174
+ chain = CompositionChain(
175
+ chain=[p["qualname"] for p in prefix],
176
+ bridges=bridges,
177
+ tiers=tiers,
178
+ domains=domains,
179
+ crosses_domains=len(set(domains)),
180
+ crosses_tiers=len(set(tiers)),
181
+ final_output_type=prefix[-1]["output"],
182
+ pure=all(by_qual[q]["is_pure"] for q in (p["qualname"] for p in prefix)),
183
+ )
184
+ out.append(chain)
185
+
186
+ seeds = [c for c in catalog
187
+ if not (drop_anyish_seeds and is_anyish(c["output"]))]
188
+ for seed in seeds:
189
+ extend([seed])
190
+ if len(out) >= max_chains:
191
+ break
192
+ return out
@@ -0,0 +1,116 @@
1
+ """Tier a1 — pure ranker for the Emergent Scan.
2
+
3
+ Score each :class:`CompositionChain` on how 'emergent' it is, then return
4
+ the top-N as :class:`EmergentCandidateCard`.
5
+
6
+ Score components (max 100):
7
+
8
+ * cross-domain bonus ``+ 10 * (crosses_domains - 1)`` (0..30)
9
+ * cross-tier bonus ``+ 8 * (crosses_tiers - 1)`` (0..32)
10
+ * purity bonus ``+ 10`` if all steps pure
11
+ * depth bonus ``+ 6 * (len(chain) - 1)`` (0..18)
12
+ * novelty bonus ``+ 10`` if final output type isn't already
13
+ the output of any single existing symbol
14
+
15
+ Output names are heuristic kebab-case combinations of distinct domains.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ from collections.abc import Iterable
22
+
23
+ from ..a0_qk_constants.emergent_types import (
24
+ CompositionChain,
25
+ EmergentCandidateCard,
26
+ SymbolSignatureCard,
27
+ )
28
+
29
+
30
+ def _candidate_id(chain: CompositionChain) -> str:
31
+ h = hashlib.sha256("→".join(chain["chain"]).encode("utf-8")).hexdigest()
32
+ return f"emrg-{h[:8]}"
33
+
34
+
35
+ def _suggest_name(chain: CompositionChain) -> str:
36
+ seen: list[str] = []
37
+ for d in chain["domains"]:
38
+ if d not in seen:
39
+ seen.append(d)
40
+ return "-".join(seen[:4]) + "-pipeline"
41
+
42
+
43
+ def _suggested_tier(chain: CompositionChain) -> str:
44
+ """A composition that touches multiple a2 composites is itself a3."""
45
+ tiers = set(chain["tiers"])
46
+ if "a4_sy_orchestration" in tiers:
47
+ return "a4_sy_orchestration"
48
+ if "a3_og_features" in tiers or len(tiers) >= 3:
49
+ return "a3_og_features"
50
+ if "a2_mo_composites" in tiers:
51
+ return "a2_mo_composites"
52
+ return "a1_at_functions"
53
+
54
+
55
+ def _summary(chain: CompositionChain) -> str:
56
+ domains = " → ".join(chain["domains"])
57
+ return (f"Compose {len(chain['chain'])} steps across {chain['crosses_domains']} "
58
+ f"domain(s): {domains}; final output {chain['final_output_type']}")
59
+
60
+
61
+ def rank_chains(
62
+ chains: Iterable[CompositionChain],
63
+ *,
64
+ catalog: list[SymbolSignatureCard],
65
+ top_n: int = 25,
66
+ novelty_unknown_outputs: bool = True,
67
+ ) -> list[EmergentCandidateCard]:
68
+ from .emergent_compose import is_anyish
69
+
70
+ chains = list(chains)
71
+ existing_outputs = {c["output"] for c in catalog}
72
+
73
+ out: list[EmergentCandidateCard] = []
74
+ for chain in chains:
75
+ breakdown: dict[str, float] = {}
76
+ breakdown["cross_domain"] = min(30, 10 * max(0, chain["crosses_domains"] - 1))
77
+ breakdown["cross_tier"] = min(32, 8 * max(0, chain["crosses_tiers"] - 1))
78
+ breakdown["pure"] = 10 if chain["pure"] else 0
79
+ breakdown["depth"] = min(18, 6 * max(0, len(chain["chain"]) - 1))
80
+ novelty_signals: list[str] = []
81
+ novel = (chain["final_output_type"] not in existing_outputs)
82
+ breakdown["novelty"] = 10 if (novelty_unknown_outputs and novel) else 0
83
+ # Penalty for chains whose every bridge is Any-shaped — they're real
84
+ # but uninformative.
85
+ anyish_bridges = sum(1 for b in chain["bridges"] if is_anyish(b))
86
+ if anyish_bridges:
87
+ breakdown["any_penalty"] = -min(20, 6 * anyish_bridges)
88
+ if novel:
89
+ novelty_signals.append("final output type not produced by any single existing symbol")
90
+ if chain["crosses_domains"] >= 3:
91
+ novelty_signals.append("touches three or more domains")
92
+ if chain["crosses_tiers"] >= 3:
93
+ novelty_signals.append("spans three or more tiers")
94
+ if chain["pure"]:
95
+ novelty_signals.append("entirely pure — safe to materialise as a1/a3 with no I/O risk")
96
+ score = sum(breakdown.values())
97
+ if score <= 0:
98
+ continue
99
+ out.append(EmergentCandidateCard(
100
+ candidate_id=_candidate_id(chain),
101
+ name=_suggest_name(chain),
102
+ summary=_summary(chain),
103
+ chain=chain,
104
+ score=float(score),
105
+ score_breakdown=breakdown,
106
+ suggested_tier=_suggested_tier(chain),
107
+ novelty_signals=novelty_signals,
108
+ ))
109
+ out.sort(key=lambda c: c["score"], reverse=True)
110
+ # Dedupe candidate names, keep top by score per name
111
+ seen: dict[str, EmergentCandidateCard] = {}
112
+ for c in out:
113
+ if c["name"] not in seen or seen[c["name"]]["score"] < c["score"]:
114
+ seen[c["name"]] = c
115
+ deduped = sorted(seen.values(), key=lambda c: c["score"], reverse=True)
116
+ return deduped[:top_n]