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,186 @@
1
+ """Tier a1 — golden-path recipes (Codex #12).
2
+
3
+ Codex's prescription:
4
+
5
+ > For common agent tasks: recipe_release_hardening,
6
+ > recipe_add_cli_command, recipe_fix_wire_violation,
7
+ > recipe_add_feature, recipe_publish_mcp. Each returns a
8
+ > checklist, file scope, and validation gate. Agents love rails.
9
+
10
+ Pure: a recipe is a static dict. ``get_recipe(name)`` returns it,
11
+ ``list_recipes()`` returns the catalogue.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import TypedDict
16
+
17
+ SCHEMA_VERSION_RECIPE_V1 = "atomadic-forge.recipe/v1"
18
+
19
+
20
+ class GoldenRecipe(TypedDict, total=False):
21
+ schema_version: str
22
+ name: str
23
+ description: str
24
+ checklist: list[str]
25
+ file_scope_hints: list[str]
26
+ validation_gate: list[str]
27
+ notes: list[str]
28
+
29
+
30
+ _RECIPES: dict[str, GoldenRecipe] = {
31
+ "release_hardening": GoldenRecipe(
32
+ schema_version=SCHEMA_VERSION_RECIPE_V1,
33
+ name="release_hardening",
34
+ description=(
35
+ "Add CI + CHANGELOG entry + version bump + signed Receipt "
36
+ "before tagging a release."
37
+ ),
38
+ checklist=[
39
+ "Add or update .github/workflows/ci.yml",
40
+ "Append a CHANGELOG.md entry under the new version",
41
+ "Bump version in pyproject.toml (or package.json)",
42
+ "Run forge certify . --emit-receipt --sign",
43
+ "Run forge wire src --fail-on-violations",
44
+ "Tag the commit with the new version",
45
+ ],
46
+ file_scope_hints=[
47
+ ".github/workflows/ci.yml",
48
+ "CHANGELOG.md",
49
+ "pyproject.toml",
50
+ ],
51
+ validation_gate=[
52
+ "python -m ruff check .",
53
+ "python -m pytest",
54
+ "forge wire src --fail-on-violations",
55
+ "forge certify . --fail-under 90",
56
+ ],
57
+ notes=[
58
+ "Lane G1 ships --fail-under and --fail-on-violations natively; "
59
+ "use them instead of python -c JSON-parse fallbacks.",
60
+ ],
61
+ ),
62
+ "add_cli_command": GoldenRecipe(
63
+ schema_version=SCHEMA_VERSION_RECIPE_V1,
64
+ name="add_cli_command",
65
+ description=(
66
+ "Add a new top-level Forge verb (or sibling-tool verb) "
67
+ "with tier-clean wiring."
68
+ ),
69
+ checklist=[
70
+ "Decide whether the logic is pure (a1), stateful (a2), or "
71
+ "feature-orchestrating (a3); CLI wrapper goes in a4.",
72
+ "Implement the pure helper in the right tier.",
73
+ "Add the @app.command(...) handler in cli.py with --json "
74
+ "and (where applicable) --fail-on flags.",
75
+ "Add a smoke test in tests/test_cli_smoke.py + a unit test "
76
+ "for the pure helper.",
77
+ "Run forge wire src --fail-on-violations to confirm tier "
78
+ "discipline; bump CHANGELOG.",
79
+ ],
80
+ file_scope_hints=[
81
+ "src/atomadic_forge/aN_*/<your_module>.py",
82
+ "src/atomadic_forge/a4_sy_orchestration/cli.py",
83
+ "tests/test_cli_smoke.py",
84
+ "CHANGELOG.md",
85
+ ],
86
+ validation_gate=[
87
+ "python -m pytest tests/test_cli_smoke.py",
88
+ "forge wire src/atomadic_forge --fail-on-violations",
89
+ ],
90
+ ),
91
+ "fix_wire_violation": GoldenRecipe(
92
+ schema_version=SCHEMA_VERSION_RECIPE_V1,
93
+ name="fix_wire_violation",
94
+ description=(
95
+ "Resolve a tier upward-import violation surfaced by "
96
+ "forge wire (F0040–F0049)."
97
+ ),
98
+ checklist=[
99
+ "Run forge wire <pkg> --suggest-repairs to read the "
100
+ "F-code + suggested move.",
101
+ "Decide: move the file UP to the higher tier (default), "
102
+ "or invert the import direction by extracting the symbol "
103
+ "DOWN to a lower tier.",
104
+ "Run forge enforce <pkg> --apply for F0041..F0046 "
105
+ "(rollback-safe orchestrator).",
106
+ "Re-run forge wire to confirm zero violations.",
107
+ "Re-run forge certify to confirm score recovered.",
108
+ ],
109
+ file_scope_hints=[
110
+ "src/<pkg>/aN_*/<violating_file>.py",
111
+ ],
112
+ validation_gate=[
113
+ "forge wire <pkg> --fail-on-violations",
114
+ "python -m pytest",
115
+ "forge certify . --fail-under 75",
116
+ ],
117
+ ),
118
+ "add_feature": GoldenRecipe(
119
+ schema_version=SCHEMA_VERSION_RECIPE_V1,
120
+ name="add_feature",
121
+ description=(
122
+ "Implement a feature that combines existing a1 helpers "
123
+ "and a2 composites under an a3 module."
124
+ ),
125
+ checklist=[
126
+ "Read existing a3 features for tone + shape.",
127
+ "Add the feature module under a3_og_features/.",
128
+ "If the feature has a CLI surface, add a thin wrapper in "
129
+ "a4_sy_orchestration/cli.py.",
130
+ "Tests cover the pure parts at a1 level + the feature at a3.",
131
+ "Run forge wire to confirm no upward imports.",
132
+ ],
133
+ file_scope_hints=[
134
+ "src/atomadic_forge/a3_og_features/<feature>.py",
135
+ "src/atomadic_forge/a4_sy_orchestration/cli.py",
136
+ "tests/test_<feature>.py",
137
+ ],
138
+ validation_gate=[
139
+ "python -m pytest",
140
+ "forge wire src/atomadic_forge --fail-on-violations",
141
+ "forge certify . --fail-under 75",
142
+ ],
143
+ ),
144
+ "publish_mcp": GoldenRecipe(
145
+ schema_version=SCHEMA_VERSION_RECIPE_V1,
146
+ name="publish_mcp",
147
+ description=(
148
+ "Register Forge as an MCP server in a coding-agent client "
149
+ "(Cursor / Claude Code / Aider / Devin)."
150
+ ),
151
+ checklist=[
152
+ "Install Forge: pip install -e . (or pip install "
153
+ "atomadic-forge once on PyPI).",
154
+ "Add a 'forge' entry to the client's mcpServers map.",
155
+ "command = 'forge'; args = ['mcp', 'serve', '--project', "
156
+ "'/path/to/your/repo'].",
157
+ "Reconnect the client; expect tools/list to expose 11 "
158
+ "tools incl. context_pack, preflight_change, score_patch.",
159
+ "Hit forge://summary/blockers as the agent's first call "
160
+ "for orientation.",
161
+ ],
162
+ file_scope_hints=[
163
+ "<your-mcp-config>.json",
164
+ ],
165
+ validation_gate=[
166
+ "forge mcp serve --project . < /dev/null",
167
+ ],
168
+ notes=[
169
+ "On Windows, use absolute paths with forward slashes in "
170
+ "the MCP config to avoid escape-character issues.",
171
+ ],
172
+ ),
173
+ }
174
+
175
+
176
+ def list_recipes() -> list[str]:
177
+ return sorted(_RECIPES.keys())
178
+
179
+
180
+ def get_recipe(name: str) -> GoldenRecipe | None:
181
+ return _RECIPES.get(name)
182
+
183
+
184
+ def all_recipes() -> dict[str, GoldenRecipe]:
185
+ """Return the full recipe catalogue (read-only-by-convention)."""
186
+ return dict(_RECIPES)
@@ -0,0 +1,124 @@
1
+ """Tier a1 — explain_repo: humane operational orientation (Codex #6).
2
+
3
+ Codex distinguished this from context_pack:
4
+
5
+ > 'This is different from docs. It is operational orientation.
6
+ > Output: This is a Python package for… Core flow is… Do not
7
+ > break… Most important tests are… Release state is…'
8
+
9
+ Pure: takes already-computed reports (or None) and emits a short
10
+ operational paragraph + a few bullets the agent should treat as
11
+ hard constraints.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import TypedDict
17
+
18
+ SCHEMA_VERSION_EXPLAIN_V1 = "atomadic-forge.explain_repo/v1"
19
+
20
+
21
+ class RepoExplanation(TypedDict, total=False):
22
+ schema_version: str
23
+ project_root: str
24
+ one_liner: str
25
+ core_flow: str
26
+ do_not_break: list[str]
27
+ important_tests: list[str]
28
+ release_state: str
29
+ depth: str
30
+
31
+
32
+ def _detect_core_flow(scout_report: dict | None) -> str:
33
+ if not scout_report:
34
+ return "(scout report unavailable; run forge recon)"
35
+ tiers = scout_report.get("tier_distribution") or {}
36
+ if not tiers:
37
+ return "(repo has no tier-organized symbols)"
38
+ primary_tier = max(tiers.items(), key=lambda kv: kv[1])[0]
39
+ return (
40
+ f"primary symbol density is in {primary_tier} "
41
+ f"({tiers[primary_tier]} symbols). "
42
+ f"a4_sy_orchestration calls into a3 features which compose "
43
+ f"a2 stateful classes from a1 pure functions over a0 "
44
+ f"constants — the upward-only law applies."
45
+ )
46
+
47
+
48
+ def _detect_release_state(certify_report: dict | None,
49
+ wire_report: dict | None) -> str:
50
+ if certify_report is None and wire_report is None:
51
+ return "(no certify / wire scans available)"
52
+ score = (certify_report or {}).get("score")
53
+ wire_verdict = (wire_report or {}).get("verdict", "?")
54
+ # Wire FAIL is blocking regardless of whether certify ran.
55
+ if wire_verdict == "FAIL":
56
+ return f"BLOCKED — wire verdict={wire_verdict}; fix violations first."
57
+ if score is None:
58
+ return f"wire={wire_verdict}; no certify score available."
59
+ if score >= 100 and wire_verdict == "PASS":
60
+ return "PASS — every gate green; ready to ship."
61
+ if score >= 75 and wire_verdict == "PASS":
62
+ return f"GREEN-ISH — score {score:.0f}/100; ship-ready by team-grade gate."
63
+ return f"REFINE — score {score:.0f}/100 below shipping bar."
64
+
65
+
66
+ def _important_tests(project_root: Path) -> list[str]:
67
+ """Heuristic: tests at the top of tests/ are usually the smoke +
68
+ contract tests; tests with 'smoke' or 'contract' in the name
69
+ explicitly. Cap at 5."""
70
+ out: list[str] = []
71
+ for sub in ("tests", "test"):
72
+ d = project_root / sub
73
+ if not d.is_dir():
74
+ continue
75
+ # Smoke / contract first.
76
+ for f in sorted(d.glob("test_*.py")):
77
+ n = f.name.lower()
78
+ if "smoke" in n or "contract" in n or "import" in n:
79
+ out.append(str(f.relative_to(project_root).as_posix()))
80
+ # Then any other top-level tests/.
81
+ for f in sorted(d.glob("test_*.py")):
82
+ rel = str(f.relative_to(project_root).as_posix())
83
+ if rel not in out:
84
+ out.append(rel)
85
+ if len(out) >= 5:
86
+ break
87
+ break
88
+ return out[:5]
89
+
90
+
91
+ def explain_repo(
92
+ *,
93
+ project_root: Path,
94
+ repo_purpose: str,
95
+ scout_report: dict | None = None,
96
+ wire_report: dict | None = None,
97
+ certify_report: dict | None = None,
98
+ depth: str = "agent",
99
+ ) -> RepoExplanation:
100
+ """Build the humane orientation paragraph + bullets.
101
+
102
+ ``depth`` is reserved for future variants ('agent' | 'reviewer' |
103
+ 'newcomer'); v1 emits the agent-tuned version regardless.
104
+ """
105
+ project_root = Path(project_root).resolve()
106
+ one_liner = (repo_purpose or f"Repo at {project_root.name}")[:200]
107
+ do_not_break = [
108
+ "the upward-only tier law (a0 → a1 → a2 → a3 → a4); "
109
+ "any wire violation blocks merge",
110
+ "public re-exports in __init__.py — touching them is a "
111
+ "public_api_risk in score_patch",
112
+ "release-control files (pyproject.toml, CHANGELOG.md, "
113
+ "VERSION) — version bumps require a CHANGELOG entry",
114
+ ]
115
+ return RepoExplanation(
116
+ schema_version=SCHEMA_VERSION_EXPLAIN_V1,
117
+ project_root=str(project_root),
118
+ one_liner=one_liner,
119
+ core_flow=_detect_core_flow(scout_report),
120
+ do_not_break=do_not_break,
121
+ important_tests=_important_tests(project_root),
122
+ release_state=_detect_release_state(certify_report, wire_report),
123
+ depth=depth,
124
+ )
@@ -0,0 +1,265 @@
1
+ """Tier a1 — pure ROI calculator for ForgeReceiptV1.
2
+
3
+ Golden Path Lane F W3.
4
+
5
+ Given a ForgeReceiptV1 dict, applies the CISQ cost model to estimate:
6
+ * structural defects identified by Forge (wire violations + certify axes)
7
+ * avoided remediation cost (finding at architecture layer vs production)
8
+ * annual technical-debt carry cost eliminated
9
+ * CIO-facing ROI summary in USD
10
+
11
+ All numbers use conservative CISQ 2022 lower-bound figures so enterprise
12
+ claims remain defensible. See ``a0_qk_constants/roi_constants.py`` for
13
+ source citations.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from atomadic_forge.a0_qk_constants.roi_constants import (
18
+ ANNUAL_CARRY_COST_PER_KLOC_USD,
19
+ CISQ_CITATION,
20
+ CISQ_REFERENCE_YEAR,
21
+ COST_PER_CERTIFY_AXIS_FAIL_USD,
22
+ COST_PER_DEFECT_PRODUCTION_USD,
23
+ COST_PER_STRUCTURAL_DEFECT_USD,
24
+ DEFAULT_TEAM_HOURLY_RATE_USD,
25
+ HOURS_PER_CERTIFY_AXIS_FIX,
26
+ HOURS_PER_STRUCTURAL_DEFECT_FIX,
27
+ NIST_CITATION,
28
+ SCORE_THRESHOLD_GOOD,
29
+ SCORE_THRESHOLD_PASS,
30
+ SYMBOLS_PER_KLOC,
31
+ )
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Internal helpers
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ def _wire(r: dict) -> dict:
40
+ return r.get("wire") or {}
41
+
42
+
43
+ def _certify(r: dict) -> dict:
44
+ return r.get("certify") or {}
45
+
46
+
47
+ def _scout(r: dict) -> dict:
48
+ return r.get("scout") or {}
49
+
50
+
51
+ def _axes_fail_count(r: dict) -> int:
52
+ axes = _certify(r).get("axes") or {}
53
+ return sum(1 for v in axes.values() if not v)
54
+
55
+
56
+ def _symbol_count(r: dict) -> int:
57
+ return int(_scout(r).get("symbol_count") or 0)
58
+
59
+
60
+ def _kloc_estimate(r: dict) -> float:
61
+ symbols = _symbol_count(r)
62
+ if symbols <= 0:
63
+ return 0.0
64
+ return symbols / SYMBOLS_PER_KLOC
65
+
66
+
67
+ def _score(r: dict) -> float:
68
+ return float(_certify(r).get("score", 0.0))
69
+
70
+
71
+ def _wire_violations(r: dict) -> int:
72
+ return int(_wire(r).get("violation_count") or 0)
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Public API
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ def calculate_roi(
81
+ receipt: dict,
82
+ team_hourly_rate: float = DEFAULT_TEAM_HOURLY_RATE_USD,
83
+ ) -> dict:
84
+ """Calculate TD-principal reduction estimate from a ForgeReceiptV1.
85
+
86
+ Returns a dict with all figures in USD, ready to embed in a Receipt
87
+ or render as a Markdown report::
88
+
89
+ {
90
+ "wire_violations": int,
91
+ "certify_score": float,
92
+ "certify_axes_failing": int,
93
+ "estimated_structural_defects": float,
94
+ "avoided_remediation_cost_usd": float, # catch-at-arch vs catch-at-prod
95
+ "annual_carry_reduction_usd": float,
96
+ "fix_effort_hours": float,
97
+ "fix_effort_cost_usd": float,
98
+ "roi_multiplier": float, # avoided_cost / fix_cost
99
+ "grade": str, # PASS / GOOD / FAIR / POOR
100
+ "cisq_reference_year": str,
101
+ "team_hourly_rate_usd": float,
102
+ }
103
+
104
+ Never raises. Returns zeros for an empty receipt.
105
+ """
106
+ if not receipt:
107
+ return _zero_roi()
108
+
109
+ violations = _wire_violations(receipt)
110
+ axes_failing = _axes_fail_count(receipt)
111
+ kloc = _kloc_estimate(receipt)
112
+ score = _score(receipt)
113
+
114
+ # Total structural defects: each wire violation is 1 defect;
115
+ # each failing certify axis contributes 0.5 (weaker signal).
116
+ estimated_defects = float(violations) + axes_failing * 0.5
117
+
118
+ # Remediation cost at architecture layer (cheap — caught now).
119
+ review_cost = violations * COST_PER_STRUCTURAL_DEFECT_USD
120
+ review_cost += axes_failing * COST_PER_CERTIFY_AXIS_FAIL_USD
121
+
122
+ # Avoided cost = what it would cost if these reached production.
123
+ avoided_cost = violations * COST_PER_DEFECT_PRODUCTION_USD
124
+ avoided_cost += axes_failing * COST_PER_CERTIFY_AXIS_FAIL_USD * 3.0
125
+
126
+ # Annual carry cost eliminated by resolving all identified issues.
127
+ # We credit the full KLOC carry for projects at PASS; partial for others.
128
+ if score >= SCORE_THRESHOLD_PASS:
129
+ carry_credit = 0.0 # already clean
130
+ else:
131
+ debt_fraction = max(0.0, (SCORE_THRESHOLD_PASS - score) / SCORE_THRESHOLD_PASS)
132
+ carry_credit = kloc * ANNUAL_CARRY_COST_PER_KLOC_USD * debt_fraction
133
+
134
+ # Fix effort — how long it would take to address everything Forge found.
135
+ fix_hours = violations * HOURS_PER_STRUCTURAL_DEFECT_FIX
136
+ fix_hours += axes_failing * HOURS_PER_CERTIFY_AXIS_FIX
137
+ fix_cost = fix_hours * team_hourly_rate
138
+
139
+ # ROI multiplier: how many dollars of avoided future cost per dollar of fix cost.
140
+ total_avoided = avoided_cost + carry_credit
141
+ roi_mult = (total_avoided / fix_cost) if fix_cost > 0.0 else 0.0
142
+
143
+ grade = _grade(score)
144
+
145
+ return {
146
+ "wire_violations": violations,
147
+ "certify_score": round(score, 2),
148
+ "certify_axes_failing": axes_failing,
149
+ "estimated_structural_defects": round(estimated_defects, 1),
150
+ "avoided_remediation_cost_usd": round(avoided_cost, 2),
151
+ "annual_carry_reduction_usd": round(carry_credit, 2),
152
+ "fix_effort_hours": round(fix_hours, 1),
153
+ "fix_effort_cost_usd": round(fix_cost, 2),
154
+ "roi_multiplier": round(roi_mult, 1),
155
+ "grade": grade,
156
+ "cisq_reference_year": CISQ_REFERENCE_YEAR,
157
+ "team_hourly_rate_usd": team_hourly_rate,
158
+ }
159
+
160
+
161
+ def _zero_roi() -> dict:
162
+ return {
163
+ "wire_violations": 0,
164
+ "certify_score": 0.0,
165
+ "certify_axes_failing": 0,
166
+ "estimated_structural_defects": 0.0,
167
+ "avoided_remediation_cost_usd": 0.0,
168
+ "annual_carry_reduction_usd": 0.0,
169
+ "fix_effort_hours": 0.0,
170
+ "fix_effort_cost_usd": 0.0,
171
+ "roi_multiplier": 0.0,
172
+ "grade": "UNKNOWN",
173
+ "cisq_reference_year": CISQ_REFERENCE_YEAR,
174
+ "team_hourly_rate_usd": DEFAULT_TEAM_HOURLY_RATE_USD,
175
+ }
176
+
177
+
178
+ def _grade(score: float) -> str:
179
+ if score >= SCORE_THRESHOLD_PASS:
180
+ return "PASS"
181
+ if score >= SCORE_THRESHOLD_GOOD:
182
+ return "GOOD"
183
+ if score >= 60.0:
184
+ return "FAIR"
185
+ return "POOR"
186
+
187
+
188
+ def render_roi_markdown(roi: dict, project_name: str = "Project") -> str:
189
+ """Render an ROI dict as a CIO-ready Markdown report.
190
+
191
+ The output is a concise Markdown document suitable for:
192
+ * direct email attachment (rendered by most email clients)
193
+ * pasting into Confluence / Notion
194
+ * converting to PDF via ``pandoc`` or ``wkhtmltopdf``
195
+ """
196
+ violations = roi["wire_violations"]
197
+ axes_fail = roi["certify_axes_failing"]
198
+ defects = roi["estimated_structural_defects"]
199
+ avoided = roi["avoided_remediation_cost_usd"]
200
+ carry = roi["annual_carry_reduction_usd"]
201
+ fix_h = roi["fix_effort_hours"]
202
+ fix_cost = roi["fix_effort_cost_usd"]
203
+ mult = roi["roi_multiplier"]
204
+ grade = roi["grade"]
205
+ score = roi["certify_score"]
206
+ rate = roi["team_hourly_rate_usd"]
207
+ year = roi["cisq_reference_year"]
208
+
209
+ grade_icon = {"PASS": "✓", "GOOD": "~", "FAIR": "!", "POOR": "✗"}.get(grade, "?")
210
+
211
+ lines = [
212
+ f"# Forge ROI Report — {project_name}",
213
+ "",
214
+ f"**Conformity grade: {grade_icon} {grade}** | Certify score: {score:.0f}/100",
215
+ "",
216
+ "## Findings",
217
+ "",
218
+ f"| Metric | Count |",
219
+ f"|---|---|",
220
+ f"| Wire violations (structural defects) | {violations} |",
221
+ f"| Certify axes failing | {axes_fail} |",
222
+ f"| Estimated total structural defects | {defects:.1f} |",
223
+ "",
224
+ "## Financial impact (CISQ {year} cost model)".format(year=year),
225
+ "",
226
+ f"| Item | USD |",
227
+ f"|---|---|",
228
+ f"| Avoided production-defect cost | ${avoided:,.0f} |",
229
+ f"| Annual technical-debt carry eliminated | ${carry:,.0f} |",
230
+ f"| Fix effort ({fix_h:.0f} h × ${rate:,.0f}/h) | ${fix_cost:,.0f} |",
231
+ f"| **ROI multiplier** | **{mult:.1f}×** |",
232
+ "",
233
+ ]
234
+ if mult >= 1.0:
235
+ lines += [
236
+ f"Forge identified **{defects:.1f} structural defects**. "
237
+ f"Fixing them now costs an estimated **${fix_cost:,.0f}** "
238
+ f"and avoids **${avoided + carry:,.0f}** in future remediation "
239
+ f"and annual carry — a **{mult:.1f}× return on investment**.",
240
+ "",
241
+ ]
242
+ else:
243
+ lines += [
244
+ "No actionable defects detected. This project is already at "
245
+ "or near structural quality floor.",
246
+ "",
247
+ ]
248
+ lines += [
249
+ "## Methodology",
250
+ "",
251
+ "Cost-per-defect figures are CISQ 2022 lower-bound (conservative):",
252
+ "",
253
+ f"- Structural defect caught at architecture layer: ${COST_PER_STRUCTURAL_DEFECT_USD:,.0f}",
254
+ f"- Same defect reaching production: ${COST_PER_DEFECT_PRODUCTION_USD:,.0f}",
255
+ f"- Annual carry per KLOC of technical debt: ${ANNUAL_CARRY_COST_PER_KLOC_USD:,.0f}",
256
+ "",
257
+ "**References**",
258
+ "",
259
+ f"1. {CISQ_CITATION}",
260
+ f"2. {NIST_CITATION}",
261
+ "",
262
+ "_Generated by Atomadic Forge. Figures are estimates based on "
263
+ "industry-average defect costs; actual costs may vary._",
264
+ ]
265
+ return "\n".join(lines) + "\n"