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,306 @@
|
|
|
1
|
+
"""Source body extraction and made_of graph — Phase 3 pure logic (ported from atomadic-forge-v1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import re
|
|
7
|
+
import textwrap
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
_MAX_FILE_BYTES = 2_000_000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ExtractedBody:
|
|
17
|
+
source_path: str
|
|
18
|
+
source_line: int
|
|
19
|
+
symbol_name: str
|
|
20
|
+
language: str
|
|
21
|
+
body: str
|
|
22
|
+
imports: list[str]
|
|
23
|
+
callers_of: list[str]
|
|
24
|
+
exceptions_raised: list[str]
|
|
25
|
+
# Sibling top-level names defined in the same source file that are
|
|
26
|
+
# referenced from inside the extracted body. After extraction these
|
|
27
|
+
# become cross-file references and must be re-imported by the materializer.
|
|
28
|
+
sibling_refs: list[str] = None # type: ignore[assignment]
|
|
29
|
+
# Tier-classification hints harvested from the body itself. Two cheap
|
|
30
|
+
# signals: ``has_self_assign`` (mutable instance state) and
|
|
31
|
+
# ``has_class_attr_collections`` (mutable class-level dict/list/set).
|
|
32
|
+
has_self_assign: bool = False
|
|
33
|
+
has_class_attr_collections: bool = False
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if self.sibling_refs is None:
|
|
37
|
+
self.sibling_refs = []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _collect_top_level_names(tree: ast.Module) -> set[str]:
|
|
41
|
+
"""Return every name bound at module top level (classes, functions, vars)."""
|
|
42
|
+
names: set[str] = set()
|
|
43
|
+
for node in tree.body:
|
|
44
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
45
|
+
names.add(node.name)
|
|
46
|
+
elif isinstance(node, ast.Assign):
|
|
47
|
+
for target in node.targets:
|
|
48
|
+
if isinstance(target, ast.Name):
|
|
49
|
+
names.add(target.id)
|
|
50
|
+
elif isinstance(node, ast.AnnAssign):
|
|
51
|
+
if isinstance(node.target, ast.Name):
|
|
52
|
+
names.add(node.target.id)
|
|
53
|
+
elif isinstance(node, ast.Import | ast.ImportFrom):
|
|
54
|
+
for alias in node.names:
|
|
55
|
+
names.add(alias.asname or alias.name.split(".", 1)[0])
|
|
56
|
+
return names
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _detect_state_markers(target: ast.AST) -> tuple[bool, bool]:
|
|
60
|
+
"""Return ``(has_self_assign, has_class_attr_collections)``.
|
|
61
|
+
|
|
62
|
+
``has_self_assign``: the class/function body contains ``self.<attr> = …``
|
|
63
|
+
(mutable instance state — strong signal for tier a2).
|
|
64
|
+
``has_class_attr_collections``: a class-level assignment like
|
|
65
|
+
``foo: list[str] = []`` (also a state signal).
|
|
66
|
+
"""
|
|
67
|
+
has_self = False
|
|
68
|
+
has_class_attr = False
|
|
69
|
+
if isinstance(target, ast.ClassDef):
|
|
70
|
+
for node in ast.walk(target):
|
|
71
|
+
if isinstance(node, ast.Assign):
|
|
72
|
+
for t in node.targets:
|
|
73
|
+
if (isinstance(t, ast.Attribute)
|
|
74
|
+
and isinstance(t.value, ast.Name)
|
|
75
|
+
and t.value.id == "self"):
|
|
76
|
+
has_self = True
|
|
77
|
+
for node in target.body:
|
|
78
|
+
if isinstance(node, ast.Assign | ast.AnnAssign):
|
|
79
|
+
value = getattr(node, "value", None)
|
|
80
|
+
if isinstance(value, ast.List | ast.Dict | ast.Set | ast.ListComp | ast.DictComp | ast.SetComp):
|
|
81
|
+
has_class_attr = True
|
|
82
|
+
return has_self, has_class_attr
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _extract_python_body(source_text: str, symbol_name: str) -> ExtractedBody | None:
|
|
86
|
+
try:
|
|
87
|
+
tree = ast.parse(source_text)
|
|
88
|
+
except SyntaxError:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
imports: list[str] = []
|
|
92
|
+
for node in ast.walk(tree):
|
|
93
|
+
if isinstance(node, ast.Import):
|
|
94
|
+
imports.extend(alias.name for alias in node.names)
|
|
95
|
+
elif isinstance(node, ast.ImportFrom):
|
|
96
|
+
mod = node.module or ""
|
|
97
|
+
imports.extend(f"{mod}.{alias.name}" if mod else alias.name for alias in node.names)
|
|
98
|
+
|
|
99
|
+
target: ast.AST | None = None
|
|
100
|
+
for node in ast.walk(tree):
|
|
101
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
102
|
+
if node.name == symbol_name:
|
|
103
|
+
target = node
|
|
104
|
+
break
|
|
105
|
+
if target is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
lines = source_text.splitlines(keepends=True)
|
|
109
|
+
start = max(0, target.lineno - 1)
|
|
110
|
+
end = getattr(target, "end_lineno", None) or start + 1
|
|
111
|
+
body = textwrap.dedent("".join(lines[start:end]))
|
|
112
|
+
|
|
113
|
+
callers: list[str] = []
|
|
114
|
+
for node in ast.walk(target):
|
|
115
|
+
if isinstance(node, ast.Call):
|
|
116
|
+
func = node.func
|
|
117
|
+
if isinstance(func, ast.Name):
|
|
118
|
+
callers.append(func.id)
|
|
119
|
+
elif isinstance(func, ast.Attribute):
|
|
120
|
+
callers.append(func.attr)
|
|
121
|
+
|
|
122
|
+
raised: list[str] = []
|
|
123
|
+
for node in ast.walk(target):
|
|
124
|
+
if isinstance(node, ast.Raise) and node.exc is not None:
|
|
125
|
+
if isinstance(node.exc, ast.Name):
|
|
126
|
+
raised.append(node.exc.id)
|
|
127
|
+
elif isinstance(node.exc, ast.Call) and isinstance(node.exc.func, ast.Name):
|
|
128
|
+
raised.append(node.exc.func.id)
|
|
129
|
+
|
|
130
|
+
# ── Sibling references — names defined at module top level in the same
|
|
131
|
+
# source file that are referenced from inside the extracted body.
|
|
132
|
+
# After single-symbol extraction these become cross-file references.
|
|
133
|
+
top_names = _collect_top_level_names(tree)
|
|
134
|
+
referenced: set[str] = set()
|
|
135
|
+
locally_bound: set[str] = {symbol_name}
|
|
136
|
+
if isinstance(target, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
137
|
+
for arg in target.args.args + target.args.kwonlyargs:
|
|
138
|
+
locally_bound.add(arg.arg)
|
|
139
|
+
if target.args.vararg:
|
|
140
|
+
locally_bound.add(target.args.vararg.arg)
|
|
141
|
+
if target.args.kwarg:
|
|
142
|
+
locally_bound.add(target.args.kwarg.arg)
|
|
143
|
+
for node in ast.walk(target):
|
|
144
|
+
if isinstance(node, ast.Name):
|
|
145
|
+
referenced.add(node.id)
|
|
146
|
+
elif isinstance(node, ast.Assign):
|
|
147
|
+
for t in node.targets:
|
|
148
|
+
if isinstance(t, ast.Name):
|
|
149
|
+
locally_bound.add(t.id)
|
|
150
|
+
elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
151
|
+
locally_bound.add(node.name)
|
|
152
|
+
for arg in node.args.args + node.args.kwonlyargs:
|
|
153
|
+
locally_bound.add(arg.arg)
|
|
154
|
+
sibling_refs = sorted(
|
|
155
|
+
n for n in referenced
|
|
156
|
+
if n in top_names and n != symbol_name and n not in locally_bound
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
has_self_assign, has_class_attr_collections = _detect_state_markers(target)
|
|
160
|
+
|
|
161
|
+
return ExtractedBody(
|
|
162
|
+
source_path="",
|
|
163
|
+
source_line=target.lineno,
|
|
164
|
+
symbol_name=symbol_name,
|
|
165
|
+
language="python",
|
|
166
|
+
body=body,
|
|
167
|
+
imports=imports,
|
|
168
|
+
callers_of=list(dict.fromkeys(callers)),
|
|
169
|
+
exceptions_raised=list(dict.fromkeys(raised)),
|
|
170
|
+
sibling_refs=sibling_refs,
|
|
171
|
+
has_self_assign=has_self_assign,
|
|
172
|
+
has_class_attr_collections=has_class_attr_collections,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _extract_regex_body(
|
|
177
|
+
source_text: str, symbol_name: str, language: str
|
|
178
|
+
) -> ExtractedBody | None:
|
|
179
|
+
pattern = re.compile(rf"\b{re.escape(symbol_name)}\b", re.MULTILINE)
|
|
180
|
+
match = pattern.search(source_text)
|
|
181
|
+
if not match:
|
|
182
|
+
return None
|
|
183
|
+
lines = source_text.splitlines(keepends=True)
|
|
184
|
+
match_line = source_text.count("\n", 0, match.start())
|
|
185
|
+
start = max(0, match_line - 2)
|
|
186
|
+
end = min(len(lines), match_line + 40)
|
|
187
|
+
body = textwrap.dedent("".join(lines[start:end]))
|
|
188
|
+
body = re.sub(
|
|
189
|
+
r"(?m)^\s*// Extracted from .*\n"
|
|
190
|
+
r"(?:\s*// Component id: .*\n)?"
|
|
191
|
+
r"\s*\n?",
|
|
192
|
+
"",
|
|
193
|
+
body,
|
|
194
|
+
)
|
|
195
|
+
if language == "rust":
|
|
196
|
+
import_pat = re.compile(r"^\s*use\s+([\w:]+);", re.MULTILINE)
|
|
197
|
+
else:
|
|
198
|
+
import_pat = re.compile(
|
|
199
|
+
r"^\s*(?:import|export)\s.*?from\s+['\"]([^'\"]+)", re.MULTILINE
|
|
200
|
+
)
|
|
201
|
+
imports = import_pat.findall(source_text)
|
|
202
|
+
return ExtractedBody(
|
|
203
|
+
source_path="",
|
|
204
|
+
source_line=match_line + 1,
|
|
205
|
+
symbol_name=symbol_name,
|
|
206
|
+
language=language,
|
|
207
|
+
body=body,
|
|
208
|
+
imports=list(dict.fromkeys(imports)),
|
|
209
|
+
callers_of=[],
|
|
210
|
+
exceptions_raised=[],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def extract_body(
|
|
215
|
+
source_path: str | Path, symbol_name: str, language: str = "python"
|
|
216
|
+
) -> ExtractedBody | None:
|
|
217
|
+
"""Extract the body of one symbol from one source file. Returns None on failure."""
|
|
218
|
+
p = Path(source_path)
|
|
219
|
+
if not p.exists() or p.stat().st_size > _MAX_FILE_BYTES:
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
223
|
+
except OSError:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
if language == "python" or p.suffix == ".py":
|
|
227
|
+
result = _extract_python_body(text, symbol_name)
|
|
228
|
+
else:
|
|
229
|
+
result = _extract_regex_body(text, symbol_name, language)
|
|
230
|
+
if result is not None:
|
|
231
|
+
result.source_path = p.as_posix()
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def enrich_components_with_bodies(
|
|
236
|
+
plan: dict[str, Any],
|
|
237
|
+
*,
|
|
238
|
+
max_body_chars: int | None = None,
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
"""Attach extracted source bodies to every proposed component. Mutates ``plan``.
|
|
241
|
+
|
|
242
|
+
Also recomputes the tier when body extraction reveals a stateful class
|
|
243
|
+
that was originally classified at a1 by name heuristics alone. The
|
|
244
|
+
recompute is conservative — it only PROMOTES (a1 → a2), never demotes.
|
|
245
|
+
"""
|
|
246
|
+
for prop in plan.get("proposed_components") or []:
|
|
247
|
+
sym = prop.get("source_symbol") or {}
|
|
248
|
+
src_path = sym.get("path") or ""
|
|
249
|
+
name = sym.get("name") or prop.get("name")
|
|
250
|
+
lang = sym.get("language") or "python"
|
|
251
|
+
if not src_path or not name:
|
|
252
|
+
continue
|
|
253
|
+
extracted = extract_body(src_path, str(name), str(lang))
|
|
254
|
+
if extracted is None:
|
|
255
|
+
continue
|
|
256
|
+
if max_body_chars is None:
|
|
257
|
+
prop["body"] = extracted.body
|
|
258
|
+
prop["body_truncated"] = False
|
|
259
|
+
else:
|
|
260
|
+
prop["body"] = extracted.body[:max_body_chars]
|
|
261
|
+
prop["body_truncated"] = len(extracted.body) > max_body_chars
|
|
262
|
+
prop["imports"] = extracted.imports
|
|
263
|
+
prop["callers_of"] = extracted.callers_of
|
|
264
|
+
prop["exceptions_raised"] = extracted.exceptions_raised
|
|
265
|
+
prop["sibling_refs"] = extracted.sibling_refs
|
|
266
|
+
prop["has_self_assign"] = extracted.has_self_assign
|
|
267
|
+
prop["has_class_attr_collections"] = extracted.has_class_attr_collections
|
|
268
|
+
|
|
269
|
+
# Body-aware tier promotion: a class with mutable instance state
|
|
270
|
+
# belongs in a2_mo_composites, not a1_at_functions. Only promote;
|
|
271
|
+
# don't demote (the assimilator may have legitimately placed it
|
|
272
|
+
# higher because of name signals).
|
|
273
|
+
kind = str(sym.get("kind") or "").lower()
|
|
274
|
+
if (kind in ("class", "type")
|
|
275
|
+
and (extracted.has_self_assign
|
|
276
|
+
or extracted.has_class_attr_collections)
|
|
277
|
+
and prop.get("tier") == "a1_at_functions"):
|
|
278
|
+
prop["tier"] = "a2_mo_composites"
|
|
279
|
+
prop["tier_promotion_reason"] = (
|
|
280
|
+
"body has self.<attr> = … or class-level mutable collection — "
|
|
281
|
+
"ASS-ADE law promotes from a1 to a2"
|
|
282
|
+
)
|
|
283
|
+
# Re-stamp the component id so downstream stem/manifest paths
|
|
284
|
+
# match the new tier prefix.
|
|
285
|
+
cid = str(prop.get("id") or "")
|
|
286
|
+
if cid.startswith("a1."):
|
|
287
|
+
prop["id"] = "a2." + cid[3:]
|
|
288
|
+
return plan
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def derive_made_of_graph(plan: dict[str, Any]) -> dict[str, Any]:
|
|
292
|
+
"""Populate each component's ``made_of`` from call-graph analysis. Mutates ``plan``."""
|
|
293
|
+
by_name: dict[str, str] = {}
|
|
294
|
+
for prop in plan.get("proposed_components") or []:
|
|
295
|
+
name = (prop.get("name") or "").lower()
|
|
296
|
+
if name:
|
|
297
|
+
by_name[name] = str(prop["id"])
|
|
298
|
+
|
|
299
|
+
for prop in plan.get("proposed_components") or []:
|
|
300
|
+
made_of: list[str] = list(prop.get("made_of") or [])
|
|
301
|
+
for callee in prop.get("callers_of") or []:
|
|
302
|
+
target_id = by_name.get((callee or "").lower())
|
|
303
|
+
if target_id and target_id != prop["id"] and target_id not in made_of:
|
|
304
|
+
made_of.append(target_id)
|
|
305
|
+
prop["made_of"] = made_of
|
|
306
|
+
return plan
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Tier a1 — render a Forge Receipt as a 60×24 box-drawing card.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane A W1 (paired with ``receipt_emitter.py``). This
|
|
4
|
+
renderer is what the "62 → 5" 30-second viral demo (Lane E W2)
|
|
5
|
+
screen-grabs and what ``forge auto`` / ``forge certify`` print at the
|
|
6
|
+
bottom of a successful run.
|
|
7
|
+
|
|
8
|
+
Pure: takes a Receipt dict and returns a string. No I/O, no imports
|
|
9
|
+
above a0. Snapshot-tested at ~60 columns wide; height is variable
|
|
10
|
+
but bounded (target ≤ 24 rows for 'fits in one mobile screenshot').
|
|
11
|
+
|
|
12
|
+
Style notes (so the output stays visually consistent across releases):
|
|
13
|
+
* Single-line box drawing characters (U+2500..U+257F)
|
|
14
|
+
* Title bar uses U+2550 (heavy horizontal) for visual weight
|
|
15
|
+
* Verdict color hint emitted as a leading symbol (✓ / ✗ / ↻ / ⏸)
|
|
16
|
+
so terminals without color can still distinguish the four
|
|
17
|
+
verdicts. Caller may wrap with ANSI if desired.
|
|
18
|
+
* Numeric fields right-aligned; labels left-aligned; one column of
|
|
19
|
+
padding per side. No tabs, no trailing whitespace per row.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ..a0_qk_constants.receipt_schema import VALID_VERDICTS, ForgeReceiptV1
|
|
26
|
+
|
|
27
|
+
_VERDICT_GLYPH: dict[str, str] = {
|
|
28
|
+
"PASS": "✓",
|
|
29
|
+
"FAIL": "✗",
|
|
30
|
+
"REFINE": "↻",
|
|
31
|
+
"QUARANTINE": "⏸",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
36
|
+
"""Truncate ``text`` to ``max_len`` characters, adding an ellipsis
|
|
37
|
+
when shortened. Returns ``text`` unchanged when already short enough.
|
|
38
|
+
"""
|
|
39
|
+
if max_len <= 1:
|
|
40
|
+
return text[:max_len]
|
|
41
|
+
return text if len(text) <= max_len else text[: max_len - 1] + "…"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _row(left: str, right: str, *, width: int) -> str:
|
|
45
|
+
"""Format a single content row inside a card of total ``width``.
|
|
46
|
+
|
|
47
|
+
Layout: │ <left> .... <right> │
|
|
48
|
+
Inner width = width - 4 (two box chars + two single-space pads).
|
|
49
|
+
"""
|
|
50
|
+
inner = width - 4
|
|
51
|
+
if inner <= 0:
|
|
52
|
+
return ""
|
|
53
|
+
if not right:
|
|
54
|
+
return f"│ {_truncate(left, inner):<{inner}} │"
|
|
55
|
+
pad = inner - len(left) - len(right)
|
|
56
|
+
if pad < 1:
|
|
57
|
+
# Right-align right; truncate left.
|
|
58
|
+
max_left = max(0, inner - len(right) - 1)
|
|
59
|
+
left = _truncate(left, max_left)
|
|
60
|
+
pad = inner - len(left) - len(right)
|
|
61
|
+
pad = max(1, pad)
|
|
62
|
+
return f"│ {left}{' ' * pad}{right} │"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _hr(width: int, *, heavy: bool = False) -> str:
|
|
66
|
+
char = "═" if heavy else "─"
|
|
67
|
+
return ("╔" if heavy else "├") + char * (width - 2) + ("╗" if heavy else "┤")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _top(width: int) -> str:
|
|
71
|
+
return "╔" + "═" * (width - 2) + "╗"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _bottom(width: int) -> str:
|
|
75
|
+
return "╚" + "═" * (width - 2) + "╝"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _mid(width: int) -> str:
|
|
79
|
+
return "├" + "─" * (width - 2) + "┤"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _verdict_line(receipt: ForgeReceiptV1, *, width: int) -> str:
|
|
83
|
+
verdict = str(receipt.get("verdict", "FAIL"))
|
|
84
|
+
glyph = _VERDICT_GLYPH.get(verdict, "?")
|
|
85
|
+
return _row(f"{glyph} {verdict}",
|
|
86
|
+
f"forge {receipt.get('forge_version', '?')}",
|
|
87
|
+
width=width)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _certify_lines(receipt: ForgeReceiptV1, *, width: int) -> list[str]:
|
|
91
|
+
cert: dict[str, Any] = dict(receipt.get("certify", {})) # type: ignore[arg-type]
|
|
92
|
+
score = cert.get("score", 0.0)
|
|
93
|
+
axes: dict[str, Any] = dict(cert.get("axes", {})) # type: ignore[arg-type]
|
|
94
|
+
flag_glyph = lambda b: "✓" if b else "✗" # noqa: E731
|
|
95
|
+
rows: list[str] = []
|
|
96
|
+
rows.append(_row("CERTIFY", f"{score:>5.1f} / 100", width=width))
|
|
97
|
+
rows.append(_row(
|
|
98
|
+
f" docs {flag_glyph(axes.get('documentation_complete'))} "
|
|
99
|
+
f"tests {flag_glyph(axes.get('tests_present'))} "
|
|
100
|
+
f"layout {flag_glyph(axes.get('tier_layout_present'))} "
|
|
101
|
+
f"wire {flag_glyph(axes.get('no_upward_imports'))}",
|
|
102
|
+
"",
|
|
103
|
+
width=width,
|
|
104
|
+
))
|
|
105
|
+
return rows
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _wire_line(receipt: ForgeReceiptV1, *, width: int) -> str:
|
|
109
|
+
wire: dict[str, Any] = dict(receipt.get("wire", {})) # type: ignore[arg-type]
|
|
110
|
+
verdict = wire.get("verdict", "FAIL")
|
|
111
|
+
n = wire.get("violation_count", 0)
|
|
112
|
+
fixable = wire.get("auto_fixable", 0)
|
|
113
|
+
if fixable:
|
|
114
|
+
return _row("WIRE", f"{verdict} ({n} viol, {fixable} auto-fix)",
|
|
115
|
+
width=width)
|
|
116
|
+
return _row("WIRE", f"{verdict} ({n} violation{'' if n == 1 else 's'})",
|
|
117
|
+
width=width)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _scout_line(receipt: ForgeReceiptV1, *, width: int) -> str:
|
|
121
|
+
scout: dict[str, Any] = dict(receipt.get("scout", {})) # type: ignore[arg-type]
|
|
122
|
+
sym = scout.get("symbol_count", 0)
|
|
123
|
+
lang = scout.get("primary_language", "?")
|
|
124
|
+
tiers = scout.get("tier_distribution", {}) or {}
|
|
125
|
+
tier_total = sum(int(v) for v in tiers.values())
|
|
126
|
+
return _row(
|
|
127
|
+
f"SCOUT {sym} symbol{'' if sym == 1 else 's'} ({lang})",
|
|
128
|
+
f"{tier_total} tier-placed",
|
|
129
|
+
width=width,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _project_line(receipt: ForgeReceiptV1, *, width: int) -> str:
|
|
134
|
+
proj: dict[str, Any] = dict(receipt.get("project", {})) # type: ignore[arg-type]
|
|
135
|
+
name = str(proj.get("name", "?"))
|
|
136
|
+
vcs: dict[str, Any] = dict(proj.get("vcs") or {}) # type: ignore[arg-type]
|
|
137
|
+
short = vcs.get("short_sha") or vcs.get("head_sha", "")[:7]
|
|
138
|
+
branch = vcs.get("branch")
|
|
139
|
+
if short and branch:
|
|
140
|
+
right = f"{branch}@{short}"
|
|
141
|
+
elif short:
|
|
142
|
+
right = short
|
|
143
|
+
else:
|
|
144
|
+
right = ""
|
|
145
|
+
return _row(name, right, width=width)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _attestation_line(receipt: ForgeReceiptV1, *, width: int) -> str:
|
|
149
|
+
att: dict[str, Any] = dict(receipt.get("lean4_attestation") or {}) # type: ignore[arg-type]
|
|
150
|
+
total = att.get("total_theorems", 0)
|
|
151
|
+
if not total:
|
|
152
|
+
return _row("LEAN4 (no attestation)", "", width=width)
|
|
153
|
+
corpora = att.get("corpora") or []
|
|
154
|
+
corpus_count = len(corpora)
|
|
155
|
+
return _row(
|
|
156
|
+
f"LEAN4 {total} theorem{'' if total == 1 else 's'} "
|
|
157
|
+
f"across {corpus_count} corpus{'' if corpus_count == 1 else 'es'}",
|
|
158
|
+
"0 sorry",
|
|
159
|
+
width=width,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _signature_line(receipt: ForgeReceiptV1, *, width: int) -> str:
|
|
164
|
+
sigs: dict[str, Any] = dict(receipt.get("signatures") or {}) # type: ignore[arg-type]
|
|
165
|
+
has_sigstore = bool(sigs.get("sigstore"))
|
|
166
|
+
has_nexus = bool(sigs.get("aaaa_nexus"))
|
|
167
|
+
if has_sigstore and has_nexus:
|
|
168
|
+
return _row("SIGNED Sigstore + AAAA-Nexus", "", width=width)
|
|
169
|
+
if has_sigstore:
|
|
170
|
+
return _row("SIGNED Sigstore", "", width=width)
|
|
171
|
+
if has_nexus:
|
|
172
|
+
return _row("SIGNED AAAA-Nexus", "", width=width)
|
|
173
|
+
return _row("UNSIGNED (run --sign to attest)", "", width=width)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def render_receipt_card(
|
|
177
|
+
receipt: ForgeReceiptV1,
|
|
178
|
+
*,
|
|
179
|
+
width: int = 60,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Render a Receipt as a multi-line box-drawing card.
|
|
182
|
+
|
|
183
|
+
``width`` defaults to 60. Terminal widths < 40 will look cramped
|
|
184
|
+
(every label gets truncated); the function never raises on small
|
|
185
|
+
widths but the output is best-effort below 40.
|
|
186
|
+
"""
|
|
187
|
+
if width < 20:
|
|
188
|
+
raise ValueError("card width must be >= 20")
|
|
189
|
+
verdict = str(receipt.get("verdict", "FAIL"))
|
|
190
|
+
if verdict not in VALID_VERDICTS:
|
|
191
|
+
verdict = "FAIL"
|
|
192
|
+
|
|
193
|
+
lines: list[str] = []
|
|
194
|
+
lines.append(_top(width))
|
|
195
|
+
title = "Atomadic Forge Receipt"
|
|
196
|
+
lines.append(_row(title, receipt.get("schema_version", "?"), width=width))
|
|
197
|
+
lines.append(_mid(width))
|
|
198
|
+
lines.append(_verdict_line(receipt, width=width))
|
|
199
|
+
lines.append(_project_line(receipt, width=width))
|
|
200
|
+
lines.append(_mid(width))
|
|
201
|
+
lines.extend(_certify_lines(receipt, width=width))
|
|
202
|
+
lines.append(_wire_line(receipt, width=width))
|
|
203
|
+
lines.append(_scout_line(receipt, width=width))
|
|
204
|
+
lines.append(_mid(width))
|
|
205
|
+
lines.append(_attestation_line(receipt, width=width))
|
|
206
|
+
lines.append(_signature_line(receipt, width=width))
|
|
207
|
+
ts = str(receipt.get("generated_at_utc", "?"))
|
|
208
|
+
lines.append(_row("emitted", ts, width=width))
|
|
209
|
+
lines.append(_bottom(width))
|
|
210
|
+
return "\n".join(lines)
|