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.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -26,6 +26,46 @@ _REQUIRED_COLS = frozenset({
|
|
|
26
26
|
"shadow_results_json", "promoted_at", "rollback_reason",
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
# Column definitions in application order: name → SQLite type fragment.
|
|
30
|
+
_COLUMN_ADDITIONS: tuple[tuple[str, str], ...] = (
|
|
31
|
+
("is_previous", "INTEGER DEFAULT 0"),
|
|
32
|
+
("is_rollback", "INTEGER DEFAULT 0"),
|
|
33
|
+
("is_candidate", "INTEGER DEFAULT 0"),
|
|
34
|
+
("shadow_results_json", "TEXT"),
|
|
35
|
+
("promoted_at", "TEXT"),
|
|
36
|
+
("rollback_reason", "TEXT"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
41
|
+
"""Idempotent, column-by-column ALTER for crash-safe recovery.
|
|
42
|
+
|
|
43
|
+
Each column is only added when absent, so any committed prefix from a
|
|
44
|
+
prior interrupted run is handled transparently. Indexes use the standard
|
|
45
|
+
IF NOT EXISTS guard. Callers must not call commit/rollback — the runner
|
|
46
|
+
owns the transaction boundary.
|
|
47
|
+
"""
|
|
48
|
+
existing_cols: set[str] = {
|
|
49
|
+
r[1]
|
|
50
|
+
for r in conn.execute(
|
|
51
|
+
"PRAGMA table_info(learning_model_state)"
|
|
52
|
+
).fetchall()
|
|
53
|
+
}
|
|
54
|
+
for col_name, col_def in _COLUMN_ADDITIONS:
|
|
55
|
+
if col_name not in existing_cols:
|
|
56
|
+
conn.execute(
|
|
57
|
+
f"ALTER TABLE learning_model_state ADD COLUMN {col_name} {col_def}"
|
|
58
|
+
)
|
|
59
|
+
# Partial-index creation: IF NOT EXISTS guards idempotency.
|
|
60
|
+
conn.execute(
|
|
61
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_model_active_one "
|
|
62
|
+
"ON learning_model_state(profile_id) WHERE is_active=1"
|
|
63
|
+
)
|
|
64
|
+
conn.execute(
|
|
65
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_model_candidate_one "
|
|
66
|
+
"ON learning_model_state(profile_id) WHERE is_candidate=1"
|
|
67
|
+
)
|
|
68
|
+
|
|
29
69
|
|
|
30
70
|
def verify(conn: sqlite3.Connection) -> bool:
|
|
31
71
|
try:
|
|
@@ -0,0 +1,148 @@
|
|
|
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 sqlite3
|
|
7
|
+
|
|
8
|
+
NAME = "M033_projection_transactions"
|
|
9
|
+
DB_TARGET = "memory"
|
|
10
|
+
|
|
11
|
+
DDL = """
|
|
12
|
+
CREATE TABLE IF NOT EXISTS projection_obligations (
|
|
13
|
+
obligation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14
|
+
operation_id TEXT NOT NULL,
|
|
15
|
+
profile_id TEXT NOT NULL,
|
|
16
|
+
owner TEXT NOT NULL,
|
|
17
|
+
kind TEXT NOT NULL CHECK (kind IN ('apply', 'erase')),
|
|
18
|
+
subject_id TEXT NOT NULL,
|
|
19
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK (
|
|
21
|
+
state IN (
|
|
22
|
+
'pending', 'applied', 'verified',
|
|
23
|
+
'failed', 'compensated', 'erased'
|
|
24
|
+
)
|
|
25
|
+
),
|
|
26
|
+
checksum TEXT,
|
|
27
|
+
detail TEXT,
|
|
28
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
created_at REAL NOT NULL,
|
|
30
|
+
updated_at REAL NOT NULL,
|
|
31
|
+
UNIQUE (operation_id, owner, kind)
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_projection_obligations_operation
|
|
34
|
+
ON projection_obligations (operation_id);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_projection_obligations_profile_state
|
|
36
|
+
ON projection_obligations (profile_id, state);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_projection_obligations_state_updated
|
|
38
|
+
ON projection_obligations (state, updated_at);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS completion_manifests (
|
|
40
|
+
operation_id TEXT PRIMARY KEY,
|
|
41
|
+
profile_id TEXT NOT NULL,
|
|
42
|
+
state TEXT NOT NULL CHECK (
|
|
43
|
+
state IN ('COMPLETE', 'DEGRADED', 'FAILED')
|
|
44
|
+
),
|
|
45
|
+
all_met INTEGER NOT NULL DEFAULT 0 CHECK (all_met IN (0, 1)),
|
|
46
|
+
obligation_count INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
owner_evidence_json TEXT NOT NULL,
|
|
48
|
+
manifest_hash TEXT NOT NULL,
|
|
49
|
+
created_at REAL NOT NULL,
|
|
50
|
+
updated_at REAL NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_completion_manifests_profile_state
|
|
53
|
+
ON completion_manifests (profile_id, state);
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
_REQUIRED_OBLIGATION_COLUMNS = frozenset({
|
|
57
|
+
"obligation_id",
|
|
58
|
+
"operation_id",
|
|
59
|
+
"profile_id",
|
|
60
|
+
"owner",
|
|
61
|
+
"kind",
|
|
62
|
+
"subject_id",
|
|
63
|
+
"revision",
|
|
64
|
+
"state",
|
|
65
|
+
"checksum",
|
|
66
|
+
"detail",
|
|
67
|
+
"attempts",
|
|
68
|
+
"created_at",
|
|
69
|
+
"updated_at",
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
_REQUIRED_MANIFEST_COLUMNS = frozenset({
|
|
73
|
+
"operation_id",
|
|
74
|
+
"profile_id",
|
|
75
|
+
"state",
|
|
76
|
+
"all_met",
|
|
77
|
+
"obligation_count",
|
|
78
|
+
"owner_evidence_json",
|
|
79
|
+
"manifest_hash",
|
|
80
|
+
"created_at",
|
|
81
|
+
"updated_at",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
_REQUIRED_OBJECTS = frozenset({
|
|
85
|
+
"projection_obligations",
|
|
86
|
+
"completion_manifests",
|
|
87
|
+
"idx_projection_obligations_operation",
|
|
88
|
+
"idx_projection_obligations_profile_state",
|
|
89
|
+
"idx_projection_obligations_state_updated",
|
|
90
|
+
"idx_completion_manifests_profile_state",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
95
|
+
if verify(conn):
|
|
96
|
+
return
|
|
97
|
+
conn.executescript(DDL)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
101
|
+
apply(conn)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
105
|
+
if not _table_exists(conn, "projection_obligations"):
|
|
106
|
+
return False
|
|
107
|
+
if not _table_exists(conn, "completion_manifests"):
|
|
108
|
+
return False
|
|
109
|
+
if not _REQUIRED_OBLIGATION_COLUMNS <= _columns(conn, "projection_obligations"):
|
|
110
|
+
return False
|
|
111
|
+
if not _REQUIRED_MANIFEST_COLUMNS <= _columns(conn, "completion_manifests"):
|
|
112
|
+
return False
|
|
113
|
+
if not _has_unique_index(conn, "projection_obligations", ("operation_id", "owner", "kind")):
|
|
114
|
+
return False
|
|
115
|
+
present = {
|
|
116
|
+
row[0]
|
|
117
|
+
for row in conn.execute(
|
|
118
|
+
"SELECT name FROM sqlite_master WHERE name IN "
|
|
119
|
+
"(?, ?, ?, ?, ?, ?)",
|
|
120
|
+
tuple(sorted(_REQUIRED_OBJECTS)),
|
|
121
|
+
).fetchall()
|
|
122
|
+
}
|
|
123
|
+
return _REQUIRED_OBJECTS <= present
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
127
|
+
return conn.execute(
|
|
128
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
|
129
|
+
(name,),
|
|
130
|
+
).fetchone() is not None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
134
|
+
return {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _has_unique_index(
|
|
138
|
+
conn: sqlite3.Connection, table: str, columns: tuple[str, ...],
|
|
139
|
+
) -> bool:
|
|
140
|
+
for index in conn.execute(f"PRAGMA index_list({table})").fetchall():
|
|
141
|
+
if not index[2]:
|
|
142
|
+
continue
|
|
143
|
+
index_columns = tuple(
|
|
144
|
+
row[2] for row in conn.execute(f"PRAGMA index_info({index[1]})").fetchall()
|
|
145
|
+
)
|
|
146
|
+
if index_columns == columns:
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
@@ -0,0 +1,58 @@
|
|
|
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 sqlite3
|
|
7
|
+
|
|
8
|
+
NAME = "M034_obligation_integrity"
|
|
9
|
+
DB_TARGET = "memory"
|
|
10
|
+
|
|
11
|
+
DDL = """
|
|
12
|
+
ALTER TABLE projection_obligations ADD COLUMN context_digest TEXT;
|
|
13
|
+
ALTER TABLE projection_obligations ADD COLUMN verify_attempts INTEGER NOT NULL DEFAULT 0;
|
|
14
|
+
ALTER TABLE projection_obligations ADD COLUMN last_verified_at REAL;
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_ADDED_COLUMNS = {
|
|
18
|
+
"context_digest": "ALTER TABLE projection_obligations ADD COLUMN context_digest TEXT",
|
|
19
|
+
"verify_attempts": (
|
|
20
|
+
"ALTER TABLE projection_obligations "
|
|
21
|
+
"ADD COLUMN verify_attempts INTEGER NOT NULL DEFAULT 0"
|
|
22
|
+
),
|
|
23
|
+
"last_verified_at": (
|
|
24
|
+
"ALTER TABLE projection_obligations ADD COLUMN last_verified_at REAL"
|
|
25
|
+
),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
30
|
+
if not _table_exists(conn):
|
|
31
|
+
return
|
|
32
|
+
existing = _columns(conn)
|
|
33
|
+
for column, statement in _ADDED_COLUMNS.items():
|
|
34
|
+
if column not in existing:
|
|
35
|
+
conn.execute(statement)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
39
|
+
apply(conn)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
43
|
+
if not _table_exists(conn):
|
|
44
|
+
return False
|
|
45
|
+
return set(_ADDED_COLUMNS) <= _columns(conn)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _table_exists(conn: sqlite3.Connection) -> bool:
|
|
49
|
+
return conn.execute(
|
|
50
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='projection_obligations'"
|
|
51
|
+
).fetchone() is not None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _columns(conn: sqlite3.Connection) -> set[str]:
|
|
55
|
+
return {
|
|
56
|
+
row[1]
|
|
57
|
+
for row in conn.execute("PRAGMA table_info(projection_obligations)").fetchall()
|
|
58
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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 sqlite3
|
|
7
|
+
|
|
8
|
+
NAME = "M035_erasure_receipts"
|
|
9
|
+
DB_TARGET = "memory"
|
|
10
|
+
|
|
11
|
+
DDL = """
|
|
12
|
+
CREATE TABLE IF NOT EXISTS erasure_receipts (
|
|
13
|
+
erasure_id TEXT PRIMARY KEY,
|
|
14
|
+
profile_id TEXT NOT NULL,
|
|
15
|
+
subject_type TEXT NOT NULL CHECK (
|
|
16
|
+
subject_type IN ('fact', 'entity', 'profile')
|
|
17
|
+
),
|
|
18
|
+
subject_id TEXT NOT NULL,
|
|
19
|
+
requested_by TEXT NOT NULL DEFAULT '',
|
|
20
|
+
fact_count INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
state TEXT NOT NULL CHECK (state IN ('COMPLETE', 'FAILED')),
|
|
22
|
+
all_erased INTEGER NOT NULL DEFAULT 0 CHECK (all_erased IN (0, 1)),
|
|
23
|
+
owner_evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
24
|
+
audit_hash TEXT NOT NULL,
|
|
25
|
+
requested_at REAL NOT NULL,
|
|
26
|
+
completed_at REAL NOT NULL
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_erasure_receipts_profile
|
|
29
|
+
ON erasure_receipts (profile_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_erasure_receipts_state
|
|
31
|
+
ON erasure_receipts (state);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS projection_tombstones (
|
|
33
|
+
profile_id TEXT NOT NULL,
|
|
34
|
+
fact_id TEXT NOT NULL,
|
|
35
|
+
erasure_id TEXT NOT NULL,
|
|
36
|
+
memory_id TEXT,
|
|
37
|
+
created_at REAL NOT NULL,
|
|
38
|
+
PRIMARY KEY (profile_id, fact_id)
|
|
39
|
+
);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_projection_tombstones_fact
|
|
41
|
+
ON projection_tombstones (fact_id);
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_REQUIRED_RECEIPT_COLUMNS = frozenset({
|
|
45
|
+
"erasure_id",
|
|
46
|
+
"profile_id",
|
|
47
|
+
"subject_type",
|
|
48
|
+
"subject_id",
|
|
49
|
+
"requested_by",
|
|
50
|
+
"fact_count",
|
|
51
|
+
"state",
|
|
52
|
+
"all_erased",
|
|
53
|
+
"owner_evidence_json",
|
|
54
|
+
"audit_hash",
|
|
55
|
+
"requested_at",
|
|
56
|
+
"completed_at",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
_REQUIRED_TOMBSTONE_COLUMNS = frozenset({
|
|
60
|
+
"profile_id",
|
|
61
|
+
"fact_id",
|
|
62
|
+
"erasure_id",
|
|
63
|
+
"memory_id",
|
|
64
|
+
"created_at",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
_REQUIRED_OBJECTS = frozenset({
|
|
68
|
+
"erasure_receipts",
|
|
69
|
+
"projection_tombstones",
|
|
70
|
+
"idx_erasure_receipts_profile",
|
|
71
|
+
"idx_erasure_receipts_state",
|
|
72
|
+
"idx_projection_tombstones_fact",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
77
|
+
if verify(conn):
|
|
78
|
+
return
|
|
79
|
+
conn.executescript(DDL)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
83
|
+
apply(conn)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
87
|
+
if not _table_exists(conn, "erasure_receipts"):
|
|
88
|
+
return False
|
|
89
|
+
if not _table_exists(conn, "projection_tombstones"):
|
|
90
|
+
return False
|
|
91
|
+
if not _REQUIRED_RECEIPT_COLUMNS <= _columns(conn, "erasure_receipts"):
|
|
92
|
+
return False
|
|
93
|
+
if not _REQUIRED_TOMBSTONE_COLUMNS <= _columns(conn, "projection_tombstones"):
|
|
94
|
+
return False
|
|
95
|
+
present = {
|
|
96
|
+
row[0]
|
|
97
|
+
for row in conn.execute(
|
|
98
|
+
"SELECT name FROM sqlite_master WHERE name IN (?, ?, ?, ?, ?)",
|
|
99
|
+
tuple(sorted(_REQUIRED_OBJECTS)),
|
|
100
|
+
).fetchall()
|
|
101
|
+
}
|
|
102
|
+
return _REQUIRED_OBJECTS <= present
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
106
|
+
return conn.execute(
|
|
107
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
|
108
|
+
(name,),
|
|
109
|
+
).fetchone() is not None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
113
|
+
return {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 sqlite3
|
|
7
|
+
|
|
8
|
+
NAME = "M036_vector_row_map"
|
|
9
|
+
DB_TARGET = "memory"
|
|
10
|
+
|
|
11
|
+
DDL = """
|
|
12
|
+
CREATE TABLE IF NOT EXISTS vector_row_map (
|
|
13
|
+
fact_id TEXT NOT NULL,
|
|
14
|
+
profile_id TEXT NOT NULL,
|
|
15
|
+
vec_rowid INTEGER NOT NULL,
|
|
16
|
+
PRIMARY KEY (fact_id)
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_vector_row_map_profile
|
|
19
|
+
ON vector_row_map (profile_id);
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_REQUIRED_COLUMNS = frozenset({"fact_id", "profile_id", "vec_rowid"})
|
|
23
|
+
|
|
24
|
+
_REQUIRED_OBJECTS = frozenset({
|
|
25
|
+
"vector_row_map",
|
|
26
|
+
"idx_vector_row_map_profile",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
31
|
+
_reconcile(conn)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
35
|
+
_reconcile(conn)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reconcile(conn: sqlite3.Connection) -> None:
|
|
39
|
+
_rebuild_if_malformed(conn)
|
|
40
|
+
conn.executescript(DDL)
|
|
41
|
+
_backfill(conn)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _rebuild_if_malformed(conn: sqlite3.Connection) -> None:
|
|
45
|
+
if not _table_exists(conn, "vector_row_map"):
|
|
46
|
+
return
|
|
47
|
+
if _REQUIRED_COLUMNS <= _columns(conn, "vector_row_map"):
|
|
48
|
+
return
|
|
49
|
+
conn.execute("DROP TABLE vector_row_map")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _backfill(conn: sqlite3.Connection) -> None:
|
|
53
|
+
if not _table_exists(conn, "embedding_metadata"):
|
|
54
|
+
return
|
|
55
|
+
if not {"fact_id", "profile_id", "vec_rowid"} <= _columns(conn, "embedding_metadata"):
|
|
56
|
+
return
|
|
57
|
+
conn.execute(
|
|
58
|
+
"INSERT INTO vector_row_map (fact_id, profile_id, vec_rowid) "
|
|
59
|
+
"SELECT em.fact_id, em.profile_id, em.vec_rowid FROM embedding_metadata em "
|
|
60
|
+
"WHERE em.fact_id IS NOT NULL AND em.vec_rowid IS NOT NULL "
|
|
61
|
+
"AND NOT EXISTS ("
|
|
62
|
+
"SELECT 1 FROM vector_row_map vrm WHERE vrm.fact_id = em.fact_id"
|
|
63
|
+
")"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
68
|
+
if not _table_exists(conn, "vector_row_map"):
|
|
69
|
+
return False
|
|
70
|
+
if not _REQUIRED_COLUMNS <= _columns(conn, "vector_row_map"):
|
|
71
|
+
return False
|
|
72
|
+
present = {
|
|
73
|
+
row[0]
|
|
74
|
+
for row in conn.execute(
|
|
75
|
+
"SELECT name FROM sqlite_master WHERE name IN (?, ?)",
|
|
76
|
+
tuple(sorted(_REQUIRED_OBJECTS)),
|
|
77
|
+
).fetchall()
|
|
78
|
+
}
|
|
79
|
+
if not _REQUIRED_OBJECTS <= present:
|
|
80
|
+
return False
|
|
81
|
+
return _backfill_complete(conn)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _backfill_complete(conn: sqlite3.Connection) -> bool:
|
|
85
|
+
if not _table_exists(conn, "embedding_metadata"):
|
|
86
|
+
return True
|
|
87
|
+
if not {"fact_id", "vec_rowid"} <= _columns(conn, "embedding_metadata"):
|
|
88
|
+
return True
|
|
89
|
+
uncovered = conn.execute(
|
|
90
|
+
"SELECT 1 FROM embedding_metadata em "
|
|
91
|
+
"WHERE em.fact_id IS NOT NULL AND em.vec_rowid IS NOT NULL "
|
|
92
|
+
"AND NOT EXISTS ("
|
|
93
|
+
"SELECT 1 FROM vector_row_map vrm WHERE vrm.fact_id = em.fact_id"
|
|
94
|
+
") LIMIT 1"
|
|
95
|
+
).fetchone()
|
|
96
|
+
return uncovered is None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
100
|
+
return conn.execute(
|
|
101
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
|
102
|
+
(name,),
|
|
103
|
+
).fetchone() is not None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
107
|
+
return {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""M037 — additive columns for HMAC manifest/receipt versioning.
|
|
5
|
+
|
|
6
|
+
Adds ``manifest_version`` to ``completion_manifests`` and
|
|
7
|
+
``receipt_version`` to ``erasure_receipts``. Default value 1 means the
|
|
8
|
+
existing rows were written with the unkeyed SHA-256 scheme (v1). New rows
|
|
9
|
+
are written with version 2 (HMAC-SHA256). The verify path reads the
|
|
10
|
+
version column and selects the appropriate verification algorithm.
|
|
11
|
+
|
|
12
|
+
Dependency: M033 (completion_manifests), M035 (erasure_receipts).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sqlite3
|
|
19
|
+
|
|
20
|
+
NAME = "M037_manifest_hmac_version"
|
|
21
|
+
DB_TARGET = "memory"
|
|
22
|
+
|
|
23
|
+
_ADDED_COLUMNS: dict[str, tuple[str, str]] = {
|
|
24
|
+
"manifest_version_in_completion_manifests": (
|
|
25
|
+
"completion_manifests",
|
|
26
|
+
"manifest_version INTEGER NOT NULL DEFAULT 1",
|
|
27
|
+
),
|
|
28
|
+
"receipt_version_in_erasure_receipts": (
|
|
29
|
+
"erasure_receipts",
|
|
30
|
+
"receipt_version INTEGER NOT NULL DEFAULT 1",
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
DDL = "\n".join(
|
|
35
|
+
f"ALTER TABLE {tbl} ADD COLUMN {col_def};"
|
|
36
|
+
for _, (tbl, col_def) in _ADDED_COLUMNS.items()
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
41
|
+
for _key, (table, col_def) in _ADDED_COLUMNS.items():
|
|
42
|
+
if not _table_exists(conn, table):
|
|
43
|
+
continue
|
|
44
|
+
col_name = col_def.split()[0]
|
|
45
|
+
if col_name not in _columns(conn, table):
|
|
46
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col_def}")
|
|
47
|
+
|
|
48
|
+
# Re-seal pre-existing v1 rows to v2 HMAC so verify_manifest / verify_receipt
|
|
49
|
+
# continue to PASS post-migration. Without re-seal, the verify path rejects
|
|
50
|
+
# v1 rows on v2 DBs as potential downgrade attacks, silently breaking every
|
|
51
|
+
# evidence chain written before M037 was applied.
|
|
52
|
+
_reseal_manifests(conn)
|
|
53
|
+
_reseal_receipts(conn)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _reseal_manifests(conn: sqlite3.Connection) -> None:
|
|
57
|
+
"""Re-seal completion_manifests rows from v1 (unkeyed SHA) to v2 (HMAC)."""
|
|
58
|
+
if not _table_exists(conn, "completion_manifests"):
|
|
59
|
+
return
|
|
60
|
+
if "manifest_version" not in _columns(conn, "completion_manifests"):
|
|
61
|
+
return # column not yet present — nothing to re-seal
|
|
62
|
+
|
|
63
|
+
from superlocalmemory.core.transactions.manifest import (
|
|
64
|
+
MANIFEST_V2,
|
|
65
|
+
_canonical_envelope,
|
|
66
|
+
)
|
|
67
|
+
from superlocalmemory.core.transactions.manifest_key import (
|
|
68
|
+
compute_hmac,
|
|
69
|
+
derive_manifest_hmac_key,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
key = derive_manifest_hmac_key()
|
|
73
|
+
rows = conn.execute(
|
|
74
|
+
"SELECT operation_id, profile_id, state, all_met, obligation_count, "
|
|
75
|
+
"owner_evidence_json FROM completion_manifests WHERE manifest_version = 1"
|
|
76
|
+
).fetchall()
|
|
77
|
+
for row in rows:
|
|
78
|
+
evidence_dicts = json.loads(row[5] or "[]")
|
|
79
|
+
canonical = _canonical_envelope(
|
|
80
|
+
operation_id=row[0],
|
|
81
|
+
profile_id=row[1],
|
|
82
|
+
state=row[2],
|
|
83
|
+
all_met=bool(row[3]),
|
|
84
|
+
obligation_count=int(row[4]),
|
|
85
|
+
evidence_dicts=evidence_dicts,
|
|
86
|
+
manifest_version=MANIFEST_V2,
|
|
87
|
+
)
|
|
88
|
+
new_hash = compute_hmac(key, canonical)
|
|
89
|
+
conn.execute(
|
|
90
|
+
"UPDATE completion_manifests SET manifest_hash = ?, manifest_version = ? "
|
|
91
|
+
"WHERE operation_id = ?",
|
|
92
|
+
(new_hash, MANIFEST_V2, row[0]),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _reseal_receipts(conn: sqlite3.Connection) -> None:
|
|
97
|
+
"""Re-seal erasure_receipts rows from v1 (unkeyed SHA) to v2 (HMAC)."""
|
|
98
|
+
if not _table_exists(conn, "erasure_receipts"):
|
|
99
|
+
return
|
|
100
|
+
if "receipt_version" not in _columns(conn, "erasure_receipts"):
|
|
101
|
+
return # column not yet present — nothing to re-seal
|
|
102
|
+
|
|
103
|
+
from superlocalmemory.core.transactions.erasure import (
|
|
104
|
+
_RECEIPT_V2,
|
|
105
|
+
_erasure_canonical,
|
|
106
|
+
)
|
|
107
|
+
from superlocalmemory.core.transactions.manifest_key import (
|
|
108
|
+
compute_hmac,
|
|
109
|
+
derive_receipt_hmac_key,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
key = derive_receipt_hmac_key()
|
|
113
|
+
rows = conn.execute(
|
|
114
|
+
"SELECT erasure_id, profile_id, subject_type, subject_id, requested_by, "
|
|
115
|
+
"fact_count, state, all_erased, owner_evidence_json, requested_at, completed_at "
|
|
116
|
+
"FROM erasure_receipts WHERE receipt_version = 1"
|
|
117
|
+
).fetchall()
|
|
118
|
+
for row in rows:
|
|
119
|
+
canonical = _erasure_canonical(
|
|
120
|
+
erasure_id=row[0],
|
|
121
|
+
profile_id=row[1],
|
|
122
|
+
subject_type=row[2],
|
|
123
|
+
subject_id=row[3],
|
|
124
|
+
requested_by=row[4],
|
|
125
|
+
fact_count=int(row[5]),
|
|
126
|
+
state=row[6],
|
|
127
|
+
all_erased=bool(row[7]),
|
|
128
|
+
evidence_json=row[8],
|
|
129
|
+
requested_at=float(row[9]),
|
|
130
|
+
completed_at=float(row[10]),
|
|
131
|
+
receipt_version=_RECEIPT_V2,
|
|
132
|
+
)
|
|
133
|
+
new_hash = compute_hmac(key, canonical)
|
|
134
|
+
conn.execute(
|
|
135
|
+
"UPDATE erasure_receipts SET audit_hash = ?, receipt_version = ? "
|
|
136
|
+
"WHERE erasure_id = ?",
|
|
137
|
+
(new_hash, _RECEIPT_V2, row[0]),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
142
|
+
apply(conn)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
146
|
+
for _key, (table, col_def) in _ADDED_COLUMNS.items():
|
|
147
|
+
if not _table_exists(conn, table):
|
|
148
|
+
return False
|
|
149
|
+
col_name = col_def.split()[0]
|
|
150
|
+
if col_name not in _columns(conn, table):
|
|
151
|
+
return False
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
156
|
+
return conn.execute(
|
|
157
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,),
|
|
158
|
+
).fetchone() is not None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
162
|
+
return {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
#
|
|
3
|
+
# Ported into the SuperLocalMemory V4 migration sequence.
|
|
4
4
|
|
|
5
|
-
"""
|
|
5
|
+
"""M038 — add the ``channel`` column to ``learning_feedback``.
|
|
6
6
|
|
|
7
7
|
``pattern_miner._mine_channel_and_coretrieval`` has always executed::
|
|
8
8
|
|
|
@@ -35,7 +35,7 @@ from __future__ import annotations
|
|
|
35
35
|
|
|
36
36
|
import sqlite3
|
|
37
37
|
|
|
38
|
-
NAME = "
|
|
38
|
+
NAME = "M038_learning_feedback_channel"
|
|
39
39
|
DB_TARGET = "learning"
|
|
40
40
|
|
|
41
41
|
|