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