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,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)