@jungjaehoon/mama-core 1.4.2 → 1.6.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 (181) hide show
  1. package/README.md +48 -10
  2. package/db/migrations/026-create-canonical-entity-tables.sql +99 -0
  3. package/db/migrations/027-create-entity-review-support.sql +44 -0
  4. package/db/migrations/028-create-entity-audit-tables.sql +45 -0
  5. package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
  6. package/db/migrations/030-case-first-memory-substrate.sql +901 -0
  7. package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
  8. package/db/migrations/032-add-memory-provenance-columns.sql +21 -0
  9. package/db/migrations/033-create-model-runs-and-tool-traces.sql +53 -0
  10. package/db/migrations/034-add-connector-event-scope-columns.sql +14 -0
  11. package/db/migrations/035-create-twin-edges.sql +63 -0
  12. package/db/migrations/036-create-agent-situation-packets.sql +61 -0
  13. package/dist/agent-graph/alias-write.d.ts +3 -0
  14. package/dist/agent-graph/alias-write.js +231 -0
  15. package/dist/agent-graph/entity-resolve.d.ts +3 -0
  16. package/dist/agent-graph/entity-resolve.js +275 -0
  17. package/dist/agent-graph/errors.d.ts +4 -0
  18. package/dist/agent-graph/errors.js +11 -0
  19. package/dist/agent-graph/graph-query.d.ts +5 -0
  20. package/dist/agent-graph/graph-query.js +359 -0
  21. package/dist/agent-graph/index.d.ts +6 -0
  22. package/dist/agent-graph/index.js +22 -0
  23. package/dist/agent-graph/types.d.ts +162 -0
  24. package/dist/agent-graph/types.js +3 -0
  25. package/dist/agent-situation/builder.d.ts +8 -0
  26. package/dist/agent-situation/builder.js +239 -0
  27. package/dist/agent-situation/cache-key.d.ts +4 -0
  28. package/dist/agent-situation/cache-key.js +64 -0
  29. package/dist/agent-situation/index.d.ts +7 -0
  30. package/dist/agent-situation/index.js +28 -0
  31. package/dist/agent-situation/packet-store.d.ts +27 -0
  32. package/dist/agent-situation/packet-store.js +228 -0
  33. package/dist/agent-situation/ranking-policy.d.ts +18 -0
  34. package/dist/agent-situation/ranking-policy.js +75 -0
  35. package/dist/agent-situation/source-readers.d.ts +52 -0
  36. package/dist/agent-situation/source-readers.js +300 -0
  37. package/dist/agent-situation/types.d.ts +195 -0
  38. package/dist/agent-situation/types.js +3 -0
  39. package/dist/canonicalize.d.ts +38 -0
  40. package/dist/canonicalize.js +106 -0
  41. package/dist/cases/case-links.d.ts +116 -0
  42. package/dist/cases/case-links.js +794 -0
  43. package/dist/cases/composition-overrides.d.ts +90 -0
  44. package/dist/cases/composition-overrides.js +724 -0
  45. package/dist/cases/corrections.d.ts +156 -0
  46. package/dist/cases/corrections.js +1045 -0
  47. package/dist/cases/freshness.d.ts +57 -0
  48. package/dist/cases/freshness.js +283 -0
  49. package/dist/cases/live-state.d.ts +48 -0
  50. package/dist/cases/live-state.js +256 -0
  51. package/dist/cases/membership-explain.d.ts +56 -0
  52. package/dist/cases/membership-explain.js +140 -0
  53. package/dist/cases/membership-matcher.d.ts +23 -0
  54. package/dist/cases/membership-matcher.js +193 -0
  55. package/dist/cases/merge-split.d.ts +61 -0
  56. package/dist/cases/merge-split.js +368 -0
  57. package/dist/cases/role-inference.d.ts +10 -0
  58. package/dist/cases/role-inference.js +85 -0
  59. package/dist/cases/search-rollup.d.ts +27 -0
  60. package/dist/cases/search-rollup.js +309 -0
  61. package/dist/cases/sqlite-transaction.d.ts +10 -0
  62. package/dist/cases/sqlite-transaction.js +62 -0
  63. package/dist/cases/store.d.ts +74 -0
  64. package/dist/cases/store.js +816 -0
  65. package/dist/cases/target-ref.d.ts +25 -0
  66. package/dist/cases/target-ref.js +22 -0
  67. package/dist/cases/timeline-range.d.ts +6 -0
  68. package/dist/cases/timeline-range.js +322 -0
  69. package/dist/cases/tombstone-sweeper.d.ts +13 -0
  70. package/dist/cases/tombstone-sweeper.js +128 -0
  71. package/dist/cases/types.d.ts +244 -0
  72. package/dist/cases/types.js +47 -0
  73. package/dist/cases/wiki-page-index.d.ts +50 -0
  74. package/dist/cases/wiki-page-index.js +350 -0
  75. package/dist/connectors/event-index.d.ts +45 -0
  76. package/dist/connectors/event-index.js +381 -0
  77. package/dist/connectors/raw-query.d.ts +18 -0
  78. package/dist/connectors/raw-query.js +305 -0
  79. package/dist/connectors/types.d.ts +109 -0
  80. package/dist/connectors/types.js +3 -0
  81. package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -0
  82. package/dist/db-adapter/node-sqlite-adapter.js +131 -0
  83. package/dist/db-manager.d.ts +12 -1
  84. package/dist/db-manager.js +60 -5
  85. package/dist/decision-formatter.d.ts +2 -0
  86. package/dist/decision-formatter.js +11 -8
  87. package/dist/edges/ref-validation.d.ts +8 -0
  88. package/dist/edges/ref-validation.js +262 -0
  89. package/dist/edges/store.d.ts +10 -0
  90. package/dist/edges/store.js +343 -0
  91. package/dist/edges/types.d.ts +86 -0
  92. package/dist/edges/types.js +18 -0
  93. package/dist/embedding-server/mobile/daemon.d.ts +1 -0
  94. package/dist/embedding-server/mobile/daemon.js +11 -1
  95. package/dist/embeddings.js +13 -0
  96. package/dist/entities/audit-metrics.d.ts +73 -0
  97. package/dist/entities/audit-metrics.js +128 -0
  98. package/dist/entities/candidate-generator.d.ts +9 -0
  99. package/dist/entities/candidate-generator.js +436 -0
  100. package/dist/entities/entity-impact.d.ts +41 -0
  101. package/dist/entities/entity-impact.js +140 -0
  102. package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
  103. package/dist/entities/entity-linked-decision-counts.js +29 -0
  104. package/dist/entities/entity-list.d.ts +23 -0
  105. package/dist/entities/entity-list.js +99 -0
  106. package/dist/entities/entity-orphan-list.d.ts +26 -0
  107. package/dist/entities/entity-orphan-list.js +91 -0
  108. package/dist/entities/entity-search.d.ts +21 -0
  109. package/dist/entities/entity-search.js +150 -0
  110. package/dist/entities/errors.d.ts +65 -0
  111. package/dist/entities/errors.js +124 -0
  112. package/dist/entities/exact-merge-backfill.d.ts +11 -0
  113. package/dist/entities/exact-merge-backfill.js +161 -0
  114. package/dist/entities/lineage-backfill.d.ts +12 -0
  115. package/dist/entities/lineage-backfill.js +101 -0
  116. package/dist/entities/lineage-store.d.ts +51 -0
  117. package/dist/entities/lineage-store.js +308 -0
  118. package/dist/entities/normalization.d.ts +14 -0
  119. package/dist/entities/normalization.js +63 -0
  120. package/dist/entities/policy-store.d.ts +17 -0
  121. package/dist/entities/policy-store.js +263 -0
  122. package/dist/entities/policy-types.d.ts +72 -0
  123. package/dist/entities/policy-types.js +59 -0
  124. package/dist/entities/projection.d.ts +8 -0
  125. package/dist/entities/projection.js +82 -0
  126. package/dist/entities/provenance-query.d.ts +33 -0
  127. package/dist/entities/provenance-query.js +167 -0
  128. package/dist/entities/read-identity.d.ts +31 -0
  129. package/dist/entities/read-identity.js +264 -0
  130. package/dist/entities/recall-bridge.d.ts +5 -0
  131. package/dist/entities/recall-bridge.js +171 -0
  132. package/dist/entities/resolution-engine.d.ts +15 -0
  133. package/dist/entities/resolution-engine.js +69 -0
  134. package/dist/entities/rollback-preview.d.ts +39 -0
  135. package/dist/entities/rollback-preview.js +235 -0
  136. package/dist/entities/source-locator.d.ts +3 -0
  137. package/dist/entities/source-locator.js +32 -0
  138. package/dist/entities/store.d.ts +75 -0
  139. package/dist/entities/store.js +465 -0
  140. package/dist/entities/types.d.ts +162 -0
  141. package/dist/entities/types.js +30 -0
  142. package/dist/index.d.ts +62 -3
  143. package/dist/index.js +116 -26
  144. package/dist/mama-api.d.ts +53 -35
  145. package/dist/mama-api.js +437 -78
  146. package/dist/memory/api.d.ts +26 -38
  147. package/dist/memory/api.js +772 -68
  148. package/dist/memory/event-store.d.ts +3 -0
  149. package/dist/memory/event-store.js +21 -2
  150. package/dist/memory/provenance-audit.d.ts +22 -0
  151. package/dist/memory/provenance-audit.js +112 -0
  152. package/dist/memory/provenance-query.d.ts +11 -0
  153. package/dist/memory/provenance-query.js +193 -0
  154. package/dist/memory/provenance.d.ts +29 -0
  155. package/dist/memory/provenance.js +136 -0
  156. package/dist/memory/scope-backfill.d.ts +17 -0
  157. package/dist/memory/scope-backfill.js +77 -0
  158. package/dist/memory/types.d.ts +97 -16
  159. package/dist/model-runs/store.d.ts +13 -0
  160. package/dist/model-runs/store.js +457 -0
  161. package/dist/model-runs/tool-trace-store.d.ts +4 -0
  162. package/dist/model-runs/tool-trace-store.js +114 -0
  163. package/dist/model-runs/types.d.ts +69 -0
  164. package/dist/model-runs/types.js +5 -0
  165. package/dist/runtime/trusted-provenance.d.ts +3 -0
  166. package/dist/runtime/trusted-provenance.js +6 -0
  167. package/dist/search/feedback-store.d.ts +59 -0
  168. package/dist/search/feedback-store.js +211 -0
  169. package/dist/search/question-type.d.ts +5 -0
  170. package/dist/search/question-type.js +41 -0
  171. package/dist/search/ranker-features.d.ts +23 -0
  172. package/dist/search/ranker-features.js +104 -0
  173. package/dist/search/ranker-rescore.d.ts +31 -0
  174. package/dist/search/ranker-rescore.js +226 -0
  175. package/dist/search/ranker-trainer.d.ts +94 -0
  176. package/dist/search/ranker-trainer.js +860 -0
  177. package/dist/search/search-quality.d.ts +36 -0
  178. package/dist/search/search-quality.js +29 -0
  179. package/dist/test-utils.d.ts +14 -0
  180. package/dist/test-utils.js +52 -0
  181. package/package.json +28 -3
