dandelion-memory 1.0.0rc15__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 (232) hide show
  1. benchmarks/__init__.py +3 -0
  2. benchmarks/adapters/__init__.py +29 -0
  3. benchmarks/adapters/ablations.py +406 -0
  4. benchmarks/adapters/base.py +72 -0
  5. benchmarks/adapters/controlled/__init__.py +13 -0
  6. benchmarks/adapters/controlled/core_archival.py +115 -0
  7. benchmarks/adapters/controlled/extract_resolve.py +130 -0
  8. benchmarks/adapters/controlled/hybrid_rag.py +120 -0
  9. benchmarks/adapters/controlled/no_late_binding.py +113 -0
  10. benchmarks/adapters/dandelion.py +94 -0
  11. benchmarks/adapters/native/__init__.py +31 -0
  12. benchmarks/adapters/native/base.py +45 -0
  13. benchmarks/adapters/native/dandelion_native.py +164 -0
  14. benchmarks/adapters/native/dandelion_native_b0.py +165 -0
  15. benchmarks/adapters/native/dandelion_native_b1.py +170 -0
  16. benchmarks/adapters/native/dandelion_native_b2.py +169 -0
  17. benchmarks/adapters/native/dandelion_native_b3.py +171 -0
  18. benchmarks/adapters/native/dandelion_native_c4.py +261 -0
  19. benchmarks/adapters/native/dandelion_native_c4_1.py +281 -0
  20. benchmarks/adapters/native/dandelion_native_c4_2.py +435 -0
  21. benchmarks/adapters/native/event_evidence_b2.py +616 -0
  22. benchmarks/adapters/native/evidence_b2.py +609 -0
  23. benchmarks/adapters/native/letta_native.py +253 -0
  24. benchmarks/adapters/native/mem0_native.py +219 -0
  25. benchmarks/adapters/native/store_vector_b0.py +792 -0
  26. benchmarks/adapters/native/store_vector_b2.py +808 -0
  27. benchmarks/agent_adapter_stdio_acceptance.py +86 -0
  28. benchmarks/bootstrap_cross_stack.py +218 -0
  29. benchmarks/canonical_profile_instrumentation.py +167 -0
  30. benchmarks/canonical_public_baseline.py +288 -0
  31. benchmarks/cli.py +525 -0
  32. benchmarks/collector_public30_offline.py +1653 -0
  33. benchmarks/datasets/__init__.py +45 -0
  34. benchmarks/datasets/conversational_generator.py +238 -0
  35. benchmarks/datasets/generator.py +283 -0
  36. benchmarks/datasets/holdout_topology_generator.py +290 -0
  37. benchmarks/datasets/holdout_topology_generator_v2.py +294 -0
  38. benchmarks/datasets/multi_world.py +1643 -0
  39. benchmarks/datasets/scale_generator.py +289 -0
  40. benchmarks/datasets/schema.py +57 -0
  41. benchmarks/deep_collision_stress.py +199 -0
  42. benchmarks/evaluators/__init__.py +12 -0
  43. benchmarks/evaluators/manifest.py +142 -0
  44. benchmarks/evaluators/metrics.py +277 -0
  45. benchmarks/evaluators/runner.py +258 -0
  46. benchmarks/f1_step0/build_dev_measurement_manifest_v1.py +127 -0
  47. benchmarks/f1_step0/build_dev_measurement_manifest_v2.py +127 -0
  48. benchmarks/f1_step0/build_dev_measurement_manifest_v3.py +146 -0
  49. benchmarks/f1_step0/build_ledger.py +143 -0
  50. benchmarks/f1_step0/build_ledger_v2.py +296 -0
  51. benchmarks/f1_step0/compare_dev52_readiness_v1.py +106 -0
  52. benchmarks/f1_step0/locator_public_smoke.py +172 -0
  53. benchmarks/f1_step0/prepare_dev52_owner_v1.py +133 -0
  54. benchmarks/f1_step0/prepare_dev52_owner_v2.py +133 -0
  55. benchmarks/f1_step0/public_identity_probe.py +191 -0
  56. benchmarks/f1_step0/public_response_mapper.py +101 -0
  57. benchmarks/f1_step0/real_source_public_preflight.py +244 -0
  58. benchmarks/f1_step0/run_exact_fusion_dev_experiment_v1.py +83 -0
  59. benchmarks/f1_step0/scorer.py +110 -0
  60. benchmarks/f1_step0/source_aware_dev_measurement_v1.py +627 -0
  61. benchmarks/f1_step0/source_aware_dev_measurement_v2.py +659 -0
  62. benchmarks/f1_step0/source_aware_dev_measurement_v3.py +776 -0
  63. benchmarks/f1_step0/test_public_response_mapper.py +110 -0
  64. benchmarks/f1_step0/test_real_source_public_preflight.py +25 -0
  65. benchmarks/f1_step0/test_scorer.py +158 -0
  66. benchmarks/f1_step0/test_source_aware_dev_measurement.py +189 -0
  67. benchmarks/f1_step0/test_source_aware_dev_measurement_v2.py +263 -0
  68. benchmarks/f1_step0/test_source_aware_dev_measurement_v3.py +334 -0
  69. benchmarks/f1_step0/typed_readback_public_smoke.py +181 -0
  70. benchmarks/forensic_c4_1_dev.py +258 -0
  71. benchmarks/forensic_c4_dev.py +255 -0
  72. benchmarks/forensic_deep25_c4_1.py +478 -0
  73. benchmarks/legacy_migration_acceptance.py +29 -0
  74. benchmarks/longmemeval_actor.py +497 -0
  75. benchmarks/longmemeval_agent_judge.py +593 -0
  76. benchmarks/longmemeval_budget.py +139 -0
  77. benchmarks/longmemeval_full.py +410 -0
  78. benchmarks/longmemeval_judge.py +327 -0
  79. benchmarks/longmemeval_pilot.py +316 -0
  80. benchmarks/longmemeval_s.py +318 -0
  81. benchmarks/memory_bench.py +248 -0
  82. benchmarks/memory_formation/__init__.py +1 -0
  83. benchmarks/memory_formation/audit.py +801 -0
  84. benchmarks/memory_formation/real_audit.py +688 -0
  85. benchmarks/memory_session_acceptance.py +279 -0
  86. benchmarks/native_codex_host.py +513 -0
  87. benchmarks/owner_dense_acceptance.py +126 -0
  88. benchmarks/pilot_seeker_vault.py +266 -0
  89. benchmarks/prepare_public30_ripgrep_range_owner.py +115 -0
  90. benchmarks/profile_canonical.py +373 -0
  91. benchmarks/profile_canonical_lanes.py +405 -0
  92. benchmarks/profile_search_chunks.py +351 -0
  93. benchmarks/public_ranges_faq_proof.py +121 -0
  94. benchmarks/public_repos/build_history_scope_v3.py +330 -0
  95. benchmarks/public_repos/build_oracle.py +108 -0
  96. benchmarks/public_repos/build_oracle_v2.py +169 -0
  97. benchmarks/public_repos/build_oracle_v3.py +176 -0
  98. benchmarks/public_repos/map_public_v2.py +296 -0
  99. benchmarks/public_repos/project_prompts_v2.py +31 -0
  100. benchmarks/public_repos/project_prompts_v3.py +29 -0
  101. benchmarks/public_repos/ready_admin_v2.py +214 -0
  102. benchmarks/public_repos/score_public_v2.py +258 -0
  103. benchmarks/public_repos/test_map_public_v2.py +171 -0
  104. benchmarks/public_repos/test_public_repo_v2.py +44 -0
  105. benchmarks/public_repos/test_public_repo_v3.py +46 -0
  106. benchmarks/public_repos/test_ready_admin_v2.py +117 -0
  107. benchmarks/public_repos/test_score_public_v2.py +163 -0
  108. benchmarks/public_repos/test_wire_v2.py +292 -0
  109. benchmarks/public_repos/wire_v2.py +603 -0
  110. benchmarks/real_blocker_fix_acceptance.py +155 -0
  111. benchmarks/real_scan_rescan_acceptance.py +41 -0
  112. benchmarks/recall_v2_20k_smoke.py +112 -0
  113. benchmarks/recall_v2_e1_retrieve.py +70 -0
  114. benchmarks/recall_v2_e1_score.py +271 -0
  115. benchmarks/recall_v2_e2_prepare.py +69 -0
  116. benchmarks/recall_v2_e2_public_score.py +91 -0
  117. benchmarks/recall_v2_e2_score.py +108 -0
  118. benchmarks/recall_v2_navigation_mcp_replay.py +308 -0
  119. benchmarks/recall_v2_reader_capture.py +118 -0
  120. benchmarks/recall_v2_reader_replay.py +136 -0
  121. benchmarks/run_blind_h10_holdout.py +284 -0
  122. benchmarks/run_blind_h9_holdout.py +275 -0
  123. benchmarks/run_c4_2_nightly_scale.py +446 -0
  124. benchmarks/run_context_scale_simulation.py +189 -0
  125. benchmarks/run_conversational_bench.py +170 -0
  126. benchmarks/run_h11_evaluation.py +316 -0
  127. benchmarks/run_h11_v2_evaluation.py +431 -0
  128. benchmarks/run_native_fixed12.py +469 -0
  129. benchmarks/run_native_preflight.py +280 -0
  130. benchmarks/run_scale_stress.py +225 -0
  131. benchmarks/solanazation_brownfield_01/build_oracle.py +970 -0
  132. benchmarks/solanazation_brownfield_01/check_level_a.py +78 -0
  133. benchmarks/solanazation_brownfield_01/debug_scope.py +13 -0
  134. benchmarks/solanazation_brownfield_01/evaluate_bootstrap_02.py +570 -0
  135. benchmarks/solanazation_brownfield_01/evaluate_first_pass.py +306 -0
  136. benchmarks/solanazation_brownfield_01/generate_dryrun_report.py +222 -0
  137. benchmarks/solanazation_brownfield_01/generate_manifests.py +146 -0
  138. benchmarks/solanazation_brownfield_01/inspect_misses.py +29 -0
  139. benchmarks/solanazation_brownfield_01/run_miss_forensic.py +206 -0
  140. benchmarks/solanazation_brownfield_01/test_facts.py +15 -0
  141. benchmarks/statistics/__init__.py +15 -0
  142. benchmarks/statistics/significance.py +326 -0
  143. benchmarks/test_native_mcp_preflight.py +229 -0
  144. benchmarks/test_prepare_public30_range_owner.py +35 -0
  145. benchmarks/test_public30_source_reference.py +698 -0
  146. benchmarks/test_recall_v2_e1_reader_input.py +89 -0
  147. benchmarks/test_recall_v2_navigation_mcp_replay.py +155 -0
  148. benchmarks/test_recall_v2_reader_replay.py +41 -0
  149. dandelion/__init__.py +3 -0
  150. dandelion/__main__.py +11 -0
  151. dandelion/agent_adapter.py +2369 -0
  152. dandelion/bootstrap/__init__.py +872 -0
  153. dandelion/bootstrap/alias_policy.py +50 -0
  154. dandelion/bootstrap/authority.py +225 -0
  155. dandelion/bootstrap/canonical_history.py +412 -0
  156. dandelion/bootstrap/classifier.py +403 -0
  157. dandelion/bootstrap/component_graph.py +280 -0
  158. dandelion/bootstrap/discovery.py +279 -0
  159. dandelion/bootstrap/domain_induction.py +180 -0
  160. dandelion/bootstrap/domain_topology.py +673 -0
  161. dandelion/bootstrap/evidence_envelope.py +132 -0
  162. dandelion/bootstrap/extractors/base.py +171 -0
  163. dandelion/bootstrap/extractors/exact.py +1234 -0
  164. dandelion/bootstrap/extractors/generic.py +100 -0
  165. dandelion/bootstrap/extractors/git_temporal.py +111 -0
  166. dandelion/bootstrap/extractors/registry.py +345 -0
  167. dandelion/bootstrap/extractors/relations.py +89 -0
  168. dandelion/bootstrap/extractors/semantic.py +424 -0
  169. dandelion/bootstrap/extractors/status.py +218 -0
  170. dandelion/bootstrap/git_sweeper.py +135 -0
  171. dandelion/bootstrap/manifest.py +246 -0
  172. dandelion/bootstrap/quarantine.py +264 -0
  173. dandelion/bootstrap/report.py +259 -0
  174. dandelion/bootstrap/root_resolver.py +221 -0
  175. dandelion/bootstrap/tech_profiler.py +125 -0
  176. dandelion/bootstrap/topology.py +75 -0
  177. dandelion/bootstrap/topology_index.py +110 -0
  178. dandelion/bootstrap/transaction.py +315 -0
  179. dandelion/canonical_bootstrap.py +257 -0
  180. dandelion/canonical_memory.py +4130 -0
  181. dandelion/captured_input.py +295 -0
  182. dandelion/cli.py +767 -0
  183. dandelion/config.py +40 -0
  184. dandelion/derived_index.py +745 -0
  185. dandelion/domains.py +706 -0
  186. dandelion/embedder.py +229 -0
  187. dandelion/event_evidence.py +571 -0
  188. dandelion/evidence.py +812 -0
  189. dandelion/evidence_response.py +453 -0
  190. dandelion/evidence_workspace.py +2360 -0
  191. dandelion/exact_context.py +129 -0
  192. dandelion/hydrator.py +124 -0
  193. dandelion/inspector.py +442 -0
  194. dandelion/inspector_assets/dandelion-mark.svg +33 -0
  195. dandelion/inspector_assets/dandelion_graph.js +361 -0
  196. dandelion/inspector_assets/inspector.css +223 -0
  197. dandelion/inspector_assets/inspector.html +135 -0
  198. dandelion/inspector_assets/inspector.js +1059 -0
  199. dandelion/legacy_import.py +431 -0
  200. dandelion/live_source_grants.py +133 -0
  201. dandelion/local_owner.py +114 -0
  202. dandelion/memory_client.py +252 -0
  203. dandelion/memory_contract.py +107 -0
  204. dandelion/memory_owner.py +560 -0
  205. dandelion/reader_providers.py +118 -0
  206. dandelion/recall_v2.py +767 -0
  207. dandelion/reconciler.py +219 -0
  208. dandelion/request_budget.py +125 -0
  209. dandelion/retained_basis.py +223 -0
  210. dandelion/retrieval.py +485 -0
  211. dandelion/router.py +405 -0
  212. dandelion/search_terms.py +53 -0
  213. dandelion/security.py +124 -0
  214. dandelion/server.py +810 -0
  215. dandelion/source_content_fts.py +219 -0
  216. dandelion/source_delivery.py +89 -0
  217. dandelion/source_history.py +241 -0
  218. dandelion/source_identity.py +88 -0
  219. dandelion/source_navigator.py +5 -0
  220. dandelion/source_reader.py +444 -0
  221. dandelion/source_references.py +379 -0
  222. dandelion/source_sections.py +265 -0
  223. dandelion/store_kv.py +684 -0
  224. dandelion/store_vector.py +1289 -0
  225. dandelion/studio.py +325 -0
  226. dandelion/wal.py +227 -0
  227. dandelion_memory-1.0.0rc15.dist-info/METADATA +48 -0
  228. dandelion_memory-1.0.0rc15.dist-info/RECORD +232 -0
  229. dandelion_memory-1.0.0rc15.dist-info/WHEEL +5 -0
  230. dandelion_memory-1.0.0rc15.dist-info/entry_points.txt +5 -0
  231. dandelion_memory-1.0.0rc15.dist-info/licenses/LICENSE +21 -0
  232. dandelion_memory-1.0.0rc15.dist-info/top_level.txt +2 -0
