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