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,81 @@
|
|
|
1
|
+
"""Tier a1 — progress reporter factories for long-running walks.
|
|
2
|
+
|
|
3
|
+
Provides callable factories that produce ``(idx, total, label) -> None``
|
|
4
|
+
reporters suitable for passing to ``scout_walk.harvest_repo`` (and any
|
|
5
|
+
future long-running pipeline phase).
|
|
6
|
+
|
|
7
|
+
Pure-by-construction: the factories themselves perform no I/O. The
|
|
8
|
+
returned reporter writes to whatever stream is passed in (default
|
|
9
|
+
``sys.stderr``), which keeps the side effect at the *call site* under
|
|
10
|
+
the caller's control. Tests can pass an ``io.StringIO``; the CLI layer
|
|
11
|
+
passes ``sys.stderr``; ``progress=None`` skips reporting entirely.
|
|
12
|
+
|
|
13
|
+
Used by Lane B2 of the post-audit plan: long-running ``forge auto`` /
|
|
14
|
+
``forge recon`` invocations on large repos previously gave no on-screen
|
|
15
|
+
feedback, so users couldn't tell the tool from a hung process. This
|
|
16
|
+
module is the cheap fix.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from typing import IO
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_stderr_reporter(
|
|
26
|
+
*,
|
|
27
|
+
every: int = 25,
|
|
28
|
+
enabled: bool | None = None,
|
|
29
|
+
stream: IO[str] | None = None,
|
|
30
|
+
label: str = "scout",
|
|
31
|
+
) -> Callable[[int, int, str], None]:
|
|
32
|
+
"""Return a progress callback that writes to ``stream`` (default stderr).
|
|
33
|
+
|
|
34
|
+
Arguments:
|
|
35
|
+
every: Emit a line every Nth file (plus always at the final
|
|
36
|
+
file). Keep ≥ 1.
|
|
37
|
+
enabled: Force-enable / disable. ``None`` = auto-detect: emit
|
|
38
|
+
only when ``stream`` is a TTY. CI tasks (non-TTY) get
|
|
39
|
+
silence by default; an explicit ``enabled=True`` forces
|
|
40
|
+
output regardless.
|
|
41
|
+
stream: Defaults to ``sys.stderr``.
|
|
42
|
+
label: Short tag included in each line (``scout``, etc.).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A pure callback ``progress(idx, total, rel)`` suitable for
|
|
46
|
+
passing into ``harvest_repo``. When the reporter is disabled
|
|
47
|
+
(TTY check fails and ``enabled is not True``), the callback is
|
|
48
|
+
a no-op — *not* ``None`` — so callers can pass it unconditionally.
|
|
49
|
+
"""
|
|
50
|
+
if every < 1:
|
|
51
|
+
raise ValueError("`every` must be >= 1")
|
|
52
|
+
|
|
53
|
+
target = stream if stream is not None else sys.stderr
|
|
54
|
+
if enabled is None:
|
|
55
|
+
try:
|
|
56
|
+
active = bool(target.isatty())
|
|
57
|
+
except (AttributeError, ValueError):
|
|
58
|
+
active = False
|
|
59
|
+
else:
|
|
60
|
+
active = bool(enabled)
|
|
61
|
+
|
|
62
|
+
if not active:
|
|
63
|
+
def _noop(idx: int, total: int, rel: str) -> None:
|
|
64
|
+
return None
|
|
65
|
+
return _noop
|
|
66
|
+
|
|
67
|
+
def _report(idx: int, total: int, rel: str) -> None:
|
|
68
|
+
# Emit on every Nth file, plus always on the final one so the
|
|
69
|
+
# user sees the closing 100%-of-N line.
|
|
70
|
+
if idx == total or (idx % every) == 0:
|
|
71
|
+
try:
|
|
72
|
+
target.write(
|
|
73
|
+
f" [{label}] processed {idx}/{total} files "
|
|
74
|
+
f"(latest: {rel})\n"
|
|
75
|
+
)
|
|
76
|
+
target.flush()
|
|
77
|
+
except (OSError, ValueError):
|
|
78
|
+
# Stream closed or detached mid-run — degrade silently.
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return _report
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Tier a1 — pure helpers for LLM provider detection and connection testing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import http.client
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def detect_ollama(url: str) -> dict:
|
|
13
|
+
"""Ping Ollama at url; return availability, model list, and latency."""
|
|
14
|
+
t0 = time.monotonic()
|
|
15
|
+
try:
|
|
16
|
+
tags_url = f"{url.rstrip('/')}/api/tags"
|
|
17
|
+
with urllib.request.urlopen(tags_url, timeout=5) as resp:
|
|
18
|
+
data = json.loads(resp.read())
|
|
19
|
+
models = [m["name"] for m in data.get("models", [])]
|
|
20
|
+
latency_ms = int((time.monotonic() - t0) * 1000)
|
|
21
|
+
return {"available": True, "models": models, "url": url, "latency_ms": latency_ms}
|
|
22
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError, KeyError):
|
|
23
|
+
return {"available": False, "models": [], "url": url, "latency_ms": 0}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_ollama_models(url: str) -> list[str]:
|
|
27
|
+
"""Return model names available from Ollama at url."""
|
|
28
|
+
return detect_ollama(url)["models"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_provider(provider: str, config: dict) -> dict:
|
|
32
|
+
"""Test an LLM provider connection; return {ok, model, error, latency_ms}."""
|
|
33
|
+
t0 = time.monotonic()
|
|
34
|
+
|
|
35
|
+
if provider == "ollama":
|
|
36
|
+
return _test_ollama(config, t0)
|
|
37
|
+
|
|
38
|
+
if provider == "gemini":
|
|
39
|
+
return _test_gemini(config, t0)
|
|
40
|
+
|
|
41
|
+
if provider in ("anthropic", "claude"):
|
|
42
|
+
return _test_anthropic(config, t0)
|
|
43
|
+
|
|
44
|
+
if provider in ("openai", "gpt"):
|
|
45
|
+
return _test_openai(config, t0)
|
|
46
|
+
|
|
47
|
+
if provider == "auto":
|
|
48
|
+
return _test_auto(config, t0)
|
|
49
|
+
|
|
50
|
+
if provider == "stub":
|
|
51
|
+
return {"ok": True, "model": "stub", "error": None,
|
|
52
|
+
"latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
53
|
+
|
|
54
|
+
return {"ok": False, "model": "none",
|
|
55
|
+
"error": f"Unknown provider {provider!r}", "latency_ms": 0}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── private helpers ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def _test_ollama(config: dict, t0: float) -> dict:
|
|
61
|
+
url = config.get("ollama_url", "http://localhost:11434")
|
|
62
|
+
model = config.get("ollama_model", "mistral:7b-instruct")
|
|
63
|
+
info = detect_ollama(url)
|
|
64
|
+
latency_ms = int((time.monotonic() - t0) * 1000)
|
|
65
|
+
if not info["available"]:
|
|
66
|
+
return {"ok": False, "model": model,
|
|
67
|
+
"error": f"Ollama not reachable at {url}", "latency_ms": latency_ms}
|
|
68
|
+
if model not in info["models"] and info["models"]:
|
|
69
|
+
return {"ok": False, "model": model,
|
|
70
|
+
"error": f"Model {model!r} not found (available: {info['models']})",
|
|
71
|
+
"latency_ms": latency_ms}
|
|
72
|
+
return {"ok": True, "model": model, "error": None, "latency_ms": latency_ms}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _test_gemini(config: dict, t0: float) -> dict:
|
|
76
|
+
key = config.get("gemini_key") or ""
|
|
77
|
+
model = config.get("gemini_model") or "gemini-2.5-flash"
|
|
78
|
+
if not key:
|
|
79
|
+
return {"ok": False, "model": model, "error": "gemini_key not set", "latency_ms": 0}
|
|
80
|
+
try:
|
|
81
|
+
req = urllib.request.Request(
|
|
82
|
+
f"https://generativelanguage.googleapis.com/v1beta/models?key={key}",
|
|
83
|
+
headers={"Accept": "application/json"},
|
|
84
|
+
)
|
|
85
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
86
|
+
resp.read()
|
|
87
|
+
return {"ok": True, "model": model, "error": None,
|
|
88
|
+
"latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
89
|
+
except urllib.error.HTTPError as exc:
|
|
90
|
+
return {"ok": False, "model": model,
|
|
91
|
+
"error": f"HTTP {exc.code}", "latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
92
|
+
except (urllib.error.URLError, OSError, http.client.HTTPException) as exc:
|
|
93
|
+
return {"ok": False, "model": model,
|
|
94
|
+
"error": str(exc), "latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _test_anthropic(config: dict, t0: float) -> dict:
|
|
98
|
+
key = config.get("anthropic_key") or ""
|
|
99
|
+
model = config.get("anthropic_model") or "claude-sonnet-4-6"
|
|
100
|
+
if not key:
|
|
101
|
+
return {"ok": False, "model": model, "error": "anthropic_key not set", "latency_ms": 0}
|
|
102
|
+
try:
|
|
103
|
+
req = urllib.request.Request(
|
|
104
|
+
"https://api.anthropic.com/v1/models",
|
|
105
|
+
headers={"x-api-key": key, "anthropic-version": "2023-06-01"},
|
|
106
|
+
)
|
|
107
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
108
|
+
resp.read()
|
|
109
|
+
return {"ok": True, "model": model, "error": None,
|
|
110
|
+
"latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
111
|
+
except urllib.error.HTTPError as exc:
|
|
112
|
+
return {"ok": False, "model": model,
|
|
113
|
+
"error": f"HTTP {exc.code}", "latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
114
|
+
except (urllib.error.URLError, OSError, http.client.HTTPException) as exc:
|
|
115
|
+
return {"ok": False, "model": model,
|
|
116
|
+
"error": str(exc), "latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _test_openai(config: dict, t0: float) -> dict:
|
|
120
|
+
key = config.get("openai_key") or ""
|
|
121
|
+
model = config.get("openai_model") or "gpt-4o-mini"
|
|
122
|
+
if not key:
|
|
123
|
+
return {"ok": False, "model": model, "error": "openai_key not set", "latency_ms": 0}
|
|
124
|
+
try:
|
|
125
|
+
req = urllib.request.Request(
|
|
126
|
+
"https://api.openai.com/v1/models",
|
|
127
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
128
|
+
)
|
|
129
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
130
|
+
resp.read()
|
|
131
|
+
return {"ok": True, "model": model, "error": None,
|
|
132
|
+
"latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
133
|
+
except urllib.error.HTTPError as exc:
|
|
134
|
+
return {"ok": False, "model": model,
|
|
135
|
+
"error": f"HTTP {exc.code}", "latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
136
|
+
except (urllib.error.URLError, OSError, http.client.HTTPException) as exc:
|
|
137
|
+
return {"ok": False, "model": model,
|
|
138
|
+
"error": str(exc), "latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _test_auto(config: dict, t0: float) -> dict:
|
|
142
|
+
url = config.get("ollama_url", "http://localhost:11434")
|
|
143
|
+
info = detect_ollama(url)
|
|
144
|
+
if info["available"]:
|
|
145
|
+
model = config.get("ollama_model", "mistral:7b-instruct")
|
|
146
|
+
return {"ok": True, "model": f"ollama/{model}", "error": None,
|
|
147
|
+
"latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
148
|
+
for p, key_field in (
|
|
149
|
+
("gemini", "gemini_key"),
|
|
150
|
+
("anthropic", "anthropic_key"),
|
|
151
|
+
("openai", "openai_key"),
|
|
152
|
+
):
|
|
153
|
+
if config.get(key_field):
|
|
154
|
+
return test_provider(p, config)
|
|
155
|
+
return {"ok": False, "model": "none",
|
|
156
|
+
"error": "No provider available (Ollama unreachable, no API keys set)",
|
|
157
|
+
"latency_ms": int((time.monotonic() - t0) * 1000)}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tier a1 — shared LLM provider resolver."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .llm_client import (
|
|
8
|
+
AAAANexusClient,
|
|
9
|
+
AnthropicClient,
|
|
10
|
+
GeminiClient,
|
|
11
|
+
LLMClient,
|
|
12
|
+
OllamaClient,
|
|
13
|
+
OpenAIClient,
|
|
14
|
+
OpenRouterClient,
|
|
15
|
+
StubLLMClient,
|
|
16
|
+
resolve_default_client,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
PROVIDER_HELP = (
|
|
20
|
+
"auto | nexus | aaaa-nexus | gemini | anthropic | openai | "
|
|
21
|
+
"openrouter | ollama | stub"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_provider(name: str = "auto") -> LLMClient:
|
|
26
|
+
"""Resolve a user-facing provider name to an ``LLMClient`` instance."""
|
|
27
|
+
provider = (name or "auto").lower()
|
|
28
|
+
if provider == "stub":
|
|
29
|
+
return StubLLMClient()
|
|
30
|
+
if provider in ("nexus", "aaaa-nexus", "aaaa_nexus", "helix"):
|
|
31
|
+
return AAAANexusClient()
|
|
32
|
+
if provider in ("anthropic", "claude"):
|
|
33
|
+
return AnthropicClient()
|
|
34
|
+
if provider in ("gemini", "google"):
|
|
35
|
+
return GeminiClient(model=os.environ.get("FORGE_GEMINI_MODEL",
|
|
36
|
+
"gemini-2.5-flash"))
|
|
37
|
+
if provider in ("openai", "gpt"):
|
|
38
|
+
return OpenAIClient()
|
|
39
|
+
if provider in ("openrouter", "router"):
|
|
40
|
+
return OpenRouterClient()
|
|
41
|
+
if provider == "ollama":
|
|
42
|
+
return OllamaClient(
|
|
43
|
+
model=os.environ.get("FORGE_OLLAMA_MODEL", "qwen2.5-coder:7b"),
|
|
44
|
+
base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"),
|
|
45
|
+
)
|
|
46
|
+
if provider == "auto":
|
|
47
|
+
return resolve_default_client()
|
|
48
|
+
raise ValueError(f"unknown provider: {name!r}; expected {PROVIDER_HELP}")
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Tier a1 — pure CertifyResult+WireReport+ScoutReport → Receipt v1.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane A W1 (paired with ``card_renderer.py``).
|
|
4
|
+
|
|
5
|
+
Pure: no I/O. Takes already-computed report dicts (the same ones
|
|
6
|
+
``forge certify`` and ``forge wire`` already produce) plus a few
|
|
7
|
+
context strings, returns a ``ForgeReceiptV1`` dict ready to be
|
|
8
|
+
``json.dumps``-ed and either signed (Lane A W2 ``receipt_signer``)
|
|
9
|
+
or shipped as-is for local development.
|
|
10
|
+
|
|
11
|
+
The emitter never decides whether a Receipt should be signed. That's
|
|
12
|
+
a policy call the CLI layer makes. Same for lineage — Vanguard's
|
|
13
|
+
``/v1/forge/lineage`` lookup happens at a2; the emitter accepts a
|
|
14
|
+
pre-resolved ``lineage`` dict if the caller has one.
|
|
15
|
+
|
|
16
|
+
Verdict-decision contract (matches ``docs/RECEIPT.md`` §"How verdict
|
|
17
|
+
is decided"):
|
|
18
|
+
|
|
19
|
+
verdict = PASS when wire.verdict == 'PASS'
|
|
20
|
+
AND certify.score >= certify_threshold
|
|
21
|
+
verdict = FAIL when wire.verdict == 'FAIL'
|
|
22
|
+
OR certify.score < certify_threshold
|
|
23
|
+
verdict = REFINE when caller passed an explicit
|
|
24
|
+
override='REFINE' (iterate stagnation case)
|
|
25
|
+
verdict = QUARANTINE when caller passed an explicit
|
|
26
|
+
override='QUARANTINE' (hysteresis ratchet
|
|
27
|
+
> 0.5; reserved for Lane E)
|
|
28
|
+
|
|
29
|
+
The emitter never invents REFINE / QUARANTINE on its own.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import datetime as _dt
|
|
34
|
+
import hashlib
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from ..a0_qk_constants.receipt_schema import (
|
|
39
|
+
REQUIRED_RECEIPT_V1_FIELDS,
|
|
40
|
+
SCHEMA_VERSION_V1,
|
|
41
|
+
VALID_VERDICTS,
|
|
42
|
+
ForgeReceiptV1,
|
|
43
|
+
ReceiptArtifact,
|
|
44
|
+
ReceiptCertify,
|
|
45
|
+
ReceiptCertifyAxes,
|
|
46
|
+
ReceiptLean4Attestation,
|
|
47
|
+
ReceiptLineage,
|
|
48
|
+
ReceiptPolyglotBreakdown,
|
|
49
|
+
ReceiptProject,
|
|
50
|
+
ReceiptScout,
|
|
51
|
+
ReceiptSignatures,
|
|
52
|
+
ReceiptWire,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_DEFAULT_THRESHOLD = 100.0
|
|
56
|
+
_ARTIFACT_FILES = ("scout", "cherry", "wire", "certify", "assimilate")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _now_utc_iso() -> str:
|
|
60
|
+
"""Return the current UTC instant as YYYY-MM-DDTHH:MM:SSZ."""
|
|
61
|
+
return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _decide_verdict(
|
|
65
|
+
*,
|
|
66
|
+
wire_verdict: str,
|
|
67
|
+
certify_score: float,
|
|
68
|
+
threshold: float,
|
|
69
|
+
override: str | None,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""See module docstring's verdict-decision contract."""
|
|
72
|
+
if override is not None:
|
|
73
|
+
if override not in VALID_VERDICTS:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"verdict override {override!r} not in {VALID_VERDICTS}"
|
|
76
|
+
)
|
|
77
|
+
return override
|
|
78
|
+
if wire_verdict == "PASS" and certify_score >= threshold:
|
|
79
|
+
return "PASS"
|
|
80
|
+
return "FAIL"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _hash_file(path: Path) -> str | None:
|
|
84
|
+
"""Return the SHA-256 hex digest of a file, or None on read failure.
|
|
85
|
+
|
|
86
|
+
Pure-ish: deterministic given file contents. The 'I/O' here is a
|
|
87
|
+
bounded read of a known artifact path the caller controls; no
|
|
88
|
+
network. Keeps the emitter callable in cheap-mode (caller passes
|
|
89
|
+
``compute_artifact_hashes=False`` and we skip).
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
h = hashlib.sha256()
|
|
93
|
+
with path.open("rb") as f:
|
|
94
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
95
|
+
h.update(chunk)
|
|
96
|
+
return h.hexdigest()
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _gather_artifacts(
|
|
102
|
+
project_root: Path,
|
|
103
|
+
*,
|
|
104
|
+
compute_hashes: bool,
|
|
105
|
+
) -> list[ReceiptArtifact]:
|
|
106
|
+
"""Build the artifacts pointer list from ``.atomadic-forge/`` files."""
|
|
107
|
+
base = project_root / ".atomadic-forge"
|
|
108
|
+
if not base.is_dir():
|
|
109
|
+
return []
|
|
110
|
+
out: list[ReceiptArtifact] = []
|
|
111
|
+
for name in _ARTIFACT_FILES:
|
|
112
|
+
target = base / f"{name}.json"
|
|
113
|
+
if not target.exists():
|
|
114
|
+
continue
|
|
115
|
+
sha = _hash_file(target) if compute_hashes else None
|
|
116
|
+
out.append(ReceiptArtifact(
|
|
117
|
+
name=name,
|
|
118
|
+
path=str(target.relative_to(project_root).as_posix()),
|
|
119
|
+
sha256=sha,
|
|
120
|
+
))
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _project_block(
|
|
125
|
+
*,
|
|
126
|
+
name: str,
|
|
127
|
+
root: Path,
|
|
128
|
+
package: str | None,
|
|
129
|
+
primary_language: str,
|
|
130
|
+
languages: dict[str, int],
|
|
131
|
+
vcs: dict[str, Any] | None,
|
|
132
|
+
) -> ReceiptProject:
|
|
133
|
+
block: ReceiptProject = ReceiptProject(
|
|
134
|
+
name=name,
|
|
135
|
+
root=str(root),
|
|
136
|
+
package=package,
|
|
137
|
+
language=primary_language,
|
|
138
|
+
languages=dict(languages),
|
|
139
|
+
)
|
|
140
|
+
if vcs is not None:
|
|
141
|
+
block["vcs"] = vcs # type: ignore[typeddict-item]
|
|
142
|
+
return block
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _certify_block(certify_result: dict) -> ReceiptCertify:
|
|
146
|
+
axes = ReceiptCertifyAxes(
|
|
147
|
+
documentation_complete=bool(certify_result.get("documentation_complete", False)),
|
|
148
|
+
tests_present=bool(certify_result.get("tests_present", False)),
|
|
149
|
+
tier_layout_present=bool(certify_result.get("tier_layout_present", False)),
|
|
150
|
+
no_upward_imports=bool(certify_result.get("no_upward_imports", False)),
|
|
151
|
+
)
|
|
152
|
+
return ReceiptCertify(
|
|
153
|
+
score=float(certify_result.get("score", 0.0)),
|
|
154
|
+
axes=axes,
|
|
155
|
+
issues=list(certify_result.get("issues", []) or []),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _wire_block(wire_report: dict) -> ReceiptWire:
|
|
160
|
+
verdict = wire_report.get("verdict", "FAIL")
|
|
161
|
+
if verdict not in ("PASS", "FAIL"):
|
|
162
|
+
verdict = "FAIL"
|
|
163
|
+
return ReceiptWire(
|
|
164
|
+
verdict=verdict,
|
|
165
|
+
violation_count=int(wire_report.get("violation_count", 0)),
|
|
166
|
+
auto_fixable=int(wire_report.get("auto_fixable", 0)),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _scout_block(scout_report: dict) -> ReceiptScout:
|
|
171
|
+
return ReceiptScout(
|
|
172
|
+
symbol_count=int(scout_report.get("symbol_count", 0)),
|
|
173
|
+
tier_distribution=dict(scout_report.get("tier_distribution", {}) or {}),
|
|
174
|
+
effect_distribution=dict(scout_report.get("effect_distribution", {}) or {}),
|
|
175
|
+
primary_language=str(scout_report.get("primary_language", "python")),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _polyglot_breakdown(scout_report: dict) -> ReceiptPolyglotBreakdown:
|
|
180
|
+
"""Codex-6 — Receipt v1.1 polyglot_breakdown seed.
|
|
181
|
+
|
|
182
|
+
Pulls per-language file + symbol counts from the scout report.
|
|
183
|
+
Per-language certify scores ship in a future minor (Lane A W8 has
|
|
184
|
+
that on the roadmap once the JS/TS behavioural-pytest gate lands).
|
|
185
|
+
"""
|
|
186
|
+
languages = (scout_report.get("language_distribution")
|
|
187
|
+
or scout_report.get("languages") or {})
|
|
188
|
+
symbols_by_lang: dict[str, int] = {}
|
|
189
|
+
for s in scout_report.get("symbols", []) or []:
|
|
190
|
+
lang = s.get("language", "python")
|
|
191
|
+
symbols_by_lang[lang] = symbols_by_lang.get(lang, 0) + 1
|
|
192
|
+
return ReceiptPolyglotBreakdown(
|
|
193
|
+
file_count=sum(int(v) for v in languages.values()),
|
|
194
|
+
languages=dict(languages),
|
|
195
|
+
symbol_count=int(scout_report.get("symbol_count", 0)),
|
|
196
|
+
symbols_by_language=symbols_by_lang,
|
|
197
|
+
primary_language=str(scout_report.get("primary_language", "python")),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_receipt(
|
|
202
|
+
*,
|
|
203
|
+
certify_result: dict,
|
|
204
|
+
wire_report: dict,
|
|
205
|
+
scout_report: dict,
|
|
206
|
+
project_name: str,
|
|
207
|
+
project_root: Path,
|
|
208
|
+
forge_version: str,
|
|
209
|
+
package: str | None = None,
|
|
210
|
+
vcs: dict[str, Any] | None = None,
|
|
211
|
+
assimilate_digest: str | None = None,
|
|
212
|
+
signatures: ReceiptSignatures | None = None,
|
|
213
|
+
lean4_attestation: ReceiptLean4Attestation | None = None,
|
|
214
|
+
lineage: ReceiptLineage | None = None,
|
|
215
|
+
compliance_mappings: dict[str, str] | None = None,
|
|
216
|
+
notes: list[str] | None = None,
|
|
217
|
+
extra: dict[str, object] | None = None,
|
|
218
|
+
certify_threshold: float = _DEFAULT_THRESHOLD,
|
|
219
|
+
verdict_override: str | None = None,
|
|
220
|
+
compute_artifact_hashes: bool = True,
|
|
221
|
+
) -> ForgeReceiptV1:
|
|
222
|
+
"""Build a v1 Forge Receipt from already-computed reports.
|
|
223
|
+
|
|
224
|
+
Required inputs:
|
|
225
|
+
certify_result, wire_report, scout_report, project_name,
|
|
226
|
+
project_root, forge_version.
|
|
227
|
+
|
|
228
|
+
Everything else is optional and defaults to a structurally-valid
|
|
229
|
+
empty / None per the v1.0 spec. Producers MAY emit unsigned
|
|
230
|
+
Receipts; this is the local-development path.
|
|
231
|
+
|
|
232
|
+
Returns a TypedDict suitable for ``json.dumps`` (every value is
|
|
233
|
+
JSON-serializable: str, int, float, bool, None, list, dict).
|
|
234
|
+
"""
|
|
235
|
+
languages = scout_report.get("language_distribution") or scout_report.get("languages") or {}
|
|
236
|
+
primary = scout_report.get("primary_language", "python")
|
|
237
|
+
receipt: ForgeReceiptV1 = ForgeReceiptV1(
|
|
238
|
+
schema_version=SCHEMA_VERSION_V1,
|
|
239
|
+
generated_at_utc=_now_utc_iso(),
|
|
240
|
+
forge_version=forge_version,
|
|
241
|
+
verdict=_decide_verdict(
|
|
242
|
+
wire_verdict=wire_report.get("verdict", "FAIL"),
|
|
243
|
+
certify_score=float(certify_result.get("score", 0.0)),
|
|
244
|
+
threshold=certify_threshold,
|
|
245
|
+
override=verdict_override,
|
|
246
|
+
),
|
|
247
|
+
project=_project_block(
|
|
248
|
+
name=project_name,
|
|
249
|
+
root=Path(project_root).resolve(),
|
|
250
|
+
package=package,
|
|
251
|
+
primary_language=primary,
|
|
252
|
+
languages=dict(languages),
|
|
253
|
+
vcs=vcs,
|
|
254
|
+
),
|
|
255
|
+
certify=_certify_block(certify_result),
|
|
256
|
+
wire=_wire_block(wire_report),
|
|
257
|
+
scout=_scout_block(scout_report),
|
|
258
|
+
assimilate_digest=assimilate_digest,
|
|
259
|
+
artifacts=_gather_artifacts(
|
|
260
|
+
Path(project_root).resolve(),
|
|
261
|
+
compute_hashes=compute_artifact_hashes,
|
|
262
|
+
),
|
|
263
|
+
signatures=signatures or ReceiptSignatures(sigstore=None, aaaa_nexus=None),
|
|
264
|
+
lean4_attestation=lean4_attestation or ReceiptLean4Attestation(),
|
|
265
|
+
lineage=lineage or ReceiptLineage(),
|
|
266
|
+
compliance_mappings=dict(compliance_mappings or {}),
|
|
267
|
+
notes=list(notes or []),
|
|
268
|
+
extra=dict(extra or {}),
|
|
269
|
+
# Codex-6 / Lane A W8 seed: per-language file + symbol breakdown.
|
|
270
|
+
polyglot_breakdown=_polyglot_breakdown(scout_report),
|
|
271
|
+
)
|
|
272
|
+
# Spot-check that we populated every required v1 field. Defensive
|
|
273
|
+
# — TypedDict does not enforce at runtime.
|
|
274
|
+
for f in REQUIRED_RECEIPT_V1_FIELDS:
|
|
275
|
+
if f not in receipt:
|
|
276
|
+
raise RuntimeError(
|
|
277
|
+
f"receipt_emitter built a Receipt missing required field "
|
|
278
|
+
f"{f!r} — schema/emitter drift"
|
|
279
|
+
)
|
|
280
|
+
return receipt
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def receipt_to_json(receipt: ForgeReceiptV1, *, indent: int = 2) -> str:
|
|
284
|
+
"""Serialize a Receipt to a stable JSON string.
|
|
285
|
+
|
|
286
|
+
Sorted-keys NOT used — the emitter writes fields in spec order so
|
|
287
|
+
the wire format is human-readable. ``indent`` defaults to 2 to
|
|
288
|
+
match Forge's other manifest writers.
|
|
289
|
+
"""
|
|
290
|
+
import json
|
|
291
|
+
return json.dumps(receipt, indent=indent, default=str)
|