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,314 @@
|
|
|
1
|
+
"""Tier a1 — pure manifest diff for atomadic-forge.* JSON reports.
|
|
2
|
+
|
|
3
|
+
Compares two Forge manifests (scout / cherry / assimilate / wire / certify /
|
|
4
|
+
synergy / emergent / any other forge schema) and emits a structured diff.
|
|
5
|
+
|
|
6
|
+
This module is pure: no file I/O, no subprocess, no a2+ imports. Callers
|
|
7
|
+
load JSON themselves and pass the parsed dicts in.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Bound recursion + collection size so a 50MB manifest can never blow up.
|
|
15
|
+
_MAX_DEPTH = 6
|
|
16
|
+
_MAX_LIST_ITEMS = 100
|
|
17
|
+
_TRUNCATED = "...truncated"
|
|
18
|
+
|
|
19
|
+
_DIFF_SCHEMA = "atomadic-forge.diff/v1"
|
|
20
|
+
_FORGE_PREFIX = "atomadic-forge."
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --------------------------------------------------------------------------- #
|
|
24
|
+
# helpers
|
|
25
|
+
# --------------------------------------------------------------------------- #
|
|
26
|
+
|
|
27
|
+
def _require_forge_manifest(m: Any, side: str) -> str:
|
|
28
|
+
if not isinstance(m, dict):
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"{side} manifest is not a JSON object — expected a dict with a "
|
|
31
|
+
f"`schema_version` starting with `{_FORGE_PREFIX}`."
|
|
32
|
+
)
|
|
33
|
+
sv = m.get("schema_version")
|
|
34
|
+
if not isinstance(sv, str) or not sv.startswith(_FORGE_PREFIX):
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"{side} manifest has no recognisable forge `schema_version` — "
|
|
37
|
+
f"expected a string starting with `{_FORGE_PREFIX}`, got {sv!r}."
|
|
38
|
+
)
|
|
39
|
+
return sv
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _schema_family(schema_version: str) -> str:
|
|
43
|
+
"""Strip the trailing `/vN`. `atomadic-forge.certify/v1` -> `atomadic-forge.certify`."""
|
|
44
|
+
return schema_version.rsplit("/", 1)[0]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_delta(n: int | float) -> str:
|
|
48
|
+
if n == 0:
|
|
49
|
+
return "0"
|
|
50
|
+
return f"+{n}" if n > 0 else f"{n}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _bounded_list(items: list, depth: int) -> list:
|
|
54
|
+
"""Truncate long lists; recursively bound nested values."""
|
|
55
|
+
if len(items) > _MAX_LIST_ITEMS:
|
|
56
|
+
head = [_bound_value(v, depth + 1) for v in items[:_MAX_LIST_ITEMS]]
|
|
57
|
+
head.append(_TRUNCATED)
|
|
58
|
+
return head
|
|
59
|
+
return [_bound_value(v, depth + 1) for v in items]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _bound_value(v: Any, depth: int) -> Any:
|
|
63
|
+
"""Recursively cap depth + list size for safe inclusion in the diff."""
|
|
64
|
+
if depth >= _MAX_DEPTH:
|
|
65
|
+
if isinstance(v, dict | list):
|
|
66
|
+
return _TRUNCATED
|
|
67
|
+
return v
|
|
68
|
+
if isinstance(v, dict):
|
|
69
|
+
return {k: _bound_value(val, depth + 1) for k, val in v.items()}
|
|
70
|
+
if isinstance(v, list):
|
|
71
|
+
return _bounded_list(v, depth)
|
|
72
|
+
return v
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
# generic recursive diff
|
|
77
|
+
# --------------------------------------------------------------------------- #
|
|
78
|
+
|
|
79
|
+
def _walk(left: Any, right: Any, path: str, depth: int,
|
|
80
|
+
added: list, removed: list, changed: list) -> None:
|
|
81
|
+
"""Recursive diff that respects _MAX_DEPTH and bounds list inclusions."""
|
|
82
|
+
if depth >= _MAX_DEPTH:
|
|
83
|
+
if left != right:
|
|
84
|
+
changed.append({"path": path or "<root>",
|
|
85
|
+
"left": _TRUNCATED, "right": _TRUNCATED})
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if isinstance(left, dict) and isinstance(right, dict):
|
|
89
|
+
l_keys = set(left.keys())
|
|
90
|
+
r_keys = set(right.keys())
|
|
91
|
+
for k in sorted(r_keys - l_keys):
|
|
92
|
+
sub = f"{path}.{k}" if path else k
|
|
93
|
+
added.append({"path": sub, "value": _bound_value(right[k], depth + 1)})
|
|
94
|
+
for k in sorted(l_keys - r_keys):
|
|
95
|
+
sub = f"{path}.{k}" if path else k
|
|
96
|
+
removed.append({"path": sub, "value": _bound_value(left[k], depth + 1)})
|
|
97
|
+
for k in sorted(l_keys & r_keys):
|
|
98
|
+
sub = f"{path}.{k}" if path else k
|
|
99
|
+
_walk(left[k], right[k], sub, depth + 1, added, removed, changed)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if isinstance(left, list) and isinstance(right, list):
|
|
103
|
+
# Treat lists as opaque values when they differ — element-wise diffs
|
|
104
|
+
# explode in size for big manifests. Per-schema summaries below
|
|
105
|
+
# handle the *interesting* lists (violations, candidates).
|
|
106
|
+
if left != right:
|
|
107
|
+
changed.append({
|
|
108
|
+
"path": path or "<root>",
|
|
109
|
+
"left": _bound_value(left, depth + 1),
|
|
110
|
+
"right": _bound_value(right, depth + 1),
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if left != right:
|
|
115
|
+
changed.append({
|
|
116
|
+
"path": path or "<root>",
|
|
117
|
+
"left": _bound_value(left, depth + 1),
|
|
118
|
+
"right": _bound_value(right, depth + 1),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------- #
|
|
123
|
+
# per-schema summaries
|
|
124
|
+
# --------------------------------------------------------------------------- #
|
|
125
|
+
|
|
126
|
+
def _summary_certify(left: dict, right: dict) -> dict:
|
|
127
|
+
summary: dict[str, Any] = {}
|
|
128
|
+
l_score = left.get("score", 0) or 0
|
|
129
|
+
r_score = right.get("score", 0) or 0
|
|
130
|
+
summary["score_delta"] = _format_delta(r_score - l_score)
|
|
131
|
+
|
|
132
|
+
axis_keys = (
|
|
133
|
+
"documentation_complete", "tests_present", "tier_layout_present",
|
|
134
|
+
"no_upward_imports", "no_stub_bodies", "package_importable",
|
|
135
|
+
"ci_workflow_present", "changelog_present",
|
|
136
|
+
)
|
|
137
|
+
flips: list[dict] = []
|
|
138
|
+
for k in axis_keys:
|
|
139
|
+
if k not in left or k not in right:
|
|
140
|
+
continue
|
|
141
|
+
lv, rv = bool(left[k]), bool(right[k])
|
|
142
|
+
if lv != rv:
|
|
143
|
+
flips.append({"axis": k, "left": lv, "right": rv,
|
|
144
|
+
"direction": "regressed" if lv and not rv else "improved"})
|
|
145
|
+
if flips:
|
|
146
|
+
summary["axis_flips"] = flips
|
|
147
|
+
|
|
148
|
+
l_ratio = left.get("test_pass_ratio")
|
|
149
|
+
r_ratio = right.get("test_pass_ratio")
|
|
150
|
+
if isinstance(l_ratio, int | float) and isinstance(r_ratio, int | float):
|
|
151
|
+
summary["test_pass_ratio_delta"] = _format_delta(round(r_ratio - l_ratio, 4))
|
|
152
|
+
return summary
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _summary_wire(left: dict, right: dict) -> dict:
|
|
156
|
+
l_v = left.get("violations", []) or []
|
|
157
|
+
r_v = right.get("violations", []) or []
|
|
158
|
+
l_count = left.get("violation_count", len(l_v))
|
|
159
|
+
r_count = right.get("violation_count", len(r_v))
|
|
160
|
+
|
|
161
|
+
def _key(v: dict) -> tuple:
|
|
162
|
+
return (v.get("file", ""), v.get("from_tier", ""),
|
|
163
|
+
v.get("to_tier", ""), v.get("imported", ""))
|
|
164
|
+
|
|
165
|
+
l_keys = {_key(v) for v in l_v if isinstance(v, dict)}
|
|
166
|
+
r_keys = {_key(v) for v in r_v if isinstance(v, dict)}
|
|
167
|
+
new_keys = r_keys - l_keys
|
|
168
|
+
fixed_keys = l_keys - r_keys
|
|
169
|
+
|
|
170
|
+
new_violations = [v for v in r_v if isinstance(v, dict) and _key(v) in new_keys]
|
|
171
|
+
fixed_violations = [v for v in l_v if isinstance(v, dict) and _key(v) in fixed_keys]
|
|
172
|
+
|
|
173
|
+
summary: dict[str, Any] = {
|
|
174
|
+
"violations_delta": _format_delta(r_count - l_count),
|
|
175
|
+
"left_verdict": left.get("verdict"),
|
|
176
|
+
"right_verdict": right.get("verdict"),
|
|
177
|
+
}
|
|
178
|
+
if new_violations:
|
|
179
|
+
summary["new_violations"] = _bounded_list(new_violations, 1)
|
|
180
|
+
if fixed_violations:
|
|
181
|
+
summary["fixed_violations"] = _bounded_list(fixed_violations, 1)
|
|
182
|
+
return summary
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _summary_scout_like(left: dict, right: dict) -> dict:
|
|
186
|
+
"""Covers scout/v1 and assimilate/v1 — both have tier_distribution + symbol_count."""
|
|
187
|
+
summary: dict[str, Any] = {}
|
|
188
|
+
|
|
189
|
+
l_sc = left.get("symbol_count")
|
|
190
|
+
r_sc = right.get("symbol_count")
|
|
191
|
+
if isinstance(l_sc, int) and isinstance(r_sc, int):
|
|
192
|
+
summary["symbol_count_delta"] = _format_delta(r_sc - l_sc)
|
|
193
|
+
|
|
194
|
+
l_td = left.get("tier_distribution") or {}
|
|
195
|
+
r_td = right.get("tier_distribution") or {}
|
|
196
|
+
if isinstance(l_td, dict) and isinstance(r_td, dict):
|
|
197
|
+
td_delta: dict[str, str] = {}
|
|
198
|
+
for tier in sorted(set(l_td) | set(r_td)):
|
|
199
|
+
lv = int(l_td.get(tier, 0) or 0)
|
|
200
|
+
rv = int(r_td.get(tier, 0) or 0)
|
|
201
|
+
if lv != rv:
|
|
202
|
+
td_delta[tier] = _format_delta(rv - lv)
|
|
203
|
+
if td_delta:
|
|
204
|
+
summary["tier_distribution_delta"] = td_delta
|
|
205
|
+
|
|
206
|
+
# assimilate adds components_emitted
|
|
207
|
+
l_ce = left.get("components_emitted")
|
|
208
|
+
r_ce = right.get("components_emitted")
|
|
209
|
+
if isinstance(l_ce, int) and isinstance(r_ce, int):
|
|
210
|
+
summary["components_emitted_delta"] = _format_delta(r_ce - l_ce)
|
|
211
|
+
return summary
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _summary_synergy(left: dict, right: dict) -> dict:
|
|
215
|
+
l_c = left.get("candidates", []) or []
|
|
216
|
+
r_c = right.get("candidates", []) or []
|
|
217
|
+
l_count = left.get("candidate_count", len(l_c))
|
|
218
|
+
r_count = right.get("candidate_count", len(r_c))
|
|
219
|
+
|
|
220
|
+
def _id(c: dict) -> str:
|
|
221
|
+
return str(c.get("candidate_id", ""))
|
|
222
|
+
|
|
223
|
+
l_ids = {_id(c) for c in l_c if isinstance(c, dict)}
|
|
224
|
+
r_ids = {_id(c) for c in r_c if isinstance(c, dict)}
|
|
225
|
+
new = sorted(r_ids - l_ids)
|
|
226
|
+
dropped = sorted(l_ids - r_ids)
|
|
227
|
+
|
|
228
|
+
summary: dict[str, Any] = {
|
|
229
|
+
"candidate_count_delta": _format_delta(r_count - l_count),
|
|
230
|
+
}
|
|
231
|
+
if new:
|
|
232
|
+
summary["new_candidates"] = new[:_MAX_LIST_ITEMS]
|
|
233
|
+
if dropped:
|
|
234
|
+
summary["dropped_candidates"] = dropped[:_MAX_LIST_ITEMS]
|
|
235
|
+
return summary
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _summary_emergent(left: dict, right: dict) -> dict:
|
|
239
|
+
summary: dict[str, Any] = {}
|
|
240
|
+
l_cs = left.get("catalog_size")
|
|
241
|
+
r_cs = right.get("catalog_size")
|
|
242
|
+
if isinstance(l_cs, int) and isinstance(r_cs, int):
|
|
243
|
+
summary["catalog_size_delta"] = _format_delta(r_cs - l_cs)
|
|
244
|
+
l_ch = left.get("chain_count_considered")
|
|
245
|
+
r_ch = right.get("chain_count_considered")
|
|
246
|
+
if isinstance(l_ch, int) and isinstance(r_ch, int):
|
|
247
|
+
summary["chain_count_delta"] = _format_delta(r_ch - l_ch)
|
|
248
|
+
# candidate-level diff matches synergy
|
|
249
|
+
cand_diff = _summary_synergy(left, right)
|
|
250
|
+
if "candidate_count_delta" in cand_diff:
|
|
251
|
+
summary["candidate_count_delta"] = cand_diff["candidate_count_delta"]
|
|
252
|
+
if "new_candidates" in cand_diff:
|
|
253
|
+
summary["new_candidates"] = cand_diff["new_candidates"]
|
|
254
|
+
if "dropped_candidates" in cand_diff:
|
|
255
|
+
summary["dropped_candidates"] = cand_diff["dropped_candidates"]
|
|
256
|
+
return summary
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
_SUMMARY_BY_FAMILY: dict[str, Any] = {
|
|
260
|
+
"atomadic-forge.certify": _summary_certify,
|
|
261
|
+
"atomadic-forge.wire": _summary_wire,
|
|
262
|
+
"atomadic-forge.scout": _summary_scout_like,
|
|
263
|
+
"atomadic-forge.assimilate": _summary_scout_like,
|
|
264
|
+
"atomadic-forge.synergy.scan": _summary_synergy,
|
|
265
|
+
"atomadic-forge.emergent.scan": _summary_emergent,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# --------------------------------------------------------------------------- #
|
|
270
|
+
# public API
|
|
271
|
+
# --------------------------------------------------------------------------- #
|
|
272
|
+
|
|
273
|
+
def diff_manifests(left: dict, right: dict) -> dict:
|
|
274
|
+
"""Compare two forge manifests and return a structured diff.
|
|
275
|
+
|
|
276
|
+
Both manifests must declare a ``schema_version`` that starts with
|
|
277
|
+
``atomadic-forge.`` — otherwise raise ``ValueError``.
|
|
278
|
+
"""
|
|
279
|
+
l_schema = _require_forge_manifest(left, "left")
|
|
280
|
+
r_schema = _require_forge_manifest(right, "right")
|
|
281
|
+
|
|
282
|
+
l_family = _schema_family(l_schema)
|
|
283
|
+
r_family = _schema_family(r_schema)
|
|
284
|
+
compatible = l_family == r_family
|
|
285
|
+
|
|
286
|
+
summary: dict[str, Any] = {}
|
|
287
|
+
if compatible:
|
|
288
|
+
builder = _SUMMARY_BY_FAMILY.get(l_family)
|
|
289
|
+
if builder is not None:
|
|
290
|
+
summary = builder(left, right)
|
|
291
|
+
|
|
292
|
+
added: list = []
|
|
293
|
+
removed: list = []
|
|
294
|
+
changed: list = []
|
|
295
|
+
_walk(left, right, "", 0, added, removed, changed)
|
|
296
|
+
|
|
297
|
+
# Truncate the generic walks too — keep the diff manifest itself bounded.
|
|
298
|
+
if len(added) > _MAX_LIST_ITEMS:
|
|
299
|
+
added = added[:_MAX_LIST_ITEMS] + [_TRUNCATED]
|
|
300
|
+
if len(removed) > _MAX_LIST_ITEMS:
|
|
301
|
+
removed = removed[:_MAX_LIST_ITEMS] + [_TRUNCATED]
|
|
302
|
+
if len(changed) > _MAX_LIST_ITEMS:
|
|
303
|
+
changed = changed[:_MAX_LIST_ITEMS] + [_TRUNCATED]
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"schema_version": _DIFF_SCHEMA,
|
|
307
|
+
"left_schema": l_schema,
|
|
308
|
+
"right_schema": r_schema,
|
|
309
|
+
"compatible": compatible,
|
|
310
|
+
"summary": summary,
|
|
311
|
+
"added": added,
|
|
312
|
+
"removed": removed,
|
|
313
|
+
"changed": changed,
|
|
314
|
+
}
|