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,228 @@
|
|
|
1
|
+
"""Tier a1 — pure compliance-mapping checker for ForgeReceiptV1.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane F W2.
|
|
4
|
+
|
|
5
|
+
Evaluates a ForgeReceiptV1 dict against EU AI Act Annex IV, SR 11-7,
|
|
6
|
+
FDA PCCP, and CMMC-AI and returns a ``compliance_mappings`` dict
|
|
7
|
+
suitable for embedding in the Receipt or rendering in the CS-1
|
|
8
|
+
Conformity Statement.
|
|
9
|
+
|
|
10
|
+
Status values per framework:
|
|
11
|
+
PASS — all required checks pass
|
|
12
|
+
PARTIAL — at least one required check fails, no critical failure
|
|
13
|
+
FAIL — a critical check fails (wire FAIL or certify below floor)
|
|
14
|
+
NOT_ASSESSED — essential Receipt fields missing; cannot evaluate
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Public constants
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
COMPLIANCE_FRAMEWORK_KEYS: tuple[str, ...] = (
|
|
23
|
+
"eu_ai_act",
|
|
24
|
+
"sr_11_7",
|
|
25
|
+
"fda_pccp",
|
|
26
|
+
"cmmc_ai",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
ComplianceStatus = str # "PASS" | "PARTIAL" | "FAIL" | "NOT_ASSESSED"
|
|
30
|
+
|
|
31
|
+
_VALID_STATUSES: frozenset[str] = frozenset(
|
|
32
|
+
{"PASS", "PARTIAL", "FAIL", "NOT_ASSESSED"}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Internal accessors
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wire_pass(r: dict) -> bool:
|
|
41
|
+
return (r.get("wire") or {}).get("verdict") == "PASS"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _score(r: dict) -> float:
|
|
45
|
+
return float((r.get("certify") or {}).get("score", 0.0))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _axes(r: dict) -> dict:
|
|
49
|
+
return (r.get("certify") or {}).get("axes") or {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _lean4(r: dict) -> dict:
|
|
53
|
+
return r.get("lean4_attestation") or {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _lineage(r: dict) -> dict:
|
|
57
|
+
return r.get("lineage") or {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _project(r: dict) -> dict:
|
|
61
|
+
return r.get("project") or {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _has_lean4_clean(r: dict) -> bool:
|
|
65
|
+
"""True iff at least one corpus present and all have sorry_count == 0."""
|
|
66
|
+
corpora = _lean4(r).get("corpora") or []
|
|
67
|
+
return bool(corpora) and all(
|
|
68
|
+
int(c.get("sorry_count", 1)) == 0 for c in corpora
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _has_any_signature(r: dict) -> bool:
|
|
73
|
+
sigs = r.get("signatures") or {}
|
|
74
|
+
return bool(
|
|
75
|
+
(sigs.get("sigstore") or {}).get("rekor_uuid")
|
|
76
|
+
or (sigs.get("aaaa_nexus") or {}).get("signature")
|
|
77
|
+
or (sigs.get("local_sign") or {}).get("signature")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _chain_depth(r: dict) -> int:
|
|
82
|
+
return int(_lineage(r).get("chain_depth") or 0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Per-framework checkers
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _check_eu_ai_act(r: dict) -> ComplianceStatus:
|
|
91
|
+
"""EU AI Act Annex IV, Regulation (EU) 2024/1689.
|
|
92
|
+
|
|
93
|
+
Critical (FAIL / NOT_ASSESSED):
|
|
94
|
+
§1 project.name + project.language must be present
|
|
95
|
+
§2 wire scan must PASS (structural documentation of architecture)
|
|
96
|
+
§2 certify.score >= 60
|
|
97
|
+
|
|
98
|
+
Required (PARTIAL if any false):
|
|
99
|
+
§3 Lean4 corpora present with all sorry_count == 0
|
|
100
|
+
§4 lineage.chain_depth >= 1 (monitoring record exists)
|
|
101
|
+
§5 certify.axes.documentation_complete
|
|
102
|
+
"""
|
|
103
|
+
proj = _project(r)
|
|
104
|
+
if not proj.get("name") or not proj.get("language"):
|
|
105
|
+
return "NOT_ASSESSED"
|
|
106
|
+
if not _wire_pass(r) or _score(r) < 60.0:
|
|
107
|
+
return "FAIL"
|
|
108
|
+
required = [
|
|
109
|
+
_has_lean4_clean(r),
|
|
110
|
+
_chain_depth(r) >= 1,
|
|
111
|
+
_axes(r).get("documentation_complete", False),
|
|
112
|
+
]
|
|
113
|
+
return "PASS" if all(required) else "PARTIAL"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _check_sr_11_7(r: dict) -> ComplianceStatus:
|
|
117
|
+
"""Federal Reserve SR Letter 11-7 (2011) + FAQ (2021).
|
|
118
|
+
|
|
119
|
+
Critical:
|
|
120
|
+
§III.A tier_layout_present (governance framework exists)
|
|
121
|
+
§IV certify.score >= 70 (development-process standards)
|
|
122
|
+
§IV.A wire scan PASS (conceptual soundness)
|
|
123
|
+
|
|
124
|
+
Required (PARTIAL if any false):
|
|
125
|
+
§IV.A Lean4 attestation (formal soundness evidence)
|
|
126
|
+
§IV.B tests_present (outcomes analysis capability)
|
|
127
|
+
§V.A lineage.chain_depth >= 2 (ongoing-monitoring track record)
|
|
128
|
+
"""
|
|
129
|
+
if not _axes(r).get("tier_layout_present", False):
|
|
130
|
+
return "NOT_ASSESSED"
|
|
131
|
+
if _score(r) < 70.0 or not _wire_pass(r):
|
|
132
|
+
return "FAIL"
|
|
133
|
+
required = [
|
|
134
|
+
_has_lean4_clean(r),
|
|
135
|
+
_axes(r).get("tests_present", False),
|
|
136
|
+
_chain_depth(r) >= 2,
|
|
137
|
+
]
|
|
138
|
+
return "PASS" if all(required) else "PARTIAL"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_fda_pccp(r: dict) -> ComplianceStatus:
|
|
142
|
+
"""FDA Predetermined Change Control Plan (Draft 2023).
|
|
143
|
+
|
|
144
|
+
Critical:
|
|
145
|
+
§II.A schema_version must be present (version-controlled artifact)
|
|
146
|
+
§II.A lineage.chain_depth >= 1 (algorithm change protocol in place)
|
|
147
|
+
|
|
148
|
+
Required (PARTIAL if any false):
|
|
149
|
+
§II.B certify.score >= 80 (impact-assessment standard met)
|
|
150
|
+
§II.C tests_present (re-validation methodology exists)
|
|
151
|
+
"""
|
|
152
|
+
if not r.get("schema_version"):
|
|
153
|
+
return "NOT_ASSESSED"
|
|
154
|
+
if _chain_depth(r) < 1:
|
|
155
|
+
return "FAIL"
|
|
156
|
+
required = [
|
|
157
|
+
_score(r) >= 80.0,
|
|
158
|
+
_axes(r).get("tests_present", False),
|
|
159
|
+
]
|
|
160
|
+
return "PASS" if all(required) else "PARTIAL"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _check_cmmc_ai(r: dict) -> ComplianceStatus:
|
|
164
|
+
"""CMMC 2.0 + NIST AI RMF 1.0 (2023).
|
|
165
|
+
|
|
166
|
+
Critical:
|
|
167
|
+
GOVERN 1.1 wire scan PASS (no architecture policy violations)
|
|
168
|
+
GOVERN 1.2 tier_layout_present (systematic governance structure)
|
|
169
|
+
(floor) certify.score >= 60
|
|
170
|
+
|
|
171
|
+
Required (PARTIAL if any false):
|
|
172
|
+
MAP 1.1 Lean4 corpora with sorry_count == 0
|
|
173
|
+
MEASURE 2.5 certify.score >= 80
|
|
174
|
+
MANAGE 1.3 at least one signature present
|
|
175
|
+
"""
|
|
176
|
+
if not _wire_pass(r) or not _axes(r).get("tier_layout_present", False):
|
|
177
|
+
return "FAIL"
|
|
178
|
+
if _score(r) < 60.0:
|
|
179
|
+
return "FAIL"
|
|
180
|
+
required = [
|
|
181
|
+
_has_lean4_clean(r),
|
|
182
|
+
_score(r) >= 80.0,
|
|
183
|
+
_has_any_signature(r),
|
|
184
|
+
]
|
|
185
|
+
return "PASS" if all(required) else "PARTIAL"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Public API
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
_CHECKERS: dict[str, object] = {
|
|
193
|
+
"eu_ai_act": _check_eu_ai_act,
|
|
194
|
+
"sr_11_7": _check_sr_11_7,
|
|
195
|
+
"fda_pccp": _check_fda_pccp,
|
|
196
|
+
"cmmc_ai": _check_cmmc_ai,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def check_compliance(receipt: dict) -> dict[str, str]:
|
|
201
|
+
"""Evaluate a ForgeReceiptV1 dict against all 4 compliance frameworks.
|
|
202
|
+
|
|
203
|
+
Returns a ``compliance_mappings`` dict ready to embed in the Receipt
|
|
204
|
+
or pass to ``cs1_renderer.render_cs1``::
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
"eu_ai_act": "PASS" | "PARTIAL" | "FAIL" | "NOT_ASSESSED",
|
|
208
|
+
"sr_11_7": "PASS" | "PARTIAL" | "FAIL" | "NOT_ASSESSED",
|
|
209
|
+
"fda_pccp": "PASS" | "PARTIAL" | "FAIL" | "NOT_ASSESSED",
|
|
210
|
+
"cmmc_ai": "PASS" | "PARTIAL" | "FAIL" | "NOT_ASSESSED",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Never raises. Returns all ``"NOT_ASSESSED"`` for an empty or None receipt.
|
|
214
|
+
"""
|
|
215
|
+
if not receipt:
|
|
216
|
+
return {k: "NOT_ASSESSED" for k in COMPLIANCE_FRAMEWORK_KEYS}
|
|
217
|
+
return {k: _CHECKERS[k](receipt) for k in COMPLIANCE_FRAMEWORK_KEYS} # type: ignore[operator]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def check_compliance_framework(receipt: dict, framework: str) -> str:
|
|
221
|
+
"""Evaluate a single framework by key.
|
|
222
|
+
|
|
223
|
+
Returns ``"NOT_ASSESSED"`` for unknown framework keys.
|
|
224
|
+
"""
|
|
225
|
+
checker = _CHECKERS.get(framework)
|
|
226
|
+
if checker is None:
|
|
227
|
+
return "NOT_ASSESSED"
|
|
228
|
+
return checker(receipt) # type: ignore[operator]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Tier a1 — pure helpers for config file I/O, merging, and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from atomadic_forge.a0_qk_constants.config_defaults import (
|
|
9
|
+
CONFIG_FILE_NAME,
|
|
10
|
+
DEFAULT_CONFIG,
|
|
11
|
+
GLOBAL_CONFIG_DIR,
|
|
12
|
+
LOCAL_CONFIG_DIR,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_config_file(path: Path) -> dict:
|
|
17
|
+
"""Read a JSON config file; return empty dict if missing or malformed."""
|
|
18
|
+
try:
|
|
19
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
20
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_config(project_dir: Path) -> dict:
|
|
25
|
+
"""Load config with priority: local > global > defaults."""
|
|
26
|
+
defaults = dict(DEFAULT_CONFIG)
|
|
27
|
+
global_path = Path(GLOBAL_CONFIG_DIR).expanduser() / CONFIG_FILE_NAME
|
|
28
|
+
local_path = project_dir / LOCAL_CONFIG_DIR / CONFIG_FILE_NAME
|
|
29
|
+
|
|
30
|
+
global_cfg = read_config_file(global_path)
|
|
31
|
+
local_cfg = read_config_file(local_path)
|
|
32
|
+
return merge_configs(local_cfg, global_cfg, defaults)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def save_config(config: dict, path: Path) -> None:
|
|
36
|
+
"""Write config dict as pretty-printed JSON; create parent dirs as needed."""
|
|
37
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def merge_configs(local: dict, global_: dict, defaults: dict) -> dict:
|
|
42
|
+
"""Merge configs with priority local > global > defaults; None values are skipped."""
|
|
43
|
+
merged = dict(defaults)
|
|
44
|
+
merged.update({k: v for k, v in global_.items() if v is not None})
|
|
45
|
+
merged.update({k: v for k, v in local.items() if v is not None})
|
|
46
|
+
return merged
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_config(config: dict) -> list[str]:
|
|
50
|
+
"""Return a list of validation issues; empty list means the config is valid."""
|
|
51
|
+
issues: list[str] = []
|
|
52
|
+
|
|
53
|
+
valid_providers = {"auto", "ollama", "gemini", "anthropic", "openai", "stub"}
|
|
54
|
+
provider = config.get("provider", "auto")
|
|
55
|
+
if provider not in valid_providers:
|
|
56
|
+
issues.append(
|
|
57
|
+
f"Unknown provider {provider!r}; choose from {sorted(valid_providers)}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
score = config.get("default_target_score", 75.0)
|
|
61
|
+
if not isinstance(score, int | float) or not (0 <= float(score) <= 100):
|
|
62
|
+
issues.append("default_target_score must be a number between 0 and 100")
|
|
63
|
+
|
|
64
|
+
ollama_url = config.get("ollama_url", "")
|
|
65
|
+
if ollama_url and not str(ollama_url).startswith(("http://", "https://")):
|
|
66
|
+
issues.append("ollama_url must start with http:// or https://")
|
|
67
|
+
|
|
68
|
+
return issues
|