@@ -0,0 +1,901 @@
1
+ -- Migration 030: Case-First Memory Substrate (Phase 1 + 2 + 3)
2
+ -- Consolidated from pre-ship migrations 030..049.
3
+ -- Covers: entity substrate (decision_entity_sources, entity_lineage,
4
+ -- entity_policy), event_datetime column + backfill + created_at fill,
5
+ -- entity observation source-locator refactor, case_truth +
6
+ -- case_memberships + case_corrections + case_proposal_queue,
7
+ -- wiki page search index, entity_timeline_events.role,
8
+ -- connector_event_index, learned ranker + search feedback,
9
+ -- case_links + tombstones, case promotion/freshness columns,
10
+ -- case membership explanation columns.
11
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md
12
+
13
+ -- ================================================================
14
+ -- Entity substrate — decision_entity_sources
15
+ -- ================================================================
16
+ CREATE TABLE IF NOT EXISTS decision_entity_sources (
17
+ decision_id TEXT NOT NULL,
18
+ entity_observation_id TEXT NOT NULL,
19
+ relation_type TEXT NOT NULL DEFAULT 'support',
20
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
21
+ FOREIGN KEY (decision_id) REFERENCES decisions(id) ON DELETE CASCADE,
22
+ FOREIGN KEY (entity_observation_id) REFERENCES entity_observations(id) ON DELETE CASCADE
23
+ );
24
+
25
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_decision_entity_sources_unique
26
+ ON decision_entity_sources(decision_id, entity_observation_id, relation_type);
27
+
28
+ CREATE INDEX IF NOT EXISTS idx_decision_entity_sources_decision
29
+ ON decision_entity_sources(decision_id);
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_decision_entity_sources_observation
32
+ ON decision_entity_sources(entity_observation_id);
33
+
34
+
35
+ -- ================================================================
36
+ -- Entity substrate — lineage tables
37
+ -- ================================================================
38
+ CREATE TABLE IF NOT EXISTS entity_ingest_runs (
39
+ id TEXT PRIMARY KEY,
40
+ connector TEXT NOT NULL,
41
+ run_kind TEXT NOT NULL CHECK (run_kind IN ('live', 'replay', 'backfill')),
42
+ status TEXT NOT NULL CHECK (status IN ('running', 'complete', 'failed')),
43
+ scope_key TEXT NOT NULL,
44
+ source_window_start INTEGER,
45
+ source_window_end INTEGER,
46
+ raw_count INTEGER NOT NULL DEFAULT 0,
47
+ observation_count INTEGER NOT NULL DEFAULT 0,
48
+ candidate_count INTEGER NOT NULL DEFAULT 0,
49
+ reviewable_count INTEGER NOT NULL DEFAULT 0,
50
+ audit_run_id TEXT,
51
+ audit_classification TEXT CHECK (audit_classification IN ('improved', 'stable', 'regressed', 'inconclusive')),
52
+ error_reason TEXT,
53
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
54
+ completed_at INTEGER,
55
+ FOREIGN KEY (audit_run_id) REFERENCES entity_audit_runs(id) ON DELETE SET NULL
56
+ );
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_entity_ingest_runs_connector_created_at
59
+ ON entity_ingest_runs(connector, created_at DESC);
60
+ CREATE INDEX IF NOT EXISTS idx_entity_ingest_runs_status_created_at
61
+ ON entity_ingest_runs(status, created_at DESC);
62
+
63
+ CREATE TABLE IF NOT EXISTS entity_lineage_links (
64
+ id TEXT PRIMARY KEY,
65
+ canonical_entity_id TEXT NOT NULL,
66
+ entity_observation_id TEXT NOT NULL,
67
+ source_entity_id TEXT,
68
+ contribution_kind TEXT NOT NULL CHECK (
69
+ contribution_kind IN ('seed', 'merge_adopt', 'manual_attach', 'rollback_restore')
70
+ ),
71
+ run_id TEXT,
72
+ candidate_id TEXT,
73
+ review_action_id TEXT,
74
+ status TEXT NOT NULL CHECK (status IN ('active', 'superseded', 'rolled_back')),
75
+ capture_mode TEXT NOT NULL CHECK (capture_mode IN ('direct', 'backfilled')),
76
+ confidence REAL NOT NULL DEFAULT 1,
77
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
78
+ superseded_at INTEGER,
79
+ FOREIGN KEY (canonical_entity_id) REFERENCES entity_nodes(id) ON DELETE CASCADE,
80
+ FOREIGN KEY (entity_observation_id) REFERENCES entity_observations(id) ON DELETE CASCADE,
81
+ FOREIGN KEY (source_entity_id) REFERENCES entity_nodes(id) ON DELETE SET NULL,
82
+ FOREIGN KEY (run_id) REFERENCES entity_ingest_runs(id) ON DELETE SET NULL,
83
+ FOREIGN KEY (candidate_id) REFERENCES entity_resolution_candidates(id) ON DELETE SET NULL,
84
+ FOREIGN KEY (review_action_id) REFERENCES entity_merge_actions(id) ON DELETE SET NULL,
85
+ CHECK (confidence >= 0 AND confidence <= 1)
86
+ );
87
+
88
+ CREATE INDEX IF NOT EXISTS idx_entity_lineage_links_entity_status_created_at
89
+ ON entity_lineage_links(canonical_entity_id, status, created_at DESC);
90
+ CREATE INDEX IF NOT EXISTS idx_entity_lineage_links_observation
91
+ ON entity_lineage_links(entity_observation_id);
92
+ CREATE INDEX IF NOT EXISTS idx_entity_lineage_links_run
93
+ ON entity_lineage_links(run_id);
94
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_entity_lineage_active_pair
95
+ ON entity_lineage_links(canonical_entity_id, entity_observation_id)
96
+ WHERE status = 'active';
97
+
98
+
99
+ -- ================================================================
100
+ -- Entity substrate — policy tables
101
+ -- ================================================================
102
+ CREATE TABLE IF NOT EXISTS entity_policy (
103
+ policy_key TEXT PRIMARY KEY,
104
+ policy_kind TEXT NOT NULL,
105
+ value_json TEXT NOT NULL,
106
+ version INTEGER NOT NULL,
107
+ created_at INTEGER NOT NULL,
108
+ updated_at INTEGER NOT NULL
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS entity_role_bindings (
112
+ actor_id TEXT PRIMARY KEY,
113
+ role TEXT NOT NULL,
114
+ created_at INTEGER NOT NULL,
115
+ updated_at INTEGER NOT NULL
116
+ );
117
+
118
+ CREATE TABLE IF NOT EXISTS entity_policy_proposals (
119
+ proposal_id TEXT PRIMARY KEY,
120
+ policy_key TEXT NOT NULL,
121
+ policy_kind TEXT NOT NULL,
122
+ proposed_value_json TEXT NOT NULL,
123
+ proposer_actor TEXT NOT NULL,
124
+ approver_actor TEXT,
125
+ reason TEXT NOT NULL,
126
+ status TEXT NOT NULL,
127
+ created_at INTEGER NOT NULL,
128
+ approved_at INTEGER
129
+ );
130
+
131
+ CREATE INDEX IF NOT EXISTS idx_entity_policy_kind
132
+ ON entity_policy(policy_kind, updated_at DESC);
133
+
134
+ CREATE INDEX IF NOT EXISTS idx_entity_policy_proposals_status_created
135
+ ON entity_policy_proposals(status, created_at DESC);
136
+
137
+ CREATE INDEX IF NOT EXISTS idx_entity_policy_proposals_policy_key
138
+ ON entity_policy_proposals(policy_key, created_at DESC);
139
+
140
+ -- ================================================================
141
+ -- Temporal — add event_datetime column
142
+ -- ================================================================
143
+ -- Migration 033: Add event_datetime column to decisions table
144
+ -- Stores source event timestamp in milliseconds when known.
145
+ ALTER TABLE decisions ADD COLUMN event_datetime INTEGER;
146
+
147
+ CREATE INDEX IF NOT EXISTS idx_decisions_event_datetime ON decisions(event_datetime);
148
+
149
+
150
+ -- ================================================================
151
+ -- Temporal — backfill from linked observations
152
+ -- ================================================================
153
+ -- Migration 034: Backfill event_datetime for existing decisions
154
+ -- Priority:
155
+ -- 1) Latest linked observation timestamp when provenance exists
156
+ -- 2) event_date at 00:00:00 UTC when only date is known
157
+
158
+ UPDATE decisions
159
+ SET event_datetime = (
160
+ SELECT MAX(COALESCE(o.timestamp_observed, o.created_at))
161
+ FROM decision_entity_sources des
162
+ JOIN entity_observations o ON o.id = des.entity_observation_id
163
+ WHERE des.decision_id = decisions.id
164
+ )
165
+ WHERE event_datetime IS NULL
166
+ AND EXISTS (
167
+ SELECT 1
168
+ FROM decision_entity_sources des
169
+ JOIN entity_observations o ON o.id = des.entity_observation_id
170
+ WHERE des.decision_id = decisions.id
171
+ );
172
+
173
+ UPDATE decisions
174
+ SET event_datetime = CAST(strftime('%s', event_date || 'T00:00:00Z') AS INTEGER) * 1000
175
+ WHERE event_datetime IS NULL
176
+ AND event_date IS NOT NULL;
177
+
178
+ CREATE INDEX IF NOT EXISTS idx_decisions_event_datetime_backfill ON decisions(event_datetime);
179
+
180
+
181
+ -- ================================================================
182
+ -- Temporal — final fill from created_at
183
+ -- ================================================================
184
+ -- Migration 035: Fill any remaining event_datetime gaps from created_at
185
+ -- This is the final fallback for decisions that have neither provenance-linked
186
+ -- observation timestamps nor an explicit event_date.
187
+
188
+ UPDATE decisions
189
+ SET event_datetime = created_at
190
+ WHERE event_datetime IS NULL;
191
+
192
+
193
+ -- ================================================================
194
+ -- Source locator — add column + seed from raw_db_ref
195
+ -- ================================================================
196
+ ALTER TABLE entity_observations ADD COLUMN source_locator TEXT;
197
+
198
+ UPDATE entity_observations
199
+ SET source_locator = source_raw_db_ref
200
+ WHERE source_locator IS NULL;
201
+
202
+
203
+ -- ================================================================
204
+ -- Source locator — switch identity + triggers
205
+ -- ================================================================
206
+ UPDATE entity_observations
207
+ SET source_locator = COALESCE(source_locator, source_raw_db_ref, '')
208
+ WHERE source_locator IS NULL OR source_locator = '';
209
+
210
+ DROP INDEX IF EXISTS idx_entity_observations_source_record;
211
+
212
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_observations_source_record
213
+ ON entity_observations(
214
+ source_connector,
215
+ source_locator,
216
+ source_raw_record_id,
217
+ observation_type
218
+ );
219
+
220
+ CREATE INDEX IF NOT EXISTS idx_entity_observations_source_locator_record
221
+ ON entity_observations(source_connector, source_locator, source_raw_record_id);
222
+
223
+ CREATE TRIGGER IF NOT EXISTS trg_entity_observations_backfill_source_locator_insert
224
+ AFTER INSERT ON entity_observations
225
+ WHEN (NEW.source_locator IS NULL OR NEW.source_locator = '')
226
+ AND COALESCE(NEW.source_raw_db_ref, '') <> ''
227
+ BEGIN
228
+ UPDATE entity_observations
229
+ SET source_locator = NEW.source_raw_db_ref
230
+ WHERE id = NEW.id;
231
+ END;
232
+
233
+ CREATE TRIGGER IF NOT EXISTS trg_entity_observations_backfill_source_locator_update
234
+ AFTER UPDATE ON entity_observations
235
+ WHEN (NEW.source_locator IS NULL OR NEW.source_locator = '')
236
+ AND COALESCE(NEW.source_raw_db_ref, '') <> ''
237
+ BEGIN
238
+ UPDATE entity_observations
239
+ SET source_locator = NEW.source_raw_db_ref
240
+ WHERE id = NEW.id;
241
+ END;
242
+
243
+
244
+ -- ================================================================
245
+ -- Source locator — drop legacy raw_db_ref
246
+ -- ================================================================
247
+ UPDATE entity_observations
248
+ SET source_locator = COALESCE(source_locator, source_raw_db_ref, '')
249
+ WHERE source_locator IS NULL OR source_locator = '';
250
+
251
+ DROP TRIGGER IF EXISTS trg_entity_observations_backfill_source_locator_insert;
252
+ DROP TRIGGER IF EXISTS trg_entity_observations_backfill_source_locator_update;
253
+
254
+ ALTER TABLE entity_observations DROP COLUMN source_raw_db_ref;
255
+
256
+
257
+ -- ================================================================
258
+ -- Phase 1 — case_truth
259
+ -- ================================================================
260
+ -- Migration 039: Create case_truth table
261
+ -- Phase 1 of MAMA Case-First Memory System
262
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md §5.2
263
+
264
+ CREATE TABLE IF NOT EXISTS case_truth (
265
+ case_id TEXT PRIMARY KEY, -- UUID, also persisted in wiki frontmatter
266
+ current_wiki_path TEXT, -- nullable (merged cases retain no current page)
267
+ title TEXT NOT NULL,
268
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','blocked','resolved','stale','archived','merged','split')),
269
+ status_reason TEXT,
270
+ primary_actors TEXT, -- JSON: [{entity_id, role}]
271
+ blockers TEXT, -- JSON: [{text, source_decision_id?, source_event_id?}]
272
+ last_activity_at TEXT, -- ISO 8601 — fast-writer field
273
+ canonical_case_id TEXT REFERENCES case_truth(case_id), -- merge survivor pointer
274
+ split_from_case_id TEXT REFERENCES case_truth(case_id), -- split parent reference
275
+ wiki_path_history TEXT, -- JSON: [{path, valid_from, valid_to}]
276
+ scope_refs TEXT, -- JSON: [{kind, id}]
277
+ confidence TEXT, -- high | medium | low
278
+ compiled_at TEXT, -- wiki-compiler last write
279
+ state_updated_at TEXT, -- memory-agent last write (status / last_activity)
280
+ created_at TEXT NOT NULL,
281
+ updated_at TEXT NOT NULL
282
+ );
283
+
284
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_case_truth_current_path
285
+ ON case_truth(current_wiki_path COLLATE NOCASE)
286
+ WHERE current_wiki_path IS NOT NULL;
287
+
288
+ CREATE INDEX IF NOT EXISTS idx_case_truth_status ON case_truth(status);
289
+ CREATE INDEX IF NOT EXISTS idx_case_truth_canonical ON case_truth(canonical_case_id);
290
+ CREATE INDEX IF NOT EXISTS idx_case_truth_last_activity ON case_truth(last_activity_at DESC);
291
+
292
+
293
+ -- ================================================================
294
+ -- Phase 1 — case_memberships
295
+ -- ================================================================
296
+ -- Migration 040: Create case_memberships table
297
+ -- Phase 1 of MAMA Case-First Memory System
298
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md §5.3
299
+ --
300
+ -- Locked tombstone encoding (§0d Option B): (status='stale', user_locked=1).
301
+ -- CHECK enum stays at 5 values; no 'stale_locked'.
302
+
303
+ CREATE TABLE IF NOT EXISTS case_memberships (
304
+ case_id TEXT NOT NULL REFERENCES case_truth(case_id),
305
+ source_type TEXT NOT NULL CHECK(source_type IN ('decision','event','observation','artifact')),
306
+ source_id TEXT NOT NULL,
307
+ role TEXT, -- soft-typed: primary | supporting | contradicting | ...
308
+ confidence REAL CHECK(confidence IS NULL OR (confidence >= 0 AND confidence <= 1)),
309
+ reason TEXT,
310
+ status TEXT NOT NULL CHECK(status IN ('active','candidate','removed','excluded','stale')),
311
+ added_by TEXT NOT NULL CHECK(added_by IN ('wiki-compiler','memory-agent','user-correction')),
312
+ added_at TEXT NOT NULL,
313
+ updated_at TEXT NOT NULL,
314
+ user_locked INTEGER NOT NULL DEFAULT 0,
315
+ PRIMARY KEY (case_id, source_type, source_id)
316
+ );
317
+
318
+ CREATE INDEX IF NOT EXISTS idx_case_memberships_source
319
+ ON case_memberships(source_type, source_id, status);
320
+ CREATE INDEX IF NOT EXISTS idx_case_memberships_case_status
321
+ ON case_memberships(case_id, status);
322
+
323
+
324
+ -- ================================================================
325
+ -- Phase 1 — case_corrections
326
+ -- ================================================================
327
+ -- Migration 041: Create case_corrections table
328
+ -- Phase 1 of MAMA Case-First Memory System
329
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md §5.4
330
+ --
331
+ -- Option C (Phase 0 §0f): target_ref_json TEXT (readable canonical JSON) +
332
+ -- target_ref_hash BLOB (32-byte SHA-256 over canonical JSON).
333
+ -- Partial UNIQUE index on (case_id, target_ref_hash) WHERE active lock ensures
334
+ -- one active lock per (case, target).
335
+ --
336
+ -- Phase 1 SCAFFOLD ONLY: the table ships empty. Write flows
337
+ -- (apply/revert/supersede helpers, HITL UX, recompile lock-respect) are
338
+ -- Phase 2 per spec §14 L811 amendment.
339
+
340
+ CREATE TABLE IF NOT EXISTS case_corrections (
341
+ correction_id TEXT PRIMARY KEY, -- UUID
342
+ case_id TEXT NOT NULL REFERENCES case_truth(case_id),
343
+ target_kind TEXT NOT NULL CHECK(target_kind IN ('case_field','membership','wiki_section')),
344
+ target_ref_json TEXT NOT NULL, -- canonical JSON (human-readable)
345
+ target_ref_hash BLOB NOT NULL CHECK(length(target_ref_hash) = 32), -- SHA-256 32 bytes
346
+ field_name TEXT, -- redundant convenience for target_kind='case_field'
347
+ old_value_json TEXT, -- snapshot at correction time
348
+ new_value_json TEXT NOT NULL, -- user's intended value
349
+ reason TEXT NOT NULL,
350
+ is_lock_active INTEGER NOT NULL DEFAULT 1,
351
+ superseded_by TEXT REFERENCES case_corrections(correction_id),
352
+ reverted_at TEXT,
353
+ applied_by TEXT NOT NULL,
354
+ applied_at TEXT NOT NULL
355
+ );
356
+
357
+ CREATE INDEX IF NOT EXISTS idx_case_corrections_case_active
358
+ ON case_corrections(case_id, is_lock_active, reverted_at);
359
+
360
+ -- Partial UNIQUE: at most one active lock per (case_id, target_ref_hash).
361
+ -- Reverted or superseded rows do not block new active locks on the same target.
362
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_case_corrections_active_target
363
+ ON case_corrections(case_id, target_ref_hash)
364
+ WHERE is_lock_active = 1 AND reverted_at IS NULL;
365
+
366
+ CREATE INDEX IF NOT EXISTS idx_case_corrections_target_kind
367
+ ON case_corrections(case_id, target_kind);
368
+
369
+
370
+ -- ================================================================
371
+ -- Phase 1 — case_proposal_queue
372
+ -- ================================================================
373
+ -- Migration 042: Create case_proposal_queue table
374
+ -- Phase 1 of MAMA Case-First Memory System
375
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md §5.7
376
+ --
377
+ -- Acceptance-layer queue for LLM proposals rejected by the deterministic
378
+ -- acceptance layer (§4.2). Phase 1 SCAFFOLD + quarantine writes only; HITL
379
+ -- resolution UX is Phase 2.
380
+ --
381
+ -- payload_fingerprint is a SHA-256 (32 bytes) over a STABLE per-kind canonical
382
+ -- JSON subset — NOT over the full proposed_payload (full LLM output varies
383
+ -- across runs and would defeat dedup). Per-kind subsets live in the
384
+ -- acceptance-layer implementation; see spec §5.7 write rules.
385
+
386
+ CREATE TABLE IF NOT EXISTS case_proposal_queue (
387
+ proposal_id TEXT PRIMARY KEY, -- UUID
388
+ project TEXT NOT NULL,
389
+ proposal_kind TEXT NOT NULL CHECK(proposal_kind IN (
390
+ 'ambiguous_slug',
391
+ 'duplicate_frontmatter',
392
+ 'missing_frontmatter',
393
+ 'unknown_case_id',
394
+ 'stale_case_id',
395
+ 'merged_target',
396
+ 'archived_target',
397
+ 'lock_conflict',
398
+ 'corrupt_frontmatter',
399
+ 'quarantined_accepted_case'
400
+ )),
401
+ proposed_payload TEXT NOT NULL, -- JSON: full LLM proposal
402
+ payload_fingerprint BLOB NOT NULL CHECK(length(payload_fingerprint) = 32),
403
+ conflicting_case_id TEXT,
404
+ detected_at TEXT NOT NULL,
405
+ resolved_at TEXT,
406
+ resolution TEXT CHECK(resolution IS NULL OR resolution IN ('accepted','rejected','modified','merged')),
407
+ resolution_note TEXT
408
+ );
409
+
410
+ CREATE INDEX IF NOT EXISTS idx_case_proposal_queue_unresolved
411
+ ON case_proposal_queue(project, resolved_at);
412
+ CREATE INDEX IF NOT EXISTS idx_case_proposal_queue_kind
413
+ ON case_proposal_queue(proposal_kind, resolved_at);
414
+
415
+ -- Composite UNIQUE for idempotent re-detection. COALESCE allows dedup even
416
+ -- when conflicting_case_id is NULL (e.g. corrupt_frontmatter).
417
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_case_proposal_queue_dedup
418
+ ON case_proposal_queue(
419
+ project,
420
+ proposal_kind,
421
+ COALESCE(conflicting_case_id, ''),
422
+ payload_fingerprint
423
+ )
424
+ WHERE resolved_at IS NULL;
425
+
426
+
427
+ -- ================================================================
428
+ -- Phase 1 — wiki_page_search_index
429
+ -- ================================================================
430
+ -- Migration 043: Wiki page search index for mama_search integration
431
+ -- Phase 1 of MAMA Case-First Memory System
432
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md §7.1
433
+ --
434
+ -- Tables: wiki_page_index (metadata), wiki_page_embeddings (vectors),
435
+ -- wiki_pages_fts (FTS5 inverted index). FTS5 is REGULAR (not contentless,
436
+ -- not external-content) with `page_id UNINDEXED` for app-side hit mapping.
437
+ -- Sync triggers use standard INSERT / DELETE+INSERT / DELETE.
438
+ --
439
+ -- App-layer writes to wiki_page_index MUST NOT use INSERT OR REPLACE
440
+ -- (that toggles rowids and leaves FTS pointing at deleted rows). Use
441
+ -- INSERT ... ON CONFLICT(page_id) DO UPDATE SET ... so the UPDATE trigger
442
+ -- fires and the FTS stays consistent.
443
+
444
+ CREATE TABLE IF NOT EXISTS wiki_page_index (
445
+ page_id TEXT PRIMARY KEY,
446
+ source_type TEXT NOT NULL DEFAULT 'wiki_page' CHECK(source_type = 'wiki_page'),
447
+ source_locator TEXT NOT NULL,
448
+ case_id TEXT REFERENCES case_truth(case_id),
449
+ title TEXT NOT NULL,
450
+ -- Spec §5.6 enum (original 4 + 'case' added in Phase 1). DB-level CHECK
451
+ -- prevents malformed page_type from entering the search index and later
452
+ -- silently breaking the mama_search roll-up that keys on 'case'.
453
+ page_type TEXT NOT NULL CHECK(page_type IN ('entity','lesson','synthesis','process','case')),
454
+ content TEXT NOT NULL,
455
+ confidence TEXT,
456
+ compiled_at TEXT NOT NULL,
457
+ updated_at TEXT NOT NULL,
458
+ UNIQUE(source_type, source_locator)
459
+ );
460
+
461
+ CREATE TABLE IF NOT EXISTS wiki_page_embeddings (
462
+ page_id TEXT PRIMARY KEY REFERENCES wiki_page_index(page_id) ON DELETE CASCADE,
463
+ embedding BLOB NOT NULL
464
+ );
465
+
466
+ CREATE VIRTUAL TABLE IF NOT EXISTS wiki_pages_fts USING fts5(
467
+ page_id UNINDEXED,
468
+ title,
469
+ content,
470
+ source_locator
471
+ );
472
+
473
+ -- INSERT trigger: sync new row into FTS.
474
+ CREATE TRIGGER IF NOT EXISTS trg_wiki_page_index_ai
475
+ AFTER INSERT ON wiki_page_index
476
+ BEGIN
477
+ INSERT INTO wiki_pages_fts(page_id, title, content, source_locator)
478
+ VALUES (NEW.page_id, NEW.title, NEW.content, NEW.source_locator);
479
+ END;
480
+
481
+ -- UPDATE trigger: delete-then-insert keeps FTS tokens in sync on edits.
482
+ -- Only fires when indexed/stored FTS-adjacent columns change; page_id stays
483
+ -- unchanged via the UPSERT contract noted above.
484
+ CREATE TRIGGER IF NOT EXISTS trg_wiki_page_index_au
485
+ AFTER UPDATE OF title, content, source_locator ON wiki_page_index
486
+ BEGIN
487
+ DELETE FROM wiki_pages_fts WHERE page_id = OLD.page_id;
488
+ INSERT INTO wiki_pages_fts(page_id, title, content, source_locator)
489
+ VALUES (NEW.page_id, NEW.title, NEW.content, NEW.source_locator);
490
+ END;
491
+
492
+ -- DELETE trigger: remove all FTS rows for the page.
493
+ CREATE TRIGGER IF NOT EXISTS trg_wiki_page_index_ad
494
+ AFTER DELETE ON wiki_page_index
495
+ BEGIN
496
+ DELETE FROM wiki_pages_fts WHERE page_id = OLD.page_id;
497
+ END;
498
+
499
+
500
+ -- ================================================================
501
+ -- Phase 2 — entity_timeline_events.role
502
+ -- ================================================================
503
+ -- Migration 044: Add entity_timeline_events.role
504
+ --
505
+ -- Phase 2 Task 1 (spec §14 item 11, §4 L105-111): add a nullable `role`
506
+ -- column to `entity_timeline_events` so memory-agent role inference
507
+ -- (Phase 2 Task 3) can attach a role to each timeline event without
508
+ -- constraining downstream consumers. Role is soft-typed per spec §5.1
509
+ -- (requester | implementer | reviewer | observer | affected | NULL)
510
+ -- — the CHECK enum is intentionally NOT applied so future spec changes
511
+ -- don't require another migration.
512
+
513
+ ALTER TABLE entity_timeline_events ADD COLUMN role TEXT;
514
+
515
+
516
+ -- ================================================================
517
+ -- Phase 3 — connector_event_index
518
+ -- ================================================================
519
+ -- Migration 045: Create connector_event_index and replay-safe connector cursors
520
+ -- Phase 3 of MAMA Case-First Memory System
521
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md Amendment 9
522
+
523
+ CREATE TABLE IF NOT EXISTS connector_event_index (
524
+ event_index_id TEXT PRIMARY KEY,
525
+ source_connector TEXT NOT NULL,
526
+ source_type TEXT NOT NULL,
527
+ source_id TEXT NOT NULL,
528
+ source_locator TEXT,
529
+ channel TEXT,
530
+ author TEXT,
531
+ title TEXT,
532
+ content TEXT NOT NULL,
533
+ event_datetime INTEGER,
534
+ event_date TEXT,
535
+ source_timestamp_ms INTEGER NOT NULL,
536
+ metadata_json TEXT,
537
+ artifact_locator TEXT,
538
+ artifact_title TEXT,
539
+ content_hash BLOB NOT NULL CHECK(length(content_hash) = 32),
540
+ indexed_at TEXT NOT NULL,
541
+ updated_at TEXT NOT NULL,
542
+ expires_at TEXT
543
+ );
544
+
545
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_source_identity
546
+ ON connector_event_index(source_connector, source_id);
547
+
548
+ CREATE INDEX IF NOT EXISTS idx_connector_event_index_cursor_order
549
+ ON connector_event_index(source_connector, source_timestamp_ms, source_id);
550
+
551
+ CREATE INDEX IF NOT EXISTS idx_connector_event_index_event_datetime
552
+ ON connector_event_index(event_datetime, event_index_id);
553
+
554
+ CREATE INDEX IF NOT EXISTS idx_connector_event_index_event_date
555
+ ON connector_event_index(event_date, event_datetime, event_index_id);
556
+
557
+ CREATE INDEX IF NOT EXISTS idx_connector_event_index_artifact_locator
558
+ ON connector_event_index(artifact_locator)
559
+ WHERE artifact_locator IS NOT NULL;
560
+
561
+ CREATE INDEX IF NOT EXISTS idx_connector_event_index_expires_at
562
+ ON connector_event_index(expires_at)
563
+ WHERE expires_at IS NOT NULL;
564
+
565
+ CREATE VIRTUAL TABLE IF NOT EXISTS connector_event_index_fts USING fts5(
566
+ event_index_id UNINDEXED,
567
+ title,
568
+ content,
569
+ author,
570
+ channel,
571
+ tokenize = 'unicode61 remove_diacritics 2'
572
+ );
573
+
574
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_ai
575
+ AFTER INSERT ON connector_event_index
576
+ BEGIN
577
+ INSERT INTO connector_event_index_fts(event_index_id, title, content, author, channel)
578
+ VALUES (NEW.event_index_id, NEW.title, NEW.content, NEW.author, NEW.channel);
579
+ END;
580
+
581
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_au
582
+ AFTER UPDATE OF title, content, author, channel ON connector_event_index
583
+ BEGIN
584
+ DELETE FROM connector_event_index_fts WHERE event_index_id = OLD.event_index_id;
585
+ INSERT INTO connector_event_index_fts(event_index_id, title, content, author, channel)
586
+ VALUES (NEW.event_index_id, NEW.title, NEW.content, NEW.author, NEW.channel);
587
+ END;
588
+
589
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_ad
590
+ AFTER DELETE ON connector_event_index
591
+ BEGIN
592
+ DELETE FROM connector_event_index_fts WHERE event_index_id = OLD.event_index_id;
593
+ END;
594
+
595
+ CREATE TABLE IF NOT EXISTS connector_event_index_cursors (
596
+ connector_name TEXT PRIMARY KEY,
597
+ last_seen_timestamp_ms INTEGER NOT NULL DEFAULT 0,
598
+ last_seen_source_id TEXT NOT NULL DEFAULT '',
599
+ last_sweep_at TEXT,
600
+ last_success_at TEXT,
601
+ last_error TEXT,
602
+ last_error_at TEXT,
603
+ indexed_count INTEGER NOT NULL DEFAULT 0
604
+ );
605
+
606
+
607
+ -- ================================================================
608
+ -- Phase 3 — search_feedback + ranker
609
+ -- ================================================================
610
+ -- Migration 046: Create search_feedback and ranker_model_versions
611
+ -- Phase 3 of MAMA Case-First Memory System
612
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md Amendment 10
613
+
614
+ CREATE TABLE IF NOT EXISTS search_feedback (
615
+ feedback_id TEXT PRIMARY KEY,
616
+ query TEXT NOT NULL,
617
+ query_hash BLOB NOT NULL CHECK(length(query_hash) = 32),
618
+ question_type TEXT NOT NULL CHECK(question_type IN (
619
+ 'status',
620
+ 'timeline',
621
+ 'artifact',
622
+ 'correction',
623
+ 'decision_reason',
624
+ 'how_to',
625
+ 'unknown'
626
+ )),
627
+ result_source_type TEXT NOT NULL CHECK(result_source_type IN (
628
+ 'decision',
629
+ 'checkpoint',
630
+ 'wiki_page',
631
+ 'case',
632
+ 'connector_event'
633
+ )),
634
+ result_source_id TEXT NOT NULL,
635
+ case_id TEXT REFERENCES case_truth(case_id),
636
+ feedback_kind TEXT NOT NULL CHECK(feedback_kind IN ('shown','click','accept','reject','hide')),
637
+ rank_position INTEGER NOT NULL CHECK(rank_position >= 0),
638
+ score_before REAL,
639
+ score_after REAL,
640
+ session_id TEXT,
641
+ actor TEXT NOT NULL,
642
+ metadata_json TEXT,
643
+ created_at TEXT NOT NULL,
644
+ updated_at TEXT
645
+ );
646
+
647
+ CREATE INDEX IF NOT EXISTS idx_search_feedback_query_hash_created
648
+ ON search_feedback(query_hash, created_at DESC);
649
+
650
+ CREATE INDEX IF NOT EXISTS idx_search_feedback_result_created
651
+ ON search_feedback(result_source_type, result_source_id, created_at DESC);
652
+
653
+ CREATE INDEX IF NOT EXISTS idx_search_feedback_case_created
654
+ ON search_feedback(case_id, created_at DESC)
655
+ WHERE case_id IS NOT NULL;
656
+
657
+ CREATE INDEX IF NOT EXISTS idx_search_feedback_kind_created
658
+ ON search_feedback(feedback_kind, created_at DESC);
659
+
660
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_search_feedback_shown_dedupe
661
+ ON search_feedback(session_id, query_hash, result_source_type, result_source_id)
662
+ WHERE feedback_kind = 'shown' AND session_id IS NOT NULL;
663
+
664
+ CREATE TABLE IF NOT EXISTS ranker_model_versions (
665
+ model_id TEXT PRIMARY KEY,
666
+ model_version TEXT NOT NULL,
667
+ feature_set_version TEXT NOT NULL,
668
+ coefficients_json TEXT NOT NULL,
669
+ metrics_json TEXT NOT NULL,
670
+ training_window_json TEXT NOT NULL,
671
+ baseline_metrics_json TEXT NOT NULL,
672
+ quality_gate_status TEXT NOT NULL CHECK(quality_gate_status IN ('passed','failed','not_run')),
673
+ trained_at TEXT NOT NULL,
674
+ trained_by TEXT,
675
+ active INTEGER NOT NULL DEFAULT 0 CHECK(active IN (0,1))
676
+ );
677
+
678
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ranker_model_versions_single_active
679
+ ON ranker_model_versions(active)
680
+ WHERE active = 1;
681
+
682
+ CREATE INDEX IF NOT EXISTS idx_ranker_model_versions_trained_at
683
+ ON ranker_model_versions(trained_at DESC);
684
+
685
+ CREATE INDEX IF NOT EXISTS idx_ranker_model_versions_feature_set
686
+ ON ranker_model_versions(feature_set_version);
687
+
688
+ CREATE TABLE IF NOT EXISTS search_ranker_settings (
689
+ key TEXT PRIMARY KEY,
690
+ value_json TEXT NOT NULL,
691
+ updated_at TEXT NOT NULL
692
+ );
693
+
694
+ INSERT OR IGNORE INTO search_ranker_settings (key, value_json, updated_at)
695
+ VALUES ('search_feedback_retention_days', '180', strftime('%Y-%m-%dT%H:%M:%fZ','now'));
696
+
697
+
698
+ -- ================================================================
699
+ -- Phase 3 — case_links + tombstones
700
+ -- ================================================================
701
+ -- Migration 047: Create case_links and revoked wiki tombstones
702
+ -- Phase 3 of MAMA Case-First Memory System
703
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md Amendment 11
704
+
705
+ CREATE TABLE IF NOT EXISTS case_links (
706
+ link_id TEXT PRIMARY KEY,
707
+ case_id_from TEXT NOT NULL REFERENCES case_truth(case_id),
708
+ case_id_to TEXT NOT NULL REFERENCES case_truth(case_id),
709
+ link_type TEXT NOT NULL CHECK(link_type IN (
710
+ 'related',
711
+ 'supersedes-case',
712
+ 'subcase-of',
713
+ 'blocked-by',
714
+ 'duplicate-of'
715
+ )),
716
+ created_at TEXT NOT NULL,
717
+ created_by TEXT NOT NULL,
718
+ confidence REAL CHECK(confidence IS NULL OR (confidence >= 0 AND confidence <= 1)),
719
+ reason_json TEXT,
720
+ source_kind TEXT NOT NULL DEFAULT 'manual' CHECK(source_kind IN (
721
+ 'manual',
722
+ 'wiki_compiler',
723
+ 'hitl_correction',
724
+ 'system_backfill'
725
+ )),
726
+ source_ref TEXT,
727
+ source_ref_fingerprint BLOB CHECK(
728
+ source_ref_fingerprint IS NULL OR length(source_ref_fingerprint) = 32
729
+ ),
730
+ revoked_at TEXT,
731
+ revoked_by TEXT,
732
+ revoke_reason TEXT,
733
+ CHECK(case_id_from <> case_id_to)
734
+ );
735
+
736
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_case_links_active_unique
737
+ ON case_links(case_id_from, case_id_to, link_type)
738
+ WHERE revoked_at IS NULL;
739
+
740
+ CREATE INDEX IF NOT EXISTS idx_case_links_from_active
741
+ ON case_links(case_id_from, created_at DESC, link_id)
742
+ WHERE revoked_at IS NULL;
743
+
744
+ CREATE INDEX IF NOT EXISTS idx_case_links_to_active
745
+ ON case_links(case_id_to, created_at DESC, link_id)
746
+ WHERE revoked_at IS NULL;
747
+
748
+ CREATE INDEX IF NOT EXISTS idx_case_links_type_active
749
+ ON case_links(link_type, created_at DESC, link_id)
750
+ WHERE revoked_at IS NULL;
751
+
752
+ CREATE INDEX IF NOT EXISTS idx_case_links_source_ref_fingerprint
753
+ ON case_links(source_ref_fingerprint)
754
+ WHERE source_ref_fingerprint IS NOT NULL;
755
+
756
+ CREATE TABLE IF NOT EXISTS case_links_revoked_wiki_tombstones (
757
+ tombstone_id TEXT PRIMARY KEY,
758
+ case_id_from TEXT NOT NULL,
759
+ case_id_to TEXT NOT NULL,
760
+ link_type TEXT NOT NULL,
761
+ source_ref_fingerprint BLOB NOT NULL CHECK(length(source_ref_fingerprint) = 32),
762
+ source_ref TEXT NOT NULL,
763
+ created_at TEXT NOT NULL,
764
+ created_by TEXT NOT NULL,
765
+ revoke_reason TEXT NOT NULL,
766
+ unsuppressed_at TEXT,
767
+ unsuppressed_by TEXT
768
+ );
769
+
770
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_case_links_wiki_tombstone_active_unique
771
+ ON case_links_revoked_wiki_tombstones(
772
+ case_id_from,
773
+ case_id_to,
774
+ link_type,
775
+ source_ref_fingerprint
776
+ )
777
+ WHERE unsuppressed_at IS NULL;
778
+
779
+ CREATE INDEX IF NOT EXISTS idx_case_links_wiki_tombstone_source
780
+ ON case_links_revoked_wiki_tombstones(case_id_from, unsuppressed_at);
781
+
782
+ INSERT OR IGNORE INTO case_links (
783
+ link_id,
784
+ case_id_from,
785
+ case_id_to,
786
+ link_type,
787
+ created_at,
788
+ created_by,
789
+ confidence,
790
+ reason_json,
791
+ source_kind,
792
+ source_ref,
793
+ source_ref_fingerprint,
794
+ revoked_at,
795
+ revoked_by,
796
+ revoke_reason
797
+ )
798
+ SELECT
799
+ 'backfill:canonical:' || case_id,
800
+ case_id,
801
+ canonical_case_id,
802
+ 'duplicate-of',
803
+ updated_at,
804
+ 'system',
805
+ NULL,
806
+ '{"authority":"case_truth.canonical_case_id"}',
807
+ 'system_backfill',
808
+ 'case_truth.canonical_case_id',
809
+ NULL,
810
+ NULL,
811
+ NULL,
812
+ NULL
813
+ FROM case_truth
814
+ WHERE canonical_case_id IS NOT NULL
815
+ AND canonical_case_id <> case_id;
816
+
817
+ INSERT OR IGNORE INTO case_links (
818
+ link_id,
819
+ case_id_from,
820
+ case_id_to,
821
+ link_type,
822
+ created_at,
823
+ created_by,
824
+ confidence,
825
+ reason_json,
826
+ source_kind,
827
+ source_ref,
828
+ source_ref_fingerprint,
829
+ revoked_at,
830
+ revoked_by,
831
+ revoke_reason
832
+ )
833
+ SELECT
834
+ 'backfill:split:' || case_id,
835
+ case_id,
836
+ split_from_case_id,
837
+ 'subcase-of',
838
+ updated_at,
839
+ 'system',
840
+ NULL,
841
+ '{"authority":"case_truth.split_from_case_id"}',
842
+ 'system_backfill',
843
+ 'case_truth.split_from_case_id',
844
+ NULL,
845
+ NULL,
846
+ NULL,
847
+ NULL
848
+ FROM case_truth
849
+ WHERE split_from_case_id IS NOT NULL
850
+ AND split_from_case_id <> case_id;
851
+
852
+
853
+ -- ================================================================
854
+ -- Phase 3 — case_truth promotion + freshness
855
+ -- ================================================================
856
+ -- Migration 048: Add case promotion and freshness columns
857
+ -- Phase 3 of MAMA Case-First Memory System
858
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md Amendments 12 + 13
859
+
860
+ ALTER TABLE case_truth ADD COLUMN canonical_decision_id TEXT;
861
+ ALTER TABLE case_truth ADD COLUMN canonical_event_id TEXT;
862
+ ALTER TABLE case_truth ADD COLUMN promoted_at TEXT;
863
+ ALTER TABLE case_truth ADD COLUMN promoted_by TEXT;
864
+ ALTER TABLE case_truth ADD COLUMN promotion_reason TEXT;
865
+
866
+ ALTER TABLE case_truth ADD COLUMN freshness_score REAL CHECK(
867
+ freshness_score IS NULL OR (freshness_score >= 0 AND freshness_score <= 1)
868
+ );
869
+ ALTER TABLE case_truth ADD COLUMN freshness_state TEXT CHECK(
870
+ freshness_state IS NULL OR freshness_state IN ('fresh','stale','drifted','unknown')
871
+ );
872
+ ALTER TABLE case_truth ADD COLUMN freshness_score_is_drifted INTEGER NOT NULL DEFAULT 0 CHECK(
873
+ freshness_score_is_drifted IN (0,1)
874
+ );
875
+ ALTER TABLE case_truth ADD COLUMN freshness_drift_threshold REAL CHECK(
876
+ freshness_drift_threshold IS NULL OR (
877
+ freshness_drift_threshold >= 0 AND freshness_drift_threshold <= 1
878
+ )
879
+ );
880
+ ALTER TABLE case_truth ADD COLUMN freshness_checked_at TEXT;
881
+ ALTER TABLE case_truth ADD COLUMN freshness_reason_json TEXT;
882
+
883
+
884
+ -- ================================================================
885
+ -- Phase 3 — case_memberships explanation columns
886
+ -- ================================================================
887
+ -- Migration 049: Add case membership explanation columns
888
+ -- Phase 3 of MAMA Case-First Memory System
889
+ -- Spec: docs/superpowers/specs/2026-04-17-mama-work-case-first-memory-system-design.md Amendment 14
890
+
891
+ ALTER TABLE case_memberships ADD COLUMN assignment_strategy TEXT;
892
+ ALTER TABLE case_memberships ADD COLUMN score_breakdown_json TEXT;
893
+ ALTER TABLE case_memberships ADD COLUMN source_locator TEXT;
894
+ ALTER TABLE case_memberships ADD COLUMN explanation_updated_at TEXT;
895
+
896
+
897
+ -- ================================================================
898
+ -- Final schema_version marker
899
+ -- ================================================================
900
+ INSERT OR IGNORE INTO schema_version (version, description)
901
+ VALUES (30, 'Case-First Memory Substrate consolidated (Phase 1+2+3)');