superlocalmemory 3.8.13 → 4.0.0

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 (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  3. package/README.md +65 -63
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +36 -9
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -0,0 +1,114 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from types import MappingProxyType
9
+ from typing import Any, Mapping, Protocol, runtime_checkable
10
+
11
+
12
+ class ObligationKind(StrEnum):
13
+ APPLY = "apply"
14
+ ERASE = "erase"
15
+
16
+
17
+ class ObligationState(StrEnum):
18
+ PENDING = "pending"
19
+ APPLIED = "applied"
20
+ VERIFIED = "verified"
21
+ FAILED = "failed"
22
+ COMPENSATED = "compensated"
23
+ ERASED = "erased"
24
+
25
+
26
+ _TERMINAL_SUCCESS = frozenset({ObligationState.VERIFIED, ObligationState.ERASED})
27
+
28
+
29
+ def is_terminal_success(state: ObligationState) -> bool:
30
+ return state in _TERMINAL_SUCCESS
31
+
32
+
33
+ def _freeze(mapping: Mapping[str, Any] | None) -> Mapping[str, Any]:
34
+ return MappingProxyType(dict(mapping or {}))
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class OperationContext:
39
+ operation_id: str
40
+ profile_id: str
41
+ subject_id: str
42
+ revision: int = 0
43
+ generation: int = 0
44
+ fact_ids: tuple[str, ...] = ()
45
+ payload: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
46
+
47
+ def __post_init__(self) -> None:
48
+ if not self.operation_id:
49
+ raise ValueError("operation_id is required")
50
+ if not self.profile_id:
51
+ raise ValueError("profile_id is required")
52
+ if not self.subject_id:
53
+ raise ValueError("subject_id is required")
54
+ object.__setattr__(self, "fact_ids", tuple(self.fact_ids))
55
+ object.__setattr__(self, "payload", _freeze(self.payload))
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class OwnerResult:
60
+ owner: str
61
+ ok: bool
62
+ checksum: str | None = None
63
+ detail: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
64
+
65
+ def __post_init__(self) -> None:
66
+ object.__setattr__(self, "detail", _freeze(self.detail))
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class OwnerErasureProof:
71
+ owner: str
72
+ erased: bool
73
+ checksum: str | None = None
74
+ detail: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
75
+
76
+ def __post_init__(self) -> None:
77
+ object.__setattr__(self, "detail", _freeze(self.detail))
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class OwnerHealth:
82
+ owner: str
83
+ healthy: bool
84
+ detail: str = ""
85
+
86
+
87
+ @runtime_checkable
88
+ class ProjectionOwner(Protocol):
89
+ @property
90
+ def name(self) -> str: ...
91
+
92
+ def apply(self, context: OperationContext) -> OwnerResult: ...
93
+
94
+ def verify(self, context: OperationContext) -> OwnerResult: ...
95
+
96
+ def compensate(self, context: OperationContext) -> OwnerResult: ...
97
+
98
+ def erase(self, context: OperationContext) -> OwnerErasureProof: ...
99
+
100
+ def prove_erased(self, context: OperationContext) -> OwnerErasureProof: ...
101
+
102
+ def health(self) -> OwnerHealth: ...
103
+
104
+
105
+ __all__ = [
106
+ "ObligationKind",
107
+ "ObligationState",
108
+ "OperationContext",
109
+ "OwnerErasureProof",
110
+ "OwnerHealth",
111
+ "OwnerResult",
112
+ "ProjectionOwner",
113
+ "is_terminal_success",
114
+ ]
@@ -0,0 +1,285 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib as _hashlib
7
+ import hmac as _hmac
8
+ import json
9
+ import sqlite3
10
+ import time
11
+
12
+ from superlocalmemory.core.transactions.manifest import (
13
+ MANIFEST_V1,
14
+ MANIFEST_V2,
15
+ CompletionManifest,
16
+ ManifestState,
17
+ OwnerEvidence,
18
+ build_evidence,
19
+ compute_envelope_hash,
20
+ compute_envelope_hmac,
21
+ derive_state,
22
+ evidence_json,
23
+ hash_envelope_fields,
24
+ verify_envelope_hmac,
25
+ )
26
+ from superlocalmemory.core.transactions.obligations import ObligationLedger
27
+ from superlocalmemory.core.transactions.owners import ObligationKind, ObligationState
28
+
29
+
30
+ class Reconciler:
31
+ def __init__(self, ledger: ObligationLedger | None = None) -> None:
32
+ self._ledger = ledger or ObligationLedger()
33
+
34
+ def reconcile(
35
+ self,
36
+ conn: sqlite3.Connection,
37
+ operation_id: str,
38
+ profile_id: str,
39
+ *,
40
+ canonical_committed: bool | None = None,
41
+ ) -> CompletionManifest:
42
+ committed = self._resolve_canonical(conn, operation_id, canonical_committed)
43
+ obligations = self._ledger.fetch(conn, operation_id)
44
+ evidence = build_evidence(obligations)
45
+ state, all_met = derive_state(obligations, canonical_committed=committed)
46
+
47
+ version = _manifest_version_supported(conn)
48
+ if version >= MANIFEST_V2:
49
+ manifest_hash = compute_envelope_hmac(
50
+ operation_id=operation_id,
51
+ profile_id=profile_id,
52
+ state=state,
53
+ all_met=all_met,
54
+ obligation_count=len(obligations),
55
+ evidence=evidence,
56
+ )
57
+ else:
58
+ manifest_hash = compute_envelope_hash(
59
+ operation_id=operation_id,
60
+ profile_id=profile_id,
61
+ state=state,
62
+ all_met=all_met,
63
+ obligation_count=len(obligations),
64
+ evidence=evidence,
65
+ )
66
+
67
+ payload = evidence_json(evidence)
68
+ now = time.time()
69
+ existing = conn.execute(
70
+ "SELECT created_at FROM completion_manifests WHERE operation_id = ?",
71
+ (operation_id,),
72
+ ).fetchone()
73
+ created_at = float(existing[0]) if existing is not None else now
74
+
75
+ if version >= MANIFEST_V2:
76
+ conn.execute(
77
+ "INSERT INTO completion_manifests "
78
+ "(operation_id, profile_id, state, all_met, obligation_count, "
79
+ "owner_evidence_json, manifest_hash, manifest_version, created_at, updated_at) "
80
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
81
+ "ON CONFLICT(operation_id) DO UPDATE SET "
82
+ "profile_id = excluded.profile_id, "
83
+ "state = excluded.state, "
84
+ "all_met = excluded.all_met, "
85
+ "obligation_count = excluded.obligation_count, "
86
+ "owner_evidence_json = excluded.owner_evidence_json, "
87
+ "manifest_hash = excluded.manifest_hash, "
88
+ "manifest_version = excluded.manifest_version, "
89
+ "updated_at = excluded.updated_at",
90
+ (
91
+ operation_id, profile_id, str(state), 1 if all_met else 0,
92
+ len(obligations), payload, manifest_hash, MANIFEST_V2,
93
+ created_at, now,
94
+ ),
95
+ )
96
+ else:
97
+ conn.execute(
98
+ "INSERT INTO completion_manifests "
99
+ "(operation_id, profile_id, state, all_met, obligation_count, "
100
+ "owner_evidence_json, manifest_hash, created_at, updated_at) "
101
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "
102
+ "ON CONFLICT(operation_id) DO UPDATE SET "
103
+ "profile_id = excluded.profile_id, "
104
+ "state = excluded.state, "
105
+ "all_met = excluded.all_met, "
106
+ "obligation_count = excluded.obligation_count, "
107
+ "owner_evidence_json = excluded.owner_evidence_json, "
108
+ "manifest_hash = excluded.manifest_hash, "
109
+ "updated_at = excluded.updated_at",
110
+ (
111
+ operation_id, profile_id, str(state), 1 if all_met else 0,
112
+ len(obligations), payload, manifest_hash, created_at, now,
113
+ ),
114
+ )
115
+
116
+ return CompletionManifest(
117
+ operation_id=operation_id,
118
+ profile_id=profile_id,
119
+ state=state,
120
+ all_met=all_met,
121
+ obligation_count=len(obligations),
122
+ owner_evidence=evidence,
123
+ manifest_hash=manifest_hash,
124
+ created_at=created_at,
125
+ updated_at=now,
126
+ )
127
+
128
+ def fetch_manifest(
129
+ self, conn: sqlite3.Connection, operation_id: str,
130
+ ) -> CompletionManifest | None:
131
+ row = conn.execute(
132
+ "SELECT operation_id, profile_id, state, all_met, obligation_count, "
133
+ "owner_evidence_json, manifest_hash, created_at, updated_at "
134
+ "FROM completion_manifests WHERE operation_id = ?",
135
+ (operation_id,),
136
+ ).fetchone()
137
+ if row is None:
138
+ return None
139
+ return CompletionManifest(
140
+ operation_id=row[0],
141
+ profile_id=row[1],
142
+ state=ManifestState(row[2]),
143
+ all_met=bool(row[3]),
144
+ obligation_count=int(row[4]),
145
+ owner_evidence=_evidence_from_json(row[5]),
146
+ manifest_hash=row[6],
147
+ created_at=float(row[7]),
148
+ updated_at=float(row[8]),
149
+ )
150
+
151
+ def verify_manifest(
152
+ self, conn: sqlite3.Connection, operation_id: str,
153
+ ) -> bool:
154
+ row = conn.execute(
155
+ "SELECT operation_id, profile_id, state, all_met, obligation_count, "
156
+ "owner_evidence_json, manifest_hash "
157
+ "FROM completion_manifests WHERE operation_id = ?",
158
+ (operation_id,),
159
+ ).fetchone()
160
+ if row is None:
161
+ return False
162
+ try:
163
+ evidence_dicts = json.loads(row[5])
164
+ if not isinstance(evidence_dicts, list):
165
+ return False
166
+ except (TypeError, json.JSONDecodeError):
167
+ return False
168
+
169
+ db_version = _manifest_version_supported(conn)
170
+ row_version = _manifest_row_version(conn, operation_id)
171
+
172
+ if db_version >= MANIFEST_V2:
173
+ # On M037-capable DBs NEVER accept the unkeyed v1 SHA path —
174
+ # an attacker can write manifest_version=1 with a valid SHA of
175
+ # mutated content. Any v1 row here is either a downgrade attack
176
+ # or a legacy row that hasn't been re-sealed; both are rejected.
177
+ if row_version < MANIFEST_V2:
178
+ return False
179
+ return verify_envelope_hmac(
180
+ operation_id=row[0],
181
+ profile_id=row[1],
182
+ state=row[2],
183
+ all_met=bool(row[3]),
184
+ obligation_count=int(row[4]),
185
+ evidence_dicts=evidence_dicts,
186
+ stored_mac=row[6],
187
+ )
188
+
189
+ # v1 path (M037 absent) — use constant-time comparison to prevent
190
+ # timing oracles on old SHA256 hex digests.
191
+ recomputed = hash_envelope_fields(
192
+ operation_id=row[0],
193
+ profile_id=row[1],
194
+ state=row[2],
195
+ all_met=bool(row[3]),
196
+ obligation_count=int(row[4]),
197
+ evidence_dicts=evidence_dicts,
198
+ )
199
+ stored = row[6] or ""
200
+ return _hmac.compare_digest(
201
+ recomputed.encode("utf-8"), stored.encode("utf-8")
202
+ )
203
+
204
+ def _resolve_canonical(
205
+ self,
206
+ conn: sqlite3.Connection,
207
+ operation_id: str,
208
+ canonical_committed: bool | None,
209
+ ) -> bool:
210
+ if canonical_committed is not None:
211
+ return canonical_committed
212
+ table = conn.execute(
213
+ "SELECT 1 FROM sqlite_master WHERE type='table' "
214
+ "AND name='ingestion_operations'"
215
+ ).fetchone()
216
+ if table is None:
217
+ return True
218
+ row = conn.execute(
219
+ "SELECT state FROM ingestion_operations WHERE operation_id = ? LIMIT 1",
220
+ (operation_id,),
221
+ ).fetchone()
222
+ if row is None:
223
+ return False
224
+ return row[0] in ("queryable", "enriching", "complete")
225
+
226
+
227
+ def _manifest_version_supported(conn: sqlite3.Connection) -> int:
228
+ """Return the highest manifest version this DB supports.
229
+
230
+ Returns MANIFEST_V2 if the ``manifest_version`` column exists (M037 applied),
231
+ otherwise MANIFEST_V1.
232
+ """
233
+ try:
234
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(completion_manifests)").fetchall()}
235
+ return MANIFEST_V2 if "manifest_version" in cols else MANIFEST_V1
236
+ except Exception: # noqa: BLE001
237
+ # Fail-closed: PRAGMA failure must not silently route verification through
238
+ # the unkeyed-SHA v1 path — that allows a downgrade-forgery attack where
239
+ # an attacker resets manifest_version=1 and recomputes a valid v1 SHA.
240
+ return MANIFEST_V2
241
+
242
+
243
+ def _manifest_row_version(conn: sqlite3.Connection, operation_id: str) -> int:
244
+ """Read the manifest_version column for an existing row.
245
+
246
+ Falls back to MANIFEST_V1 when the column does not exist.
247
+ """
248
+ try:
249
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(completion_manifests)").fetchall()}
250
+ if "manifest_version" not in cols:
251
+ return MANIFEST_V1
252
+ row = conn.execute(
253
+ "SELECT manifest_version FROM completion_manifests WHERE operation_id = ?",
254
+ (operation_id,),
255
+ ).fetchone()
256
+ return int(row[0]) if row else MANIFEST_V1
257
+ except Exception: # noqa: BLE001
258
+ return MANIFEST_V1
259
+
260
+
261
+ def _evidence_from_json(payload: str) -> tuple[OwnerEvidence, ...]:
262
+ try:
263
+ rows = json.loads(payload)
264
+ except (TypeError, json.JSONDecodeError):
265
+ return ()
266
+ if not isinstance(rows, list):
267
+ return ()
268
+ evidence: list[OwnerEvidence] = []
269
+ for entry in rows:
270
+ if not isinstance(entry, dict):
271
+ continue
272
+ try:
273
+ evidence.append(OwnerEvidence(
274
+ owner=str(entry["owner"]),
275
+ kind=ObligationKind(entry["kind"]),
276
+ state=ObligationState(entry["state"]),
277
+ revision=int(entry["revision"]),
278
+ checksum=str(entry["checksum"]),
279
+ ))
280
+ except (KeyError, ValueError):
281
+ continue
282
+ return tuple(evidence)
283
+
284
+
285
+ __all__ = ["Reconciler"]