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,130 @@
|
|
|
1
|
+
"""Tier a1 — compose_tools: tool-use planner (Codex #9).
|
|
2
|
+
|
|
3
|
+
Codex's prescription:
|
|
4
|
+
|
|
5
|
+
> Forge has synergy/emergent. Make that agent-native: compose_tools(
|
|
6
|
+
> {goal}). Returns a sequence like:
|
|
7
|
+
> 1. recon
|
|
8
|
+
> 2. summary
|
|
9
|
+
> 3. preflight_change
|
|
10
|
+
> 4. score_patch
|
|
11
|
+
> 5. select_tests
|
|
12
|
+
> 6. certify
|
|
13
|
+
> This makes Forge not just a toolbox, but a planner for tool use.
|
|
14
|
+
|
|
15
|
+
Pure: maps a goal-keyword to a sequence of MCP tool names with
|
|
16
|
+
short rationale per step. Heuristic; the agent picks one or runs
|
|
17
|
+
the whole sequence.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TypedDict
|
|
22
|
+
|
|
23
|
+
SCHEMA_VERSION_COMPOSE_V1 = "atomadic-forge.tool_compose/v1"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ComposedToolStep(TypedDict, total=False):
|
|
27
|
+
step: int
|
|
28
|
+
tool: str
|
|
29
|
+
why: str
|
|
30
|
+
inputs_hint: dict[str, str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ComposedToolPlan(TypedDict, total=False):
|
|
34
|
+
schema_version: str
|
|
35
|
+
goal: str
|
|
36
|
+
matched_recipe: str
|
|
37
|
+
steps: list[ComposedToolStep]
|
|
38
|
+
notes: list[str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Recipe library — keyword → ordered tool sequence.
|
|
42
|
+
_RECIPES: dict[str, dict] = {
|
|
43
|
+
"orient": {
|
|
44
|
+
"match": ("orient", "what is this", "explain", "first time",
|
|
45
|
+
"context", "onboard", "new repo"),
|
|
46
|
+
"steps": [
|
|
47
|
+
{"tool": "context_pack", "why": "first-call orientation"},
|
|
48
|
+
{"tool": "recon", "why": "tier inventory + symbol counts"},
|
|
49
|
+
{"tool": "audit_list", "why": "what has Forge written before?"},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
"release_check": {
|
|
53
|
+
"match": ("release", "ship", "publish", "merge", "ready to ship"),
|
|
54
|
+
"steps": [
|
|
55
|
+
{"tool": "wire", "why": "upward-import gate"},
|
|
56
|
+
{"tool": "certify", "why": "score against the 4 axes"},
|
|
57
|
+
{"tool": "score_patch", "why": "review the unified diff"},
|
|
58
|
+
{"tool": "select_tests", "why": "minimum + full test sets"},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
"fix_violation": {
|
|
62
|
+
"match": ("fix wire", "fix violation", "fix import", "f0042",
|
|
63
|
+
"f0041", "f0046", "upward import"),
|
|
64
|
+
"steps": [
|
|
65
|
+
{"tool": "wire", "why": "scan with --suggest-repairs"},
|
|
66
|
+
{"tool": "auto_plan",
|
|
67
|
+
"why": "generate ranked action cards"},
|
|
68
|
+
{"tool": "auto_apply",
|
|
69
|
+
"why": "execute applyable cards (rollback-safe)"},
|
|
70
|
+
{"tool": "certify", "why": "verify score recovered"},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
"before_edit": {
|
|
74
|
+
"match": ("before edit", "before write", "preflight", "guardrail",
|
|
75
|
+
"i'm about to"),
|
|
76
|
+
"steps": [
|
|
77
|
+
{"tool": "context_pack", "why": "ground the agent first"},
|
|
78
|
+
{"tool": "preflight_change",
|
|
79
|
+
"why": "tier check + forbidden imports + likely tests"},
|
|
80
|
+
{"tool": "select_tests",
|
|
81
|
+
"why": "what to run after the edit"},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
"verify_patch": {
|
|
85
|
+
"match": ("verify patch", "score diff", "review patch", "is this safe",
|
|
86
|
+
"review my change"),
|
|
87
|
+
"steps": [
|
|
88
|
+
{"tool": "score_patch",
|
|
89
|
+
"why": "architecture / api / release / test risk"},
|
|
90
|
+
{"tool": "wire", "why": "confirm no upward imports introduced"},
|
|
91
|
+
{"tool": "select_tests",
|
|
92
|
+
"why": "minimum tests for the changed files"},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def compose_tools(*, goal: str) -> ComposedToolPlan:
|
|
99
|
+
"""Match ``goal`` against the recipe library and return an ordered
|
|
100
|
+
tool-use plan. Falls back to the 'orient' recipe when no keyword
|
|
101
|
+
matches."""
|
|
102
|
+
goal_l = (goal or "").lower()
|
|
103
|
+
matched: str = ""
|
|
104
|
+
chosen: list[dict] | None = None
|
|
105
|
+
for name, recipe in _RECIPES.items():
|
|
106
|
+
if any(kw in goal_l for kw in recipe["match"]):
|
|
107
|
+
matched = name
|
|
108
|
+
chosen = recipe["steps"]
|
|
109
|
+
break
|
|
110
|
+
if chosen is None:
|
|
111
|
+
matched = "orient"
|
|
112
|
+
chosen = _RECIPES["orient"]["steps"]
|
|
113
|
+
steps: list[ComposedToolStep] = []
|
|
114
|
+
for i, s in enumerate(chosen, 1):
|
|
115
|
+
steps.append(ComposedToolStep(
|
|
116
|
+
step=i,
|
|
117
|
+
tool=s["tool"],
|
|
118
|
+
why=s["why"],
|
|
119
|
+
inputs_hint={},
|
|
120
|
+
))
|
|
121
|
+
notes: list[str] = []
|
|
122
|
+
if not goal:
|
|
123
|
+
notes.append("no goal supplied — defaulted to 'orient' recipe.")
|
|
124
|
+
return ComposedToolPlan(
|
|
125
|
+
schema_version=SCHEMA_VERSION_COMPOSE_V1,
|
|
126
|
+
goal=goal,
|
|
127
|
+
matched_recipe=matched,
|
|
128
|
+
steps=steps,
|
|
129
|
+
notes=notes,
|
|
130
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Tier a1 — append-only LLM transcript logger.
|
|
2
|
+
|
|
3
|
+
For full transparency, every prompt sent to the LLM and every response
|
|
4
|
+
received is appended to ``.atomadic-forge/transcripts/<run-id>.jsonl``.
|
|
5
|
+
|
|
6
|
+
Operators can audit exactly what Forge asked the LLM and exactly what the
|
|
7
|
+
LLM emitted. No black-box magic — every byte is on disk.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
log = TranscriptLog(project_root, run_id="evolve-20260427T0815")
|
|
11
|
+
log.append("system", system_prompt)
|
|
12
|
+
log.append("user", user_prompt, role="prompt")
|
|
13
|
+
log.append("assistant", llm_response, role="response")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import datetime as _dt
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TranscriptLog:
|
|
25
|
+
"""Append-only JSONL log of every LLM exchange in a Forge run."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, project_root: Path, run_id: str | None = None,
|
|
28
|
+
dirname: str = ".atomadic-forge") -> None:
|
|
29
|
+
self.project_root = Path(project_root).resolve()
|
|
30
|
+
self.run_id = run_id or _dt.datetime.now(_dt.timezone.utc).strftime(
|
|
31
|
+
"run-%Y%m%dT%H%M%S")
|
|
32
|
+
self.dir = self.project_root / dirname / "transcripts"
|
|
33
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
self.path = self.dir / f"{self.run_id}.jsonl"
|
|
35
|
+
self._turn = 0
|
|
36
|
+
if not self.path.exists():
|
|
37
|
+
self._write_meta()
|
|
38
|
+
|
|
39
|
+
def _write_meta(self) -> None:
|
|
40
|
+
self._raw_append({
|
|
41
|
+
"schema_version": "atomadic-forge.transcript/v1",
|
|
42
|
+
"ts_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
43
|
+
"kind": "meta",
|
|
44
|
+
"run_id": self.run_id,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
def append(self, kind: str, content: str, *,
|
|
48
|
+
role: str = "", llm: str = "",
|
|
49
|
+
extra: dict[str, Any] | None = None) -> None:
|
|
50
|
+
"""Append one entry.
|
|
51
|
+
|
|
52
|
+
``kind`` is e.g. ``"prompt"``, ``"response"``, ``"system"``, ``"meta"``.
|
|
53
|
+
``role`` is for chat-shape compat (``"system"|"user"|"assistant"``).
|
|
54
|
+
"""
|
|
55
|
+
self._turn += 1
|
|
56
|
+
self._raw_append({
|
|
57
|
+
"schema_version": "atomadic-forge.transcript/v1",
|
|
58
|
+
"ts_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
59
|
+
"turn": self._turn,
|
|
60
|
+
"kind": kind,
|
|
61
|
+
"role": role,
|
|
62
|
+
"llm": llm,
|
|
63
|
+
"content": content,
|
|
64
|
+
"content_len": len(content or ""),
|
|
65
|
+
"extra": extra or {},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
def _raw_append(self, entry: dict[str, Any]) -> None:
|
|
69
|
+
with self.path.open("a", encoding="utf-8") as f:
|
|
70
|
+
f.write(json.dumps(entry, default=str) + "\n")
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Tier a1 — pure upward-import scanner + auto-fix proposer.
|
|
2
|
+
|
|
3
|
+
Walks a tier-organized package and reports every import statement that
|
|
4
|
+
violates the upward-only law (lower tier importing from a higher tier).
|
|
5
|
+
Handles both Python (``from <pkg>.aN_… import …``) AND JavaScript / TypeScript
|
|
6
|
+
(``import "../aN_…/foo"`` or ``require("../aN_…/foo")``).
|
|
7
|
+
|
|
8
|
+
Lane D2 of the post-audit plan added the optional ``suggest_repairs``
|
|
9
|
+
mode: when enabled, every violation gets a concrete ``proposed_fix``
|
|
10
|
+
string and ``auto_fixable`` counts the suggestions whose minimum-edit
|
|
11
|
+
fix is mechanically obvious (move the violating file UP to the tier of
|
|
12
|
+
the symbol it imports). Heuristic, not a guarantee — the user still
|
|
13
|
+
decides whether the file should move or whether the import should
|
|
14
|
+
instead be inverted. Pure: no file writes, no exec.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
import re
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ..a0_qk_constants.error_codes import fcode_for_tier_violation
|
|
24
|
+
from ..a0_qk_constants.lang_extensions import (
|
|
25
|
+
JAVASCRIPT_EXTS,
|
|
26
|
+
PYTHON_EXTS,
|
|
27
|
+
TYPESCRIPT_EXTS,
|
|
28
|
+
path_parts_contain_ignored_dir,
|
|
29
|
+
)
|
|
30
|
+
from ..a0_qk_constants.tier_names import TIER_NAMES, can_import
|
|
31
|
+
from .js_parser import parse_imports
|
|
32
|
+
|
|
33
|
+
_TIER_PATH_RE = re.compile(r"\.(?P<tier>a\d_[a-z_]+)\.")
|
|
34
|
+
_TIER_SLASH_RE = re.compile(r"(?P<tier>a\d_[a-z_]+)(?:/|$)")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tier_of_module(module: str) -> str | None:
|
|
38
|
+
m = _TIER_PATH_RE.search(f".{module}.")
|
|
39
|
+
if m:
|
|
40
|
+
tier = m.group("tier")
|
|
41
|
+
if tier in TIER_NAMES:
|
|
42
|
+
return tier
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _tier_of_specifier(spec: str) -> str | None:
|
|
47
|
+
"""Pull a tier name out of a JS module specifier.
|
|
48
|
+
|
|
49
|
+
``"../a3_og_features/feature"`` → ``"a3_og_features"``.
|
|
50
|
+
"""
|
|
51
|
+
if not spec:
|
|
52
|
+
return None
|
|
53
|
+
m = _TIER_SLASH_RE.search(spec.replace("\\", "/"))
|
|
54
|
+
if m:
|
|
55
|
+
tier = m.group("tier")
|
|
56
|
+
if tier in TIER_NAMES:
|
|
57
|
+
return tier
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _tier_of_file(path: Path, package_root: Path) -> str | None:
|
|
62
|
+
parts = path.relative_to(package_root).parts
|
|
63
|
+
for p in parts:
|
|
64
|
+
if p in TIER_NAMES:
|
|
65
|
+
return p
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _scan_python_file(py: Path, package_root: Path,
|
|
70
|
+
from_tier: str, violations: list[dict]) -> None:
|
|
71
|
+
try:
|
|
72
|
+
tree = ast.parse(py.read_text(encoding="utf-8", errors="replace"),
|
|
73
|
+
filename=str(py))
|
|
74
|
+
except (SyntaxError, OSError):
|
|
75
|
+
return
|
|
76
|
+
for node in ast.walk(tree):
|
|
77
|
+
if not isinstance(node, ast.ImportFrom):
|
|
78
|
+
continue
|
|
79
|
+
mod = node.module or ""
|
|
80
|
+
to_tier = _tier_of_module(mod)
|
|
81
|
+
if to_tier is None:
|
|
82
|
+
continue
|
|
83
|
+
if can_import(from_tier, to_tier):
|
|
84
|
+
continue
|
|
85
|
+
for alias in node.names:
|
|
86
|
+
violations.append({
|
|
87
|
+
"file": str(py.relative_to(package_root).as_posix()),
|
|
88
|
+
"from_tier": from_tier,
|
|
89
|
+
"to_tier": to_tier,
|
|
90
|
+
"imported": alias.name,
|
|
91
|
+
"language": "python",
|
|
92
|
+
"f_code": fcode_for_tier_violation(from_tier, to_tier),
|
|
93
|
+
"proposed_fix": "",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _scan_js_file(js: Path, package_root: Path,
|
|
98
|
+
from_tier: str, language: str,
|
|
99
|
+
violations: list[dict]) -> None:
|
|
100
|
+
try:
|
|
101
|
+
text = js.read_text(encoding="utf-8", errors="replace")
|
|
102
|
+
except OSError:
|
|
103
|
+
return
|
|
104
|
+
for spec in parse_imports(text):
|
|
105
|
+
to_tier = _tier_of_specifier(spec)
|
|
106
|
+
if to_tier is None:
|
|
107
|
+
continue
|
|
108
|
+
if can_import(from_tier, to_tier):
|
|
109
|
+
continue
|
|
110
|
+
violations.append({
|
|
111
|
+
"file": str(js.relative_to(package_root).as_posix()),
|
|
112
|
+
"from_tier": from_tier,
|
|
113
|
+
"to_tier": to_tier,
|
|
114
|
+
"imported": spec,
|
|
115
|
+
"language": language,
|
|
116
|
+
"f_code": fcode_for_tier_violation(from_tier, to_tier),
|
|
117
|
+
"proposed_fix": "",
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def suggest_fix_for_violation(violation: dict) -> dict:
|
|
122
|
+
"""Return the violation augmented with a concrete repair proposal.
|
|
123
|
+
|
|
124
|
+
Heuristic: when tier T imports tier U upward (T < U), the safest
|
|
125
|
+
*mechanical* fix is to move the importing file from T to U — the
|
|
126
|
+
file is doing higher-tier work (consuming state / orchestrating)
|
|
127
|
+
and was misclassified. The alternative (push the imported symbol
|
|
128
|
+
down to T) requires semantic judgement we don't have here.
|
|
129
|
+
|
|
130
|
+
The returned dict has the original violation fields plus:
|
|
131
|
+
proposed_action — one of "move_file_up" | "review_manually"
|
|
132
|
+
proposed_destination — target tier directory (e.g. "a2_mo_composites")
|
|
133
|
+
fix_command — single shell command sketch the user can adapt
|
|
134
|
+
auto_fixable — bool: is this a clean mechanical move?
|
|
135
|
+
|
|
136
|
+
Pure: no I/O. Heuristic — for review, not auto-apply.
|
|
137
|
+
"""
|
|
138
|
+
file = violation["file"]
|
|
139
|
+
from_tier = violation["from_tier"]
|
|
140
|
+
to_tier = violation["to_tier"]
|
|
141
|
+
language = violation.get("language", "python")
|
|
142
|
+
imported = violation.get("imported", "")
|
|
143
|
+
|
|
144
|
+
auto_fixable = (
|
|
145
|
+
from_tier in TIER_NAMES
|
|
146
|
+
and to_tier in TIER_NAMES
|
|
147
|
+
and from_tier != to_tier
|
|
148
|
+
and language in ("python", "javascript", "typescript")
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if auto_fixable:
|
|
152
|
+
proposed_action = "move_file_up"
|
|
153
|
+
proposed_destination = to_tier
|
|
154
|
+
# File path under the package: the user's package root prefix
|
|
155
|
+
# is unknown to this pure function, so the command is a sketch.
|
|
156
|
+
fix_command = (
|
|
157
|
+
f"mv <package_root>/{file} <package_root>/{to_tier}/"
|
|
158
|
+
f"{Path(file).name} "
|
|
159
|
+
f"# then update imports referencing the old path"
|
|
160
|
+
)
|
|
161
|
+
reasoning = (
|
|
162
|
+
f"{file} is at tier {from_tier} but imports from "
|
|
163
|
+
f"tier {to_tier} (symbol: {imported!r}). The safest "
|
|
164
|
+
f"mechanical fix is to relocate the file up to {to_tier}; "
|
|
165
|
+
f"if the file is genuinely a {from_tier} citizen, the "
|
|
166
|
+
f"imported symbol probably belongs at {from_tier} or "
|
|
167
|
+
f"lower instead."
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
proposed_action = "review_manually"
|
|
171
|
+
proposed_destination = ""
|
|
172
|
+
fix_command = ""
|
|
173
|
+
reasoning = (
|
|
174
|
+
f"Could not auto-classify a destination for {file} "
|
|
175
|
+
f"(from_tier={from_tier!r}, to_tier={to_tier!r}, "
|
|
176
|
+
f"language={language!r}). Review manually."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
**violation,
|
|
181
|
+
"proposed_action": proposed_action,
|
|
182
|
+
"proposed_destination": proposed_destination,
|
|
183
|
+
"fix_command": fix_command,
|
|
184
|
+
"reasoning": reasoning,
|
|
185
|
+
"auto_fixable": auto_fixable,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def scan_violations(
|
|
190
|
+
package_root: Path,
|
|
191
|
+
*,
|
|
192
|
+
suggest_repairs: bool = False,
|
|
193
|
+
) -> dict:
|
|
194
|
+
"""Return a wire report dict keyed by ``schema_version``.
|
|
195
|
+
|
|
196
|
+
Polyglot: scans Python ``.py`` AND JavaScript / TypeScript files. Each
|
|
197
|
+
violation includes a ``language`` field so reports can group by source.
|
|
198
|
+
|
|
199
|
+
``suggest_repairs`` (Lane D2): when True, every violation is enriched
|
|
200
|
+
with a ``proposed_fix`` string, the top-level ``auto_fixable`` count
|
|
201
|
+
is the number of violations with a clean mechanical move, and the
|
|
202
|
+
response includes a ``repair_suggestions`` summary (one entry per
|
|
203
|
+
file, deduplicated). Default False keeps the v1 schema unchanged.
|
|
204
|
+
"""
|
|
205
|
+
package_root = Path(package_root).resolve()
|
|
206
|
+
violations: list[dict] = []
|
|
207
|
+
auto_fixable = 0
|
|
208
|
+
|
|
209
|
+
for f in package_root.rglob("*"):
|
|
210
|
+
if not f.is_file():
|
|
211
|
+
continue
|
|
212
|
+
rel_parts = f.relative_to(package_root).parts
|
|
213
|
+
if path_parts_contain_ignored_dir(rel_parts):
|
|
214
|
+
continue
|
|
215
|
+
from_tier = _tier_of_file(f, package_root)
|
|
216
|
+
if from_tier is None:
|
|
217
|
+
continue
|
|
218
|
+
suffix = f.suffix.lower()
|
|
219
|
+
if suffix in PYTHON_EXTS:
|
|
220
|
+
_scan_python_file(f, package_root, from_tier, violations)
|
|
221
|
+
elif suffix in JAVASCRIPT_EXTS:
|
|
222
|
+
_scan_js_file(f, package_root, from_tier, "javascript", violations)
|
|
223
|
+
elif suffix in TYPESCRIPT_EXTS:
|
|
224
|
+
_scan_js_file(f, package_root, from_tier, "typescript", violations)
|
|
225
|
+
|
|
226
|
+
repair_suggestions: list[dict] = []
|
|
227
|
+
if suggest_repairs:
|
|
228
|
+
for v in violations:
|
|
229
|
+
enriched = suggest_fix_for_violation(v)
|
|
230
|
+
v["proposed_fix"] = enriched["fix_command"] or enriched["reasoning"]
|
|
231
|
+
v["proposed_action"] = enriched["proposed_action"]
|
|
232
|
+
v["proposed_destination"] = enriched["proposed_destination"]
|
|
233
|
+
if enriched["auto_fixable"]:
|
|
234
|
+
auto_fixable += 1
|
|
235
|
+
# One summary entry per (file, proposed_destination) pair.
|
|
236
|
+
seen: set[tuple[str, str]] = set()
|
|
237
|
+
for v in violations:
|
|
238
|
+
key = (v["file"], v.get("proposed_destination", ""))
|
|
239
|
+
if key in seen:
|
|
240
|
+
continue
|
|
241
|
+
seen.add(key)
|
|
242
|
+
repair_suggestions.append({
|
|
243
|
+
"file": v["file"],
|
|
244
|
+
"from_tier": v["from_tier"],
|
|
245
|
+
"proposed_action": v.get("proposed_action", "review_manually"),
|
|
246
|
+
"proposed_destination": v.get("proposed_destination", ""),
|
|
247
|
+
"violation_count": sum(1 for w in violations if w["file"] == v["file"]),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
report: dict = {
|
|
251
|
+
"schema_version": "atomadic-forge.wire/v1",
|
|
252
|
+
"source_dir": str(package_root),
|
|
253
|
+
"violation_count": len(violations),
|
|
254
|
+
"auto_fixable": auto_fixable,
|
|
255
|
+
"violations": violations,
|
|
256
|
+
"verdict": "PASS" if not violations else "FAIL",
|
|
257
|
+
}
|
|
258
|
+
if suggest_repairs:
|
|
259
|
+
report["repair_suggestions"] = repair_suggestions
|
|
260
|
+
return report
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tier a2 — stateful composites. May import from a0 + a1 only."""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tier a2 — append-only lineage chain store.
|
|
2
|
+
|
|
3
|
+
Persists the local Vanguard-style lineage chain at
|
|
4
|
+
``.atomadic-forge/lineage_chain.jsonl``. One JSON line per Receipt;
|
|
5
|
+
each line records:
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
"ts_utc": "2026-04-29T05:30:00Z",
|
|
9
|
+
"hash": "<receipt content hash>",
|
|
10
|
+
"parent_receipt_hash": "<prior link hash, or null for head>",
|
|
11
|
+
"chain_depth": <int>,
|
|
12
|
+
"verdict": "PASS" | "FAIL" | "REFINE" | "QUARANTINE",
|
|
13
|
+
"schema_version": "<receipt schema_version>",
|
|
14
|
+
"lineage_path": "<local:// URI>",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Reading the tip of the chain returns ``(parent_hash, depth)`` for the
|
|
18
|
+
next link to consume. The chain log is append-only — corrupt lines
|
|
19
|
+
are skipped silently so a malformed write never breaks subsequent
|
|
20
|
+
reads.
|
|
21
|
+
|
|
22
|
+
Lane A W4 ships the LOCAL store today; the AAAA-Nexus
|
|
23
|
+
``/v1/forge/lineage`` POST publisher slots in here once the endpoint
|
|
24
|
+
is live, with the same soft-fail contract as Lane A W2's
|
|
25
|
+
``ReceiptSigner``.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import datetime as _dt
|
|
30
|
+
import json
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from ..a0_qk_constants.receipt_schema import ForgeReceiptV1
|
|
34
|
+
from ..a1_at_functions.lineage_chain import (
|
|
35
|
+
canonical_receipt_hash,
|
|
36
|
+
link_to_parent,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_DIRNAME = ".atomadic-forge"
|
|
40
|
+
_FILENAME = "lineage_chain.jsonl"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LineageChainStore:
|
|
44
|
+
"""Append-only local Vanguard ledger for one project."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, project_root: Path) -> None:
|
|
47
|
+
self.project_root = Path(project_root).resolve()
|
|
48
|
+
self.dir = self.project_root / _DIRNAME
|
|
49
|
+
self.path = self.dir / _FILENAME
|
|
50
|
+
|
|
51
|
+
# ---- read paths ----------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def read_tip(self) -> tuple[str | None, int]:
|
|
54
|
+
"""Return ``(parent_hash, depth_of_tip)``.
|
|
55
|
+
|
|
56
|
+
For the chain head (no prior receipts) the tuple is
|
|
57
|
+
``(None, 0)``; the next link should be at depth=1.
|
|
58
|
+
"""
|
|
59
|
+
if not self.path.exists():
|
|
60
|
+
return None, 0
|
|
61
|
+
last: dict | None = None
|
|
62
|
+
for line in self.path.read_text(encoding="utf-8").splitlines():
|
|
63
|
+
line = line.strip()
|
|
64
|
+
if not line:
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
entry = json.loads(line)
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
continue
|
|
70
|
+
if isinstance(entry, dict):
|
|
71
|
+
last = entry
|
|
72
|
+
if last is None:
|
|
73
|
+
return None, 0
|
|
74
|
+
return last.get("hash"), int(last.get("chain_depth", 0))
|
|
75
|
+
|
|
76
|
+
def read_all(self) -> list[dict]:
|
|
77
|
+
if not self.path.exists():
|
|
78
|
+
return []
|
|
79
|
+
out: list[dict] = []
|
|
80
|
+
for line in self.path.read_text(encoding="utf-8").splitlines():
|
|
81
|
+
line = line.strip()
|
|
82
|
+
if not line:
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
entry = json.loads(line)
|
|
86
|
+
except json.JSONDecodeError:
|
|
87
|
+
continue
|
|
88
|
+
if isinstance(entry, dict):
|
|
89
|
+
out.append(entry)
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
# ---- write path ----------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def link_and_append(self, receipt: ForgeReceiptV1) -> ForgeReceiptV1:
|
|
95
|
+
"""Compute the chain link for ``receipt``, append, return the
|
|
96
|
+
Receipt with its lineage block populated.
|
|
97
|
+
|
|
98
|
+
Always writes — append-only. Caller decides whether to also
|
|
99
|
+
re-emit the Receipt JSON (typically yes; the chain entry and
|
|
100
|
+
the on-disk Receipt should both reflect the same lineage).
|
|
101
|
+
"""
|
|
102
|
+
parent_hash, parent_depth = self.read_tip()
|
|
103
|
+
linked = link_to_parent(
|
|
104
|
+
receipt,
|
|
105
|
+
parent_receipt_hash=parent_hash,
|
|
106
|
+
chain_depth=parent_depth + 1,
|
|
107
|
+
)
|
|
108
|
+
own_hash = canonical_receipt_hash(linked)
|
|
109
|
+
entry = {
|
|
110
|
+
"ts_utc": _dt.datetime.now(_dt.timezone.utc).strftime(
|
|
111
|
+
"%Y-%m-%dT%H:%M:%SZ"),
|
|
112
|
+
"hash": own_hash,
|
|
113
|
+
"parent_receipt_hash": parent_hash,
|
|
114
|
+
"chain_depth": parent_depth + 1,
|
|
115
|
+
"verdict": linked.get("verdict"),
|
|
116
|
+
"schema_version": linked.get("schema_version"),
|
|
117
|
+
"lineage_path": (linked.get("lineage") or {}).get("lineage_path"),
|
|
118
|
+
}
|
|
119
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
with self.path.open("a", encoding="utf-8") as f:
|
|
121
|
+
f.write(json.dumps(entry) + "\n")
|
|
122
|
+
return linked
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Tier a2 — stateful manifest read/write.
|
|
2
|
+
|
|
3
|
+
Persists scout / cherry / assimilate / certify reports under a project's
|
|
4
|
+
``.atomadic-forge/`` directory, and records run lineage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import datetime as _dt
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
_DEFAULT_DIRNAME = ".atomadic-forge"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ManifestStore:
|
|
18
|
+
"""Append-only store for Forge artifacts beneath a project root."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project_root: Path, *, dirname: str = _DEFAULT_DIRNAME):
|
|
21
|
+
self.project_root = Path(project_root).resolve()
|
|
22
|
+
self.dir = self.project_root / dirname
|
|
23
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
def save(self, name: str, payload: dict[str, Any]) -> Path:
|
|
26
|
+
target = self.dir / f"{name}.json"
|
|
27
|
+
target.write_text(json.dumps(payload, indent=2, default=str),
|
|
28
|
+
encoding="utf-8")
|
|
29
|
+
self._append_lineage(name, target)
|
|
30
|
+
return target
|
|
31
|
+
|
|
32
|
+
def load(self, name: str) -> dict[str, Any] | None:
|
|
33
|
+
target = self.dir / f"{name}.json"
|
|
34
|
+
if not target.exists():
|
|
35
|
+
return None
|
|
36
|
+
return json.loads(target.read_text(encoding="utf-8"))
|
|
37
|
+
|
|
38
|
+
def _append_lineage(self, name: str, path: Path) -> None:
|
|
39
|
+
log = self.dir / "lineage.jsonl"
|
|
40
|
+
entry = {
|
|
41
|
+
"ts_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
42
|
+
"artifact": name,
|
|
43
|
+
"path": str(path.relative_to(self.project_root).as_posix()),
|
|
44
|
+
}
|
|
45
|
+
with log.open("a", encoding="utf-8") as f:
|
|
46
|
+
f.write(json.dumps(entry) + "\n")
|