@@ -0,0 +1,279 @@
1
+ """Bounded subprocess-only acceptance trace for the canonical memory owner.
2
+
3
+ Every operation goes through the public ``memory-owner`` or ``memory`` CLI.
4
+ The runner does not bootstrap a project, load a model, talk to MCP, or mutate
5
+ SQLite directly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ import time
19
+ import uuid
20
+ from typing import Any
21
+
22
+
23
+ def _sha256(data: bytes | str) -> str:
24
+ return hashlib.sha256(data.encode() if isinstance(data, str) else data).hexdigest()
25
+
26
+
27
+ def _env(root: Path, workspace: Path) -> dict[str, str]:
28
+ env = dict(os.environ)
29
+ env["DANDELION_SECRET_POLICY"] = "deny"
30
+ old = env.get("PYTHONPATH", "")
31
+ env["PYTHONPATH"] = str(workspace) + (os.pathsep + old if old else "")
32
+ return env
33
+
34
+
35
+ def _event(trace: dict[str, Any], event: dict[str, Any]) -> None:
36
+ event = dict(event)
37
+ event["seq"] = len(trace["events"]) + 1
38
+ trace["events"].append(event)
39
+
40
+
41
+ def _assert(trace: dict[str, Any], ident: str, passed: bool, observed: Any,
42
+ expected: Any, session: str) -> None:
43
+ trace["assertions"].append({"id": ident, "session": session,
44
+ "passed": bool(passed), "observed": observed,
45
+ "expected": expected})
46
+
47
+
48
+ def _run(trace: dict[str, Any], root: Path, workspace: Path,
49
+ args: list[str], session: str) -> tuple[subprocess.CompletedProcess[str], Any]:
50
+ started = time.perf_counter()
51
+ proc = subprocess.run([sys.executable, "-m", "dandelion.cli", *args],
52
+ cwd=str(workspace), env=_env(root, workspace),
53
+ capture_output=True, text=True, check=False)
54
+ parsed: Any = None
55
+ if proc.stdout.strip():
56
+ try:
57
+ parsed = json.loads(proc.stdout)
58
+ except json.JSONDecodeError:
59
+ parsed = None
60
+ _event(trace, {"kind": "cli", "session": session, "fresh_process": True,
61
+ "argv": args, "exit_code": proc.returncode,
62
+ "duration_ms": round((time.perf_counter() - started) * 1000, 3),
63
+ "stdout_sha256": _sha256(proc.stdout),
64
+ "stderr_sha256": _sha256(proc.stderr),
65
+ "stdout": proc.stdout, "stderr": proc.stderr,
66
+ "response": parsed})
67
+ return proc, parsed
68
+
69
+
70
+ def _memory(trace: dict[str, Any], root: Path, workspace: Path, operation: str,
71
+ data: dict[str, Any], session: str) -> dict[str, Any]:
72
+ proc, parsed = _run(trace, root, workspace,
73
+ ["memory", operation, "--path", str(root),
74
+ "--data", json.dumps(data, ensure_ascii=False, sort_keys=True)],
75
+ session)
76
+ if proc.returncode != 0 or not isinstance(parsed, dict):
77
+ raise AssertionError(f"memory {operation} failed: {proc.returncode}: {proc.stderr}")
78
+ return parsed
79
+
80
+
81
+ def _start_owner(trace: dict[str, Any], root: Path, workspace: Path,
82
+ session: str) -> tuple[subprocess.Popen[str], dict[str, Any]]:
83
+ proc = subprocess.Popen([sys.executable, "-m", "dandelion.cli", "memory-owner",
84
+ "start", "--path", str(root)],
85
+ cwd=str(workspace), env=_env(root, workspace),
86
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
87
+ text=True)
88
+ deadline = time.monotonic() + 10
89
+ status: dict[str, Any] | None = None
90
+ while time.monotonic() < deadline:
91
+ if proc.poll() is not None:
92
+ detail = proc.stderr.read() if proc.stderr else ""
93
+ raise AssertionError(f"owner exited during start: {proc.returncode}: {detail}")
94
+ status_proc, candidate = _run(trace, root, workspace,
95
+ ["memory-owner", "status", "--path", str(root)], session)
96
+ if status_proc.returncode == 0 and isinstance(candidate, dict):
97
+ status = candidate
98
+ break
99
+ time.sleep(0.05)
100
+ if status is None:
101
+ raise AssertionError("owner did not become available within 10 seconds")
102
+ trace["sessions"][session].update({"owner_pid": proc.pid,
103
+ "owner_id": status.get("owner_id"),
104
+ "status": status})
105
+ return proc, status
106
+
107
+
108
+ def _stop_owner(trace: dict[str, Any], root: Path, workspace: Path,
109
+ proc: subprocess.Popen[str], session: str) -> None:
110
+ stop_proc, stop_result = _run(trace, root, workspace,
111
+ ["memory-owner", "stop", "--path", str(root)], session)
112
+ if stop_proc.returncode != 0:
113
+ raise AssertionError(f"owner stop failed: {stop_proc.stderr}")
114
+ try:
115
+ proc.wait(timeout=10)
116
+ except subprocess.TimeoutExpired:
117
+ proc.kill()
118
+ proc.wait(timeout=5)
119
+ raise AssertionError("owner did not stop after public memory-owner stop")
120
+ trace["sessions"][session]["stop"] = stop_result
121
+
122
+
123
+ def _fixture(root: Path) -> str:
124
+ directory = root / ".dandelion"
125
+ directory.mkdir(parents=True, exist_ok=True)
126
+ legacy = {"legacy.endpoint": {"value": "https://legacy.example", "source": "legacy_tool", "confidence": 0.7}}
127
+ path = directory / "facts.json"
128
+ path.write_text(json.dumps(legacy, sort_keys=True, indent=2) + "\n", encoding="utf-8")
129
+ return _sha256(path.read_bytes())
130
+
131
+
132
+ def run_acceptance(root: Path, *, workspace: Path | None = None,
133
+ output: Path | None = None) -> dict[str, Any]:
134
+ """Run the owner Session A/B scenario and persist its machine trace."""
135
+ workspace = (workspace or Path(__file__).resolve().parents[1]).resolve()
136
+ root = root.resolve()
137
+ root.mkdir(parents=True, exist_ok=True)
138
+ legacy_hash_before = _fixture(root)
139
+ trace: dict[str, Any] = {
140
+ "schema_version": 2,
141
+ "trace_kind": "canonical_memory_owner_cli_acceptance",
142
+ "trace_id": uuid.uuid4().hex,
143
+ "workspace": str(root),
144
+ "sessions": {"A": {"fresh_process": True}, "B": {"fresh_process": True}},
145
+ "events": [], "assertions": [],
146
+ "scenario": {"fact": "service.port", "values": ["7311", "7422"],
147
+ "decision_reason": "audit writes require ACID",
148
+ "secret": "auth.api_token", "legacy": "legacy.endpoint"},
149
+ }
150
+ owner_a: subprocess.Popen[str] | None = None
151
+ owner_b: subprocess.Popen[str] | None = None
152
+ try:
153
+ owner_a, status_a = _start_owner(trace, root, workspace, "A")
154
+ before = _memory(trace, root, workspace, "status", {}, "A")
155
+ _assert(trace, "A_owner_started", bool(status_a.get("owner_id")),
156
+ status_a, "OK status and owner id", "A")
157
+ _memory(trace, root, workspace, "set-fact", {"key": "service.port", "value": "7311",
158
+ "source": "repo_observed", "authority": "REPO", "reason": "config file says initial port",
159
+ "confidence": 0.9}, "A")
160
+ decision = _memory(trace, root, workspace, "remember", {
161
+ "domain": "decisions", "source": "agent_inferred", "authority": "AGENT",
162
+ "reason": "audit writes require ACID",
163
+ "content": "Decision: PostgreSQL audit writes require ACID; live port {{ref:service.port}}; pinned port {{snapshot:service.port}}.",
164
+ }, "A")
165
+ _memory(trace, root, workspace, "set-fact", {"key": "service.port", "value": "7422",
166
+ "source": "tool_observed", "authority": "TOOL", "reason": "rollout changed port",
167
+ "confidence": 0.99}, "A")
168
+ current = _memory(trace, root, workspace, "get-fact", {"key": "service.port"}, "A")
169
+ history = _memory(trace, root, workspace, "get-fact", {"key": "service.port", "revision": 1}, "A")
170
+ recall = _memory(trace, root, workspace, "recall", {"query": "PostgreSQL audit port", "domain": "decisions", "limit": 3}, "A")
171
+ text = " ".join(str(item) for item in recall.get("results", []))
172
+ _assert(trace, "A_current_history", current.get("value") == "7422" and history.get("value") == "7311",
173
+ {"current": current, "history": history}, "current 7422/history 7311", "A")
174
+ _assert(trace, "A_provenance_reason", current.get("source") == "tool_observed" and current.get("reason") == "rollout changed port" and
175
+ recall.get("results", [{}])[0].get("source") == "agent_inferred" and recall.get("results", [{}])[0].get("reason") == "audit writes require ACID",
176
+ {"fact": current, "memory": recall}, "source/authority/reason preserved", "A")
177
+ _assert(trace, "A_live_and_pinned", "7422" in text and "7311" in text, text, "live 7422 and pinned 7311", "A")
178
+ _memory(trace, root, workspace, "set-fact", {"key": "auth.api_token", "value": "never-return",
179
+ "source": "tool_observed", "authority": "TOOL", "reason": "credential fixture", "security_class": "SECRET"}, "A")
180
+ _memory(trace, root, workspace, "remember", {"domain": "security", "source": "tool_observed",
181
+ "authority": "TOOL", "reason": "credential fixture", "security_class": "SECRET",
182
+ "content": "Restricted token {{ref:auth.api_token}}."}, "A")
183
+ denied_fact = _memory(trace, root, workspace, "get-fact", {"key": "auth.api_token"}, "A")
184
+ denied_memory = _memory(trace, root, workspace, "recall", {"query": "Restricted token", "domain": "security"}, "A")
185
+ _assert(
186
+ trace, "A_restricted_denied",
187
+ denied_fact.get("status") == "DENIED"
188
+ and denied_fact.get("value") == "[DENIED]"
189
+ and denied_memory.get("status") == "UNKNOWN"
190
+ and denied_memory.get("results") == [],
191
+ {"fact": denied_fact, "memory": denied_memory},
192
+ "exact fact is DENIED; restricted discovery is non-enumerating", "A",
193
+ )
194
+ unknown = _memory(trace, root, workspace, "query-claim", {"subject": "payment.service", "predicate": "database", "domain": "ops"}, "A")
195
+ _memory(trace, root, workspace, "put-claim", {"subject": "payment.service", "predicate": "database", "domain": "ops", "polarity": "ASSERT", "value": "postgres", "source": "repo_observed", "authority": "REPO", "reason": "deployment manifest"}, "A")
196
+ _memory(trace, root, workspace, "put-claim", {"subject": "payment.service", "predicate": "database", "domain": "ops", "polarity": "ASSERT", "value": "mysql", "source": "tool_observed", "authority": "TOOL", "reason": "runtime probe"}, "A")
197
+ conflict = _memory(trace, root, workspace, "query-claim", {"subject": "payment.service", "predicate": "database", "domain": "ops"}, "A")
198
+ _assert(trace, "A_unknown_and_conflict", unknown.get("status") == "UNKNOWN" and conflict.get("status") == "CONFLICT",
199
+ {"unknown": unknown, "conflict": conflict}, "UNKNOWN then CONFLICT", "A")
200
+ before_preview = _memory(trace, root, workspace, "status", {}, "A")
201
+ preview1 = _memory(trace, root, workspace, "import-legacy", {"preview": True}, "A")
202
+ status_preview = _memory(trace, root, workspace, "status", {}, "A")
203
+ preview2 = _memory(trace, root, workspace, "import-legacy", {"preview": True}, "A")
204
+ legacy_hash_after = _sha256((root / ".dandelion" / "facts.json").read_bytes())
205
+ unknown_legacy = _memory(trace, root, workspace, "get-fact", {"key": "legacy.endpoint"}, "A")
206
+ _assert(trace, "A_preview_is_non_mutating", preview1 == preview2 and preview1.get("status") == "PREVIEW" and
207
+ status_preview.get("counts") == before_preview.get("counts") and legacy_hash_before == legacy_hash_after and unknown_legacy.get("status") == "UNKNOWN",
208
+ {"first": preview1, "second": preview2, "before": before_preview, "after": status_preview, "hash_before": legacy_hash_before, "hash_after": legacy_hash_after, "legacy_fact": unknown_legacy},
209
+ "repeatable PREVIEW, unchanged counts/file, no imported fact", "A")
210
+ rebuild1 = _memory(trace, root, workspace, "rebuild-index", {}, "A")
211
+ after_rebuild1 = _memory(trace, root, workspace, "status", {}, "A")
212
+ rebuild2 = _memory(trace, root, workspace, "rebuild-index", {}, "A")
213
+ after_rebuild2 = _memory(trace, root, workspace, "status", {}, "A")
214
+ recall_after = _memory(trace, root, workspace, "recall", {"query": "PostgreSQL audit port", "domain": "decisions", "limit": 3}, "A")
215
+ _assert(trace, "A_rebuild_preserves_canonical", rebuild1.get("status") == "OK" and rebuild2.get("status") == "OK" and
216
+ after_rebuild1.get("counts") == after_rebuild2.get("counts") and after_rebuild2.get("counts", {}).get("memory_index") == after_rebuild2.get("counts", {}).get("memories") and
217
+ recall_after.get("results", [{}])[0].get("memory_id") == decision.get("memory_id"),
218
+ {"rebuild1": rebuild1, "rebuild2": rebuild2, "status": after_rebuild2, "recall": recall_after},
219
+ "canonical counts preserved and index rebuilt", "A")
220
+ _stop_owner(trace, root, workspace, owner_a, "A")
221
+ owner_a = None
222
+ trace["sessions"]["A"]["closed_before_B"] = True
223
+
224
+ owner_b, status_b = _start_owner(trace, root, workspace, "B")
225
+ trace["sessions"]["B"]["started_after_A"] = True
226
+ current_b = _memory(trace, root, workspace, "get-fact", {"key": "service.port"}, "B")
227
+ history_b = _memory(trace, root, workspace, "get-fact", {"key": "service.port", "revision": 1}, "B")
228
+ recall_b = _memory(trace, root, workspace, "recall", {"query": "PostgreSQL audit port", "domain": "decisions", "limit": 3}, "B")
229
+ conflict_b = _memory(trace, root, workspace, "query-claim", {"subject": "payment.service", "predicate": "database", "domain": "ops"}, "B")
230
+ denied_b = _memory(trace, root, workspace, "get-fact", {"key": "auth.api_token"}, "B")
231
+ rebuild_b = _memory(trace, root, workspace, "rebuild-index", {}, "B")
232
+ _assert(trace, "B_restart_persists_state", bool(status_b.get("owner_id")) and current_b.get("value") == "7422" and history_b.get("value") == "7311" and
233
+ "7422" in json.dumps(recall_b) and "7311" in json.dumps(recall_b) and conflict_b.get("status") == "CONFLICT" and denied_b.get("status") == "DENIED" and rebuild_b.get("status") == "OK",
234
+ {"status": status_b, "current": current_b, "history": history_b, "recall": recall_b, "conflict": conflict_b, "denied": denied_b, "rebuild": rebuild_b},
235
+ "fresh Session B sees current/history/live+pinned/conflict/denied/rebuilt", "B")
236
+ _stop_owner(trace, root, workspace, owner_b, "B")
237
+ owner_b = None
238
+ trace["sessions"]["B"]["closed"] = True
239
+ finally:
240
+ for proc, session in ((owner_b, "B-cleanup"), (owner_a, "A-cleanup")):
241
+ if proc is not None and proc.poll() is None:
242
+ try:
243
+ _run(trace, root, workspace, ["memory-owner", "stop", "--path", str(root)], session)
244
+ except Exception:
245
+ proc.kill()
246
+ try:
247
+ proc.wait(timeout=5)
248
+ except subprocess.TimeoutExpired:
249
+ proc.kill()
250
+ trace["status"] = "passed" if trace["assertions"] and all(item["passed"] for item in trace["assertions"]) else "failed"
251
+ trace_path = output or (root / ".dandelion" / "memory_acceptance_trace.json")
252
+ trace_path.parent.mkdir(parents=True, exist_ok=True)
253
+ trace["trace_path"] = str(trace_path)
254
+ trace_path.write_text(json.dumps(trace, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
255
+ return trace
256
+
257
+
258
+ def main(argv: list[str] | None = None) -> int:
259
+ parser = argparse.ArgumentParser(description=__doc__)
260
+ parser.add_argument("--root", type=Path)
261
+ parser.add_argument("--output", type=Path)
262
+ args = parser.parse_args(argv)
263
+ temporary: tempfile.TemporaryDirectory[str] | None = None
264
+ if args.root is None:
265
+ temporary = tempfile.TemporaryDirectory(prefix="dandelion-memory-acceptance-")
266
+ root = Path(temporary.name)
267
+ else:
268
+ root = args.root
269
+ try:
270
+ trace = run_acceptance(root, output=args.output)
271
+ print(json.dumps(trace, indent=2, ensure_ascii=False))
272
+ return 0 if trace["status"] == "passed" else 1
273
+ finally:
274
+ if temporary is not None:
275
+ temporary.cleanup()
276
+
277
+
278
+ if __name__ == "__main__":
279
+ raise SystemExit(main())