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