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,96 @@
1
+ """Tier a0 — CISQ cost-model constants for ROI calculation.
2
+
3
+ Golden Path Lane F W3.
4
+
5
+ Data sources:
6
+ * CISQ "Cost of Poor Software Quality in the US" (2022 edition)
7
+ — $2.41 trillion total cost of poor software quality
8
+ — $1.52 trillion in accumulated technical debt
9
+ — Average cost per defect: $1,480 (detection) to $14,102 (production)
10
+ * IBM Systems Sciences Institute: defect cost multiplier 100× from
11
+ design to production
12
+ * NIST: "The Economic Impacts of Inadequate Infrastructure for
13
+ Software Testing" (2002) — $59.5B/yr avoidable with better tooling
14
+ * McKinsey Digital: 20-40% of technology spend on managing technical debt
15
+
16
+ These constants are intentionally conservative (use lower-bound estimates)
17
+ so Forge ROI claims remain defensible in enterprise sales contexts.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # CISQ 2022 reference figures
23
+ # ---------------------------------------------------------------------------
24
+
25
+ CISQ_TOTAL_COST_USD: float = 2_410_000_000_000.0
26
+ """$2.41 trillion — total annual cost of poor software quality in the US (2022)."""
27
+
28
+ CISQ_TECH_DEBT_USD: float = 1_520_000_000_000.0
29
+ """$1.52 trillion — accumulated technical debt burden (CISQ 2022)."""
30
+
31
+ CISQ_REFERENCE_YEAR: str = "2022"
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Per-defect cost model (conservative / IBM lower-bound)
35
+ # ---------------------------------------------------------------------------
36
+
37
+ COST_PER_STRUCTURAL_DEFECT_USD: float = 1_480.0
38
+ """Cost to detect and fix one structural defect at the code-review stage ($1,480).
39
+
40
+ Source: CISQ 2022 lower-bound detection cost. Use this for wire violations
41
+ found by Forge (caught at the architecture layer, not production).
42
+ """
43
+
44
+ COST_PER_DEFECT_PRODUCTION_USD: float = 14_102.0
45
+ """Cost of a defect that reaches production ($14,102).
46
+
47
+ Use this as the *avoided* cost when Forge catches a violation pre-production.
48
+ """
49
+
50
+ COST_PER_CERTIFY_AXIS_FAIL_USD: float = 2_200.0
51
+ """Cost estimate per certify-axis failure (documentation, test coverage, etc.).
52
+
53
+ Conservative mid-point between review cost and rework cost.
54
+ """
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Hourly / per-symbol baselines
58
+ # ---------------------------------------------------------------------------
59
+
60
+ DEFAULT_TEAM_HOURLY_RATE_USD: float = 150.0
61
+ """Default fully-loaded engineering hourly rate used when no override provided."""
62
+
63
+ HOURS_PER_STRUCTURAL_DEFECT_FIX: float = 4.0
64
+ """Average hours to fix one structural defect (architecture-layer violation)."""
65
+
66
+ HOURS_PER_CERTIFY_AXIS_FIX: float = 8.0
67
+ """Average hours to address one failing certify axis (e.g. add test suite)."""
68
+
69
+ ANNUAL_CARRY_COST_PER_KLOC_USD: float = 14_500.0
70
+ """Annual maintenance burden per KLOC of technical debt (McKinsey / CISQ composite)."""
71
+
72
+ SYMBOLS_PER_KLOC: float = 50.0
73
+ """Empirical Python: ~50 public symbols per 1,000 lines for well-factored code."""
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Forge scoring thresholds for ROI bands
77
+ # ---------------------------------------------------------------------------
78
+
79
+ SCORE_THRESHOLD_PASS: float = 100.0
80
+ SCORE_THRESHOLD_GOOD: float = 80.0
81
+ SCORE_THRESHOLD_FAIR: float = 60.0
82
+ SCORE_THRESHOLD_POOR: float = 40.0
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Report template strings
86
+ # ---------------------------------------------------------------------------
87
+
88
+ CISQ_CITATION: str = (
89
+ "CISQ, \"The Cost of Poor Software Quality in the U.S.: A 2022 Report,\" "
90
+ "Consortium for Information & Software Quality, 2022."
91
+ )
92
+
93
+ NIST_CITATION: str = (
94
+ "NIST Planning Report 02-3, \"The Economic Impacts of Inadequate "
95
+ "Infrastructure for Software Testing,\" May 2002."
96
+ )
@@ -0,0 +1,61 @@
1
+ """Tier a0 — types for semantic conflict resolution.
2
+
3
+ When Forge absorbs from multiple repos and two symbols collide on name,
4
+ we extract an *intent signature* from each and compute a similarity score
5
+ to decide whether they're the same concept (merge), distinct flavours of
6
+ the same concept (rename with semantic suffix), or genuinely different
7
+ concepts (keep both, namespace by source module).
8
+
9
+ This is the structured wire format for that pipeline.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Literal, TypedDict
15
+
16
+ MergeDecision = Literal["merge", "rename_semantic", "rename_module"]
17
+
18
+
19
+ class IntentSignature(TypedDict):
20
+ """Cheap, deterministic proxies for a class's intent.
21
+
22
+ None of these are "real" semantics — they're surface signals correlated
23
+ with intent. The decisive design choice: combine many weak signals,
24
+ weighted, rather than rely on a single strong one.
25
+ """
26
+
27
+ name: str
28
+ fields: list[str] # ``self.<x> = …`` left-hand sides
29
+ method_names: list[str] # public methods only
30
+ field_types: dict[str, str] # field → annotation text (when present)
31
+ doc_tokens: list[str] # significant tokens from docstring
32
+ verb_signature: list[str] # method-name verb prefixes (get/set/save/post/…)
33
+ sibling_imports: list[str] # other names imported in the same file
34
+ base_classes: list[str] # superclasses
35
+ file_path: str
36
+ source_root: str # which repo it came from
37
+
38
+
39
+ class SimilarityScore(TypedDict):
40
+ """Weighted-Jaccard breakdown for an intent-signature comparison."""
41
+
42
+ overall: float # 0.0 … 1.0
43
+ fields: float
44
+ method_names: float
45
+ field_types: float
46
+ doc_tokens: float
47
+ verb_signature: float
48
+ sibling_imports: float
49
+ discriminating_tokens: list[str] # doc tokens unique to one side
50
+
51
+
52
+ class ConflictResolution(TypedDict):
53
+ """The outcome of resolving one (A, B) conflict."""
54
+
55
+ name: str
56
+ decision: MergeDecision
57
+ score: float
58
+ rationale: list[str]
59
+ merged_signature: IntentSignature | None # filled when decision == "merge"
60
+ proposed_names: list[str] # 1 for merge, 2 for rename
61
+ review_marker: str # "# REVIEW: …" line for the output
@@ -0,0 +1,81 @@
1
+ """Tier a0 — .forge sidecar v1.0 wire-format schema.
2
+
3
+ Golden Path Lane D W8 deliverable. The sidecar is a YAML file that
4
+ sits beside any source file (e.g. ``src/pkg/auth.py.forge``) and
5
+ declares the per-symbol contract:
6
+
7
+ effect: what side effects each function performs
8
+ (NetIO | KeyedCache | Logging | Pure | IO)
9
+ compose_with: named symbols this one lawfully composes with
10
+ (Bao-Rompf categorical effect functor — Lane D W20)
11
+ proves: Lean4 proof obligation labels this symbol satisfies
12
+ (Lane D W20 dispatches against aethel-nexus-proofs)
13
+
14
+ a0 invariant: pure data shape, imports limited to __future__ + typing.
15
+ The reader (a1.sidecar_parser) parses YAML into these dicts; the
16
+ validator (Lane D W11) cross-checks against the source file's AST.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from typing import Literal, TypedDict
21
+
22
+ SCHEMA_VERSION_SIDECAR_V1 = "atomadic-forge.sidecar/v1"
23
+
24
+
25
+ # v1 effect taxonomy — Bao-Rompf 2025 categorical effect lattice.
26
+ EffectKind = Literal[
27
+ "Pure", # no side effects
28
+ "IO", # arbitrary I/O (filesystem, stdin/stdout)
29
+ "NetIO", # network calls
30
+ "KeyedCache", # writes to a content-addressed cache
31
+ "Logging", # observability emission only
32
+ "Random", # non-deterministic input (rng / time / uuid)
33
+ "Mutation", # mutates passed-in arguments
34
+ ]
35
+
36
+ VALID_EFFECTS: tuple[str, ...] = (
37
+ "Pure", "IO", "NetIO", "KeyedCache", "Logging", "Random", "Mutation",
38
+ )
39
+
40
+
41
+ class SidecarSymbol(TypedDict, total=False):
42
+ """One symbol's contract within a sidecar file.
43
+
44
+ Required:
45
+ name — must match a top-level symbol in the source
46
+ effect — one of VALID_EFFECTS
47
+
48
+ Optional:
49
+ compose_with — ['module.symbol', ...] this symbol composes with
50
+ proves — ['lemma_name', ...] obligations this discharges
51
+ tier — caller may pin the tier (otherwise inferred)
52
+ notes — free-form
53
+ """
54
+ name: str
55
+ effect: EffectKind
56
+ compose_with: list[str]
57
+ proves: list[str]
58
+ tier: str
59
+ notes: list[str]
60
+
61
+
62
+ class SidecarFile(TypedDict, total=False):
63
+ """The full sidecar document.
64
+
65
+ Required:
66
+ schema_version — atomadic-forge.sidecar/v1
67
+ target — path of the source file this sidecar covers
68
+ symbols — list of SidecarSymbol; one per public symbol
69
+ """
70
+ schema_version: str
71
+ target: str
72
+ symbols: list[SidecarSymbol]
73
+ # Forward-compat: future minors may add fields. Consumers MUST
74
+ # tolerate unknown keys.
75
+ extra: dict[str, object]
76
+
77
+
78
+ REQUIRED_SIDECAR_FIELDS: tuple[str, ...] = (
79
+ "schema_version", "target", "symbols",
80
+ )
81
+ REQUIRED_SYMBOL_FIELDS: tuple[str, ...] = ("name", "effect")
@@ -0,0 +1,62 @@
1
+ """Tier a0 — types for the Synergy Scan.
2
+
3
+ Synergy Scan operates at the *feature / CLI verb* level (one level above
4
+ Emergent Scan, which operates on symbols). It finds pairs of features
5
+ where one produces an artifact the other could consume — but no code path
6
+ wires them together yet. Optionally it generates an adapter that does.
7
+
8
+ Wire format:
9
+
10
+ * :class:`FeatureSurfaceCard` — one feature/CLI verb's I/O surface.
11
+ * :class:`SynergyCandidateCard` — one (producer, consumer) pair with score.
12
+ * :class:`SynergyScanReport` — top-level scan output.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Literal, TypedDict
18
+
19
+ SynergyKind = Literal[
20
+ "json_artifact", # producer emits --json-out, consumer takes file arg
21
+ "in_memory_pipe", # producer's return type matches consumer's first arg
22
+ "shared_schema", # both reference the same JSON schema name
23
+ "shared_vocabulary", # high lexical overlap in help/docstrings
24
+ "phase_omission", # natural phase chain skips a step
25
+ ]
26
+
27
+
28
+ class FeatureSurfaceCard(TypedDict):
29
+ """One feature/CLI verb's input + output surface."""
30
+
31
+ name: str # CLI verb (e.g. "scout", "assimilate")
32
+ module: str # importable module path
33
+ help_text: str
34
+ inputs: list[str] # canonical arg names (e.g. ["repo", "policy"])
35
+ input_files: list[str] # filename patterns it accepts
36
+ outputs: list[str] # canonical product names (e.g. ["scout-report"])
37
+ output_files: list[str] # filename patterns it emits (e.g. ["*.json"])
38
+ schemas: list[str] # schema names mentioned in help/docstring
39
+ vocabulary: list[str] # unique tokens harvested from help+args
40
+ phase_hint: str # heuristic phase tag (recon/ingest/.../emit)
41
+
42
+
43
+ class SynergyCandidateCard(TypedDict):
44
+ """One detected synergy between two features."""
45
+
46
+ candidate_id: str
47
+ producer: str # feature name
48
+ consumer: str # feature name
49
+ kind: SynergyKind
50
+ why: list[str] # human-readable signals supporting this synergy
51
+ score: float # 0..100
52
+ score_breakdown: dict[str, float]
53
+ proposed_adapter_name: str # kebab-case CLI verb for the auto-wired pair
54
+ proposed_summary: str
55
+
56
+
57
+ class SynergyScanReport(TypedDict):
58
+ schema_version: str # "atomadic-forge.synergy.scan/v1"
59
+ generated_at_utc: str
60
+ feature_count: int
61
+ candidate_count: int
62
+ candidates: list[SynergyCandidateCard]
@@ -0,0 +1,47 @@
1
+ """Tier a0 — canonical tier names and effect lattice."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ TIER_NAMES: Final[tuple[str, ...]] = (
8
+ "a0_qk_constants",
9
+ "a1_at_functions",
10
+ "a2_mo_composites",
11
+ "a3_og_features",
12
+ "a4_sy_orchestration",
13
+ )
14
+
15
+ TIER_PREFIX: Final[dict[str, str]] = {
16
+ "a0_qk_constants": "a0",
17
+ "a1_at_functions": "a1",
18
+ "a2_mo_composites": "a2",
19
+ "a3_og_features": "a3",
20
+ "a4_sy_orchestration": "a4",
21
+ }
22
+
23
+ # Effect lattice — what each tier is *allowed* to do.
24
+ EFFECT_LATTICE: Final[dict[str, frozenset[str]]] = {
25
+ "a0_qk_constants": frozenset(),
26
+ "a1_at_functions": frozenset({"pure"}),
27
+ "a2_mo_composites": frozenset({"pure", "state"}),
28
+ "a3_og_features": frozenset({"pure", "state", "orchestrate"}),
29
+ "a4_sy_orchestration": frozenset({"pure", "state", "orchestrate", "io"}),
30
+ }
31
+
32
+
33
+ def tier_index(tier: str) -> int:
34
+ """Return 0..4 for the tier name; raise ValueError if unknown."""
35
+ try:
36
+ return TIER_NAMES.index(tier)
37
+ except ValueError as exc:
38
+ raise ValueError(f"unknown tier: {tier!r}") from exc
39
+
40
+
41
+ def can_import(from_tier: str, to_tier: str) -> bool:
42
+ """Return True if ``from_tier`` is allowed to import from ``to_tier``.
43
+
44
+ Tiers compose **upward only**: a higher tier may import from any lower
45
+ tier, never the reverse.
46
+ """
47
+ return tier_index(from_tier) >= tier_index(to_tier)
@@ -0,0 +1 @@
1
+ """Tier a1 — pure stateless helpers. No side effects, no I/O, no state."""
@@ -0,0 +1,193 @@
1
+ """Tier a1 — pure agent context pack.
2
+
3
+ Codex's 'Copilot's Copilot' primitive #1:
4
+
5
+ > Add one MCP tool that every agent calls first: forge_context_pack.
6
+ > Returns: repo purpose, architecture law, tier map, current
7
+ > blockers, best next action, test commands, release gate, risky
8
+ > files, recent lineage. Agents waste a lot of time rediscovering
9
+ > this.
10
+
11
+ Pure: takes already-computed reports + a project root, returns a
12
+ single bundle. The orchestration that runs the scans lives in a3.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from collections import Counter
18
+ from pathlib import Path
19
+ from typing import TypedDict
20
+
21
+ from .agent_summary import summarize_blockers
22
+ from .lineage_reader import read_lineage
23
+
24
+ SCHEMA_VERSION_CONTEXT_PACK_V1 = "atomadic-forge.context_pack/v1"
25
+
26
+
27
+ # The architecture law — pinned text, the same on every pack.
28
+ _TIER_LAW = (
29
+ "Tier composition is upward-only. Files in a0_qk_constants/ "
30
+ "import nothing. a1_at_functions/ may import a0. a2_mo_composites/ "
31
+ "may import a0 + a1. a3_og_features/ may import a0..a2. "
32
+ "a4_sy_orchestration/ may import a0..a3. Never import upward; "
33
+ "never import sideways. Wire violations carry F-codes "
34
+ "F0040–F0049."
35
+ )
36
+
37
+
38
+ class ContextPack(TypedDict, total=False):
39
+ schema_version: str
40
+ project_root: str
41
+ repo_purpose: str
42
+ architecture_law: str
43
+ tier_map: dict[str, int]
44
+ primary_language: str
45
+ blockers_summary: dict
46
+ best_next_action: dict | None
47
+ test_commands: list[str]
48
+ release_gate: list[str]
49
+ risky_files: list[dict]
50
+ recent_lineage: list[dict]
51
+ pinned_resources: list[str]
52
+
53
+
54
+ def _read_repo_purpose(project_root: Path) -> str:
55
+ """Best-effort: pull the first paragraph from the README, or the
56
+ pyproject description. Falls back to the directory name."""
57
+ for name in ("README.md", "README.rst", "README"):
58
+ readme = project_root / name
59
+ if not readme.exists():
60
+ continue
61
+ try:
62
+ text = readme.read_text(encoding="utf-8", errors="replace")
63
+ except OSError:
64
+ continue
65
+ # First non-heading paragraph.
66
+ for para in text.split("\n\n"):
67
+ stripped = para.strip()
68
+ if not stripped or stripped.startswith("#"):
69
+ continue
70
+ # Strip leading badges and HTML <p> wrappers.
71
+ cleaned = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", stripped)
72
+ cleaned = re.sub(r"\[[^\]]*\]\([^)]+\)", "", cleaned)
73
+ cleaned = re.sub(r"<[^>]+>", "", cleaned).strip()
74
+ if len(cleaned) >= 20:
75
+ return cleaned[:400]
76
+ pyproject = project_root / "pyproject.toml"
77
+ if pyproject.exists():
78
+ try:
79
+ text = pyproject.read_text(encoding="utf-8", errors="replace")
80
+ except OSError:
81
+ text = ""
82
+ m = re.search(r'description\s*=\s*"([^"]+)"', text)
83
+ if m:
84
+ return m.group(1)[:400]
85
+ return f"Repo at {project_root.name} (no README description found)"
86
+
87
+
88
+ def _detect_test_commands(project_root: Path) -> list[str]:
89
+ cmds: list[str] = []
90
+ if (project_root / "pyproject.toml").exists():
91
+ cmds.append("python -m pytest")
92
+ if (project_root / "tox.ini").exists():
93
+ cmds.append("tox")
94
+ if (project_root / "package.json").exists():
95
+ cmds.append("npm test")
96
+ if (project_root / "Cargo.toml").exists():
97
+ cmds.append("cargo test")
98
+ if (project_root / "Makefile").exists():
99
+ # Look for a 'test' target.
100
+ try:
101
+ mk = (project_root / "Makefile").read_text(
102
+ encoding="utf-8", errors="replace")
103
+ if re.search(r"^test:", mk, re.MULTILINE):
104
+ cmds.append("make test")
105
+ except OSError:
106
+ pass
107
+ if not cmds:
108
+ cmds.append("# no test runner detected — add tests/ + pytest")
109
+ return cmds
110
+
111
+
112
+ def _release_gate(project_root: Path) -> list[str]:
113
+ """Heuristic release gate: lint + tests + wire + certify ≥ 75."""
114
+ gate: list[str] = []
115
+ if (project_root / "pyproject.toml").exists():
116
+ gate.append("python -m ruff check .")
117
+ gate.extend([
118
+ "python -m pytest",
119
+ "forge wire src --fail-on-violations",
120
+ "forge certify . --fail-under 75",
121
+ ])
122
+ return gate
123
+
124
+
125
+ def _risky_files(lineage: list[dict], top_n: int = 10) -> list[dict]:
126
+ """Files written most often in the recent lineage are 'risky' —
127
+ they're the ones the agent has been touching repeatedly. Useful
128
+ proxy for hot spots without a real edit-frequency analysis."""
129
+ counter: Counter[str] = Counter()
130
+ for entry in lineage:
131
+ path = entry.get("path") or entry.get("artifact")
132
+ if path:
133
+ counter[path] += 1
134
+ return [
135
+ {"path": p, "edit_count": n}
136
+ for p, n in counter.most_common(top_n)
137
+ ]
138
+
139
+
140
+ def emit_context_pack(
141
+ *,
142
+ project_root: Path,
143
+ scout_report: dict | None = None,
144
+ wire_report: dict | None = None,
145
+ certify_report: dict | None = None,
146
+ plan: dict | None = None,
147
+ ) -> ContextPack:
148
+ """Build the first-call context bundle.
149
+
150
+ Reports may be None — the bundle degrades gracefully and reports
151
+ 'unavailable' for missing sections.
152
+ """
153
+ project_root = Path(project_root).resolve()
154
+ tier_map = (scout_report or {}).get("tier_distribution") or {}
155
+ primary_language = (scout_report or {}).get("primary_language", "?")
156
+
157
+ blockers = summarize_blockers(
158
+ wire_report=wire_report, certify_report=certify_report,
159
+ package_root=project_root.name,
160
+ )
161
+ best_next: dict | None = None
162
+ if plan and plan.get("top_actions"):
163
+ best_next = plan["top_actions"][0]
164
+ elif blockers["blockers"]:
165
+ b = blockers["blockers"][0]
166
+ best_next = {
167
+ "id": "blocker." + (b.get("f_code") or "?"),
168
+ "title": b.get("title", ""),
169
+ "next_command": b.get("next_command", ""),
170
+ "kind": "blocker",
171
+ }
172
+
173
+ lineage = read_lineage(project_root, last=20)
174
+
175
+ return ContextPack(
176
+ schema_version=SCHEMA_VERSION_CONTEXT_PACK_V1,
177
+ project_root=str(project_root),
178
+ repo_purpose=_read_repo_purpose(project_root),
179
+ architecture_law=_TIER_LAW,
180
+ tier_map=dict(tier_map),
181
+ primary_language=primary_language,
182
+ blockers_summary=blockers,
183
+ best_next_action=best_next,
184
+ test_commands=_detect_test_commands(project_root),
185
+ release_gate=_release_gate(project_root),
186
+ risky_files=_risky_files(lineage),
187
+ recent_lineage=lineage[-10:] if lineage else [],
188
+ pinned_resources=[
189
+ "forge://docs/receipt",
190
+ "forge://docs/formalization",
191
+ "forge://summary/blockers",
192
+ ],
193
+ )
@@ -0,0 +1,139 @@
1
+ """Tier a1 — agent memory queries (Codex #5).
2
+
3
+ Codex's prescription:
4
+
5
+ > Make .atomadic-forge/ a real agent memory substrate. Future
6
+ > agents can ask: why_did_this_change({file}),
7
+ > what_failed_last_time({area}). That's gold.
8
+
9
+ Pure: queries lineage.jsonl + plan state files. No I/O beyond those
10
+ two read paths.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import TypedDict
16
+
17
+ from .lineage_reader import read_lineage
18
+
19
+ SCHEMA_VERSION_WHY_V1 = "atomadic-forge.why/v1"
20
+ SCHEMA_VERSION_WHAT_FAILED_V1 = "atomadic-forge.what_failed/v1"
21
+
22
+
23
+ class WhyDidThisChange(TypedDict, total=False):
24
+ schema_version: str
25
+ file: str
26
+ related_lineage: list[dict]
27
+ related_plan_events: list[dict]
28
+ summary: str
29
+
30
+
31
+ class WhatFailedLastTime(TypedDict, total=False):
32
+ schema_version: str
33
+ area: str
34
+ failures: list[dict]
35
+ summary: str
36
+
37
+
38
+ def why_did_this_change(
39
+ *,
40
+ file: str,
41
+ project_root: Path,
42
+ ) -> WhyDidThisChange:
43
+ """Return every lineage + plan event referencing ``file``.
44
+
45
+ Heuristic: an entry 'references' the file when:
46
+ * the entry's `path` field equals or contains the file path
47
+ * the entry is a plan event whose card.write_scope contains it
48
+
49
+ Pure: read-only lineage + plan-state walks.
50
+ """
51
+ project_root = Path(project_root).resolve()
52
+ lineage = read_lineage(project_root)
53
+ related_lineage = [
54
+ entry for entry in lineage
55
+ if entry.get("path", "") == file
56
+ or file in entry.get("path", "")
57
+ ]
58
+
59
+ plan_events: list[dict] = []
60
+ plans_dir = project_root / ".atomadic-forge" / "plans"
61
+ if plans_dir.is_dir():
62
+ import json
63
+ for state_file in plans_dir.glob("*.state.json"):
64
+ try:
65
+ state = json.loads(state_file.read_text(encoding="utf-8"))
66
+ except (OSError, json.JSONDecodeError):
67
+ continue
68
+ for ev in state.get("events", []):
69
+ detail = ev.get("detail") or {}
70
+ # Direct path match in detail (e.g. README write).
71
+ if isinstance(detail.get("written"), str) \
72
+ and detail["written"] == file:
73
+ plan_events.append({**ev,
74
+ "plan_id": state.get("plan_id")})
75
+ continue
76
+ if file in str(detail):
77
+ plan_events.append({**ev,
78
+ "plan_id": state.get("plan_id")})
79
+
80
+ if related_lineage or plan_events:
81
+ last_ts = ""
82
+ for entry in related_lineage + plan_events:
83
+ ts = entry.get("ts_utc", "")
84
+ if ts > last_ts:
85
+ last_ts = ts
86
+ summary = (
87
+ f"{file} appears in {len(related_lineage)} lineage entries "
88
+ f"and {len(plan_events)} plan events; latest activity "
89
+ f"{last_ts or '(unknown)'}."
90
+ )
91
+ else:
92
+ summary = f"no recorded forge activity references {file}."
93
+
94
+ return WhyDidThisChange(
95
+ schema_version=SCHEMA_VERSION_WHY_V1,
96
+ file=file,
97
+ related_lineage=related_lineage,
98
+ related_plan_events=plan_events,
99
+ summary=summary,
100
+ )
101
+
102
+
103
+ def what_failed_last_time(
104
+ *,
105
+ area: str,
106
+ project_root: Path,
107
+ ) -> WhatFailedLastTime:
108
+ """Return plan events with status 'failed' or 'rolled_back' that
109
+ relate to ``area`` (matched as a substring of card_id / detail).
110
+ """
111
+ project_root = Path(project_root).resolve()
112
+ failures: list[dict] = []
113
+ plans_dir = project_root / ".atomadic-forge" / "plans"
114
+ if plans_dir.is_dir():
115
+ import json
116
+ for state_file in plans_dir.glob("*.state.json"):
117
+ try:
118
+ state = json.loads(state_file.read_text(encoding="utf-8"))
119
+ except (OSError, json.JSONDecodeError):
120
+ continue
121
+ for ev in state.get("events", []):
122
+ if ev.get("status") not in {"failed", "rolled_back"}:
123
+ continue
124
+ blob = (ev.get("card_id", "") + " " +
125
+ str(ev.get("detail", "")))
126
+ if not area or area.lower() in blob.lower():
127
+ failures.append({**ev,
128
+ "plan_id": state.get("plan_id")})
129
+ summary = (
130
+ f"{len(failures)} prior failure(s) referencing area={area!r}."
131
+ if failures
132
+ else f"no prior failures recorded for area={area!r}."
133
+ )
134
+ return WhatFailedLastTime(
135
+ schema_version=SCHEMA_VERSION_WHAT_FAILED_V1,
136
+ area=area,
137
+ failures=failures,
138
+ summary=summary,
139
+ )