watashi-db 0.0.13 → 0.0.14

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 (77) hide show
  1. package/CLAUDE.md +36 -0
  2. package/LICENSE +1 -1
  3. package/README.md +33 -2
  4. package/cowork-plugin/skills/groom/SKILL.md +51 -15
  5. package/cowork-plugin/skills/recall/SKILL.md +5 -6
  6. package/cowork-plugin/skills/reflect/SKILL.md +4 -4
  7. package/cowork-plugin/skills/remember/SKILL.md +3 -3
  8. package/cowork-plugin/skills/session-start/SKILL.md +3 -3
  9. package/dist/config/schema.js +1 -1
  10. package/dist/constants.d.ts +5 -1
  11. package/dist/constants.js +19 -3
  12. package/dist/constants.js.map +1 -1
  13. package/dist/database/archive.js +6 -6
  14. package/dist/database/queries-core.d.ts +75 -1
  15. package/dist/database/queries-core.js +283 -12
  16. package/dist/database/queries-core.js.map +1 -1
  17. package/dist/database/queries.d.ts +71 -1
  18. package/dist/database/queries.js +29 -0
  19. package/dist/database/queries.js.map +1 -1
  20. package/dist/database/schema.d.ts +1 -0
  21. package/dist/database/schema.js +1915 -214
  22. package/dist/database/schema.js.map +1 -1
  23. package/dist/embedding/embed-on-write.d.ts +7 -1
  24. package/dist/embedding/embed-on-write.js +8 -3
  25. package/dist/embedding/embed-on-write.js.map +1 -1
  26. package/dist/hook.js +9 -6
  27. package/dist/hook.js.map +1 -1
  28. package/dist/index.js +3 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/resources/config-guide-content.d.ts +1 -1
  31. package/dist/resources/config-guide-content.js +2 -2
  32. package/dist/server-instructions.js +16 -17
  33. package/dist/server-instructions.js.map +1 -1
  34. package/dist/server.d.ts +4 -1
  35. package/dist/server.js +42 -18
  36. package/dist/server.js.map +1 -1
  37. package/dist/setup.js +5 -6
  38. package/dist/setup.js.map +1 -1
  39. package/dist/store/federation.d.ts +12 -1
  40. package/dist/store/federation.js +38 -0
  41. package/dist/store/federation.js.map +1 -1
  42. package/dist/store/sync-manager.d.ts +1 -1
  43. package/dist/store/sync-manager.js +9 -9
  44. package/dist/tools/claim-tools.d.ts +1 -1
  45. package/dist/tools/claim-tools.js +7 -7
  46. package/dist/tools/claim-tools.js.map +1 -1
  47. package/dist/tools/decision-tools.d.ts +1 -1
  48. package/dist/tools/decision-tools.js +2 -2
  49. package/dist/tools/decision-tools.js.map +1 -1
  50. package/dist/tools/episode-tools.d.ts +1 -1
  51. package/dist/tools/episode-tools.js +2 -2
  52. package/dist/tools/episode-tools.js.map +1 -1
  53. package/dist/tools/file-tools.d.ts +3 -0
  54. package/dist/tools/file-tools.js +347 -0
  55. package/dist/tools/file-tools.js.map +1 -0
  56. package/dist/tools/get-tools.d.ts +1 -1
  57. package/dist/tools/get-tools.js +39 -5
  58. package/dist/tools/get-tools.js.map +1 -1
  59. package/dist/tools/knowledge-tools.d.ts +1 -1
  60. package/dist/tools/knowledge-tools.js +2 -2
  61. package/dist/tools/knowledge-tools.js.map +1 -1
  62. package/dist/tools/maintenance-tools.d.ts +1 -1
  63. package/dist/tools/maintenance-tools.js +38 -6
  64. package/dist/tools/maintenance-tools.js.map +1 -1
  65. package/dist/tools/memo-tools.d.ts +7 -11
  66. package/dist/tools/memo-tools.js +499 -307
  67. package/dist/tools/memo-tools.js.map +1 -1
  68. package/dist/tools/query-tools.d.ts +1 -1
  69. package/dist/tools/query-tools.js +28 -5
  70. package/dist/tools/query-tools.js.map +1 -1
  71. package/dist/types.d.ts +370 -48
  72. package/dist/types.js +124 -16
  73. package/dist/types.js.map +1 -1
  74. package/misc/20260316_110841_groom-recipe.md +483 -0
  75. package/misc/20260316_xaml-testing-library-recipe.md +817 -0
  76. package/package.json +4 -2
  77. package/scripts/update-license-version.sh +7 -0
@@ -25,13 +25,28 @@ import fs from "node:fs";
25
25
  // 2026-02-28 修正: V18追加(user_plans + user_issues テーブル新設 — Issue #57)
26
26
  // 2026-03-01 修正: V19追加(Insight l1/l2 拡張 — Issue #53)
27
27
  // 2026-03-01 修正: V20追加(全エンティティ l1/l2 カラム拡張 — Issue #59)
28
- // 2026-03-03 修正: V21追加(audit_log に client_id カラム追加 + backfill)
28
+ // 2026-03-03 修正: V21追加(audit_log に device_id カラム追加 + backfill)
29
29
  // 2026-03-03 修正: V22追加(entity_id インデックス追加 — Issue #60)
30
30
  // 2026-03-03 修正: V23追加(Claim に l1_content 追加 + embedding → l1_embedding リネーム — Issue #64)
31
31
  // 2026-03-04 修正: V24追加(user_memo/plan/issue の content → l1_content + user_memos に user_input 追加 — Issue #54)
32
32
  // 2026-03-06 修正: V25追加(user_issues に kind カラム追加 — Issue #74)
33
33
  // 2026-03-13 修正: V28追加(evidence → l2_evidence, falsifier → l2_falsifier リネーム)
34
- const SCHEMA_VERSION = 28;
34
+ // 2026-03-18 修正: V29追加(user_topics テーブル新設)
35
+ // 2026-03-19 squash: applyV1 を V30 相当のフルスキーマに書き直し。
36
+ // 新規DBでは applyV1 が全テーブルを作成し schema_version (1)〜(30) を一括挿入するため、
37
+ // V2〜V30 のマイグレーションは全てスキップされる。既存ユーザー向けに V2〜V30 はそのまま残す。
38
+ // 2026-03-24 修正: V31追加(File Vault — user_files テーブル + unified search統合)
39
+ const SCHEMA_VERSION = 31;
40
+ // devMode フラグ(startServer から schema 初期化経路に伝播)
41
+ // develop.db では released: false のマイグレーションも適用する
42
+ let _devMode = false;
43
+ export function setDevMode(enabled) {
44
+ _devMode = enabled;
45
+ }
46
+ // V31 以降で released フラグを使う場合:
47
+ // if (version < 31) {
48
+ // if (_devMode || V31_RELEASED) applyV31(db);
49
+ // }
35
50
  /**
36
51
  * データベーススキーマの初期化とマイグレーション
37
52
  *
@@ -50,6 +65,11 @@ export function initializeSchema(db) {
50
65
  `);
51
66
  const currentVersion = db.prepare("SELECT MAX(version) as version FROM schema_version").get();
52
67
  const version = currentVersion?.version ?? 0;
68
+ // 孤立した _new テーブルの掃除(マイグレーション部分適用の残骸)
69
+ const orphanedNewTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_new'").all();
70
+ for (const { name } of orphanedNewTables) {
71
+ db.exec(`DROP TABLE IF EXISTS "${name}"`);
72
+ }
53
73
  if (version >= SCHEMA_VERSION) {
54
74
  return; // 既に最新
55
75
  }
@@ -119,6 +139,10 @@ export function initializeSchema(db) {
119
139
  function _applyMigrations(db, version) {
120
140
  if (version < 1) {
121
141
  applyV1(db);
142
+ // squash 対応: applyV1 が V30 相当のフルスキーマを作成し schema_version (1)〜(30) を
143
+ // 一括挿入した場合、以降のマイグレーションをスキップするために version を再読み込みする
144
+ const row = db.prepare("SELECT MAX(version) as version FROM schema_version").get();
145
+ version = row?.version ?? 1;
122
146
  }
123
147
  if (version < 2) {
124
148
  applyV2(db);
@@ -201,82 +225,414 @@ function _applyMigrations(db, version) {
201
225
  if (version < 28) {
202
226
  applyV28(db);
203
227
  }
228
+ if (version < 29) {
229
+ applyV29(db);
230
+ }
231
+ if (version < 30) {
232
+ applyV30(db);
233
+ }
234
+ if (version < 31) {
235
+ applyV31(db);
236
+ }
204
237
  }
205
238
  /**
206
- * V1スキーマ: 初期テーブル構成
239
+ * V1スキーマ: V30相当のフルスキーマ(squash済み)
240
+ *
241
+ * 2026-03-19: applyV1 を V30 時点の最終テーブル定義で書き直し。
242
+ * 新規ユーザーが V1→V30 まで逐次マイグレーションする非効率を解消。
243
+ * V2〜V30 の関数は既存ユーザー向けにそのまま残してある。
244
+ * applyV1 の最後に schema_version (1)〜(30) を全挿入することで、
245
+ * _applyMigrations の各 `if (version < N)` ガードにより V2〜V30 が全てスキップされる。
207
246
  */
208
247
  function applyV1(db) {
248
+ // JSON配列クリーニングヘルパー(FTSトリガー用)
249
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
250
+ // user_topics 用の JSON クリーニング(V29 パターン)
251
+ const jcTopics = (col) => `REPLACE(REPLACE(${col}, '[', ''), ']', '')`;
252
+ const coal = (col) => `COALESCE(${col}, '')`;
253
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
254
+ // ================================================================
255
+ // 1. テーブル作成(CREATE TABLE + インデックス)
256
+ // ================================================================
209
257
  db.exec(`
210
- -- claims: 知識断片(SPO三つ組)
258
+ -- --------------------------------------------------------
259
+ -- claims: 知識断片(L2 SPO三つ組)
260
+ -- --------------------------------------------------------
211
261
  CREATE TABLE IF NOT EXISTS claims (
212
262
  id TEXT PRIMARY KEY,
213
- subject TEXT NOT NULL,
214
- predicate TEXT NOT NULL,
215
- object TEXT NOT NULL,
263
+ l2_subject TEXT NOT NULL,
264
+ l2_predicate TEXT NOT NULL,
265
+ l2_object TEXT NOT NULL,
216
266
  category TEXT NOT NULL CHECK(category IN ('preference','identity','skill','value','workflow','knowledge','custom')),
217
267
  scope TEXT NOT NULL DEFAULT 'global',
218
268
  confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
219
- evidence TEXT,
269
+ l2_evidence TEXT,
270
+ l2_falsifier TEXT,
271
+ l1_content TEXT,
272
+ search_summary TEXT,
273
+ l1_embedding BLOB,
274
+ hit_count INTEGER NOT NULL DEFAULT 0,
275
+ last_hit_at TEXT,
276
+ promoted_from_store TEXT,
277
+ promoted_from_id TEXT,
278
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','retracted','superseded','promoted')),
220
279
  source_tool TEXT,
221
280
  source_session TEXT,
222
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','retracted','superseded')),
281
+ client_name TEXT,
282
+ client_version TEXT,
223
283
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
224
284
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
225
285
  );
226
286
 
227
- -- claims用インデックス
228
287
  CREATE INDEX IF NOT EXISTS idx_claims_category ON claims(category);
229
288
  CREATE INDEX IF NOT EXISTS idx_claims_scope ON claims(scope);
230
289
  CREATE INDEX IF NOT EXISTS idx_claims_status ON claims(status);
231
- CREATE INDEX IF NOT EXISTS idx_claims_subject ON claims(subject);
290
+ CREATE INDEX IF NOT EXISTS idx_claims_l2_subject ON claims(l2_subject);
232
291
  CREATE INDEX IF NOT EXISTS idx_claims_updated ON claims(updated_at);
292
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_claims_promoted_from ON claims(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL;
233
293
 
234
- -- claims用FTS5全文検索(日本語対応のためtokenize=unicode61)
235
- CREATE VIRTUAL TABLE IF NOT EXISTS claims_fts USING fts5(
236
- subject, predicate, object, evidence,
237
- content='claims',
238
- content_rowid='rowid',
239
- tokenize='unicode61'
294
+ -- --------------------------------------------------------
295
+ -- episodes: エピソード記録
296
+ -- --------------------------------------------------------
297
+ CREATE TABLE IF NOT EXISTS episodes (
298
+ id TEXT PRIMARY KEY,
299
+ title TEXT NOT NULL,
300
+ l1_content TEXT,
301
+ l2_context TEXT,
302
+ l2_trigger TEXT,
303
+ l2_problems TEXT NOT NULL DEFAULT '[]',
304
+ l2_desires TEXT NOT NULL DEFAULT '[]',
305
+ l2_decisions TEXT NOT NULL DEFAULT '[]',
306
+ l2_outcomes TEXT NOT NULL DEFAULT '[]',
307
+ l2_principles TEXT NOT NULL DEFAULT '[]',
308
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
309
+ tags TEXT NOT NULL DEFAULT '[]',
310
+ scope TEXT NOT NULL DEFAULT 'global',
311
+ search_summary TEXT,
312
+ l1_embedding BLOB,
313
+ promoted_from_store TEXT,
314
+ promoted_from_id TEXT,
315
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
316
+ groomed_at TEXT,
317
+ source_tool TEXT,
318
+ session_id TEXT,
319
+ client_name TEXT,
320
+ client_version TEXT,
321
+ user_input TEXT,
322
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
323
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
240
324
  );
241
325
 
242
- -- FTS同期トリガー: INSERT
243
- CREATE TRIGGER IF NOT EXISTS claims_fts_insert AFTER INSERT ON claims BEGIN
244
- INSERT INTO claims_fts(rowid, subject, predicate, object, evidence)
245
- VALUES (new.rowid, new.subject, new.predicate, new.object, new.evidence);
246
- END;
247
-
248
- -- FTS同期トリガー: UPDATE
249
- CREATE TRIGGER IF NOT EXISTS claims_fts_update AFTER UPDATE ON claims BEGIN
250
- INSERT INTO claims_fts(claims_fts, rowid, subject, predicate, object, evidence)
251
- VALUES ('delete', old.rowid, old.subject, old.predicate, old.object, old.evidence);
252
- INSERT INTO claims_fts(rowid, subject, predicate, object, evidence)
253
- VALUES (new.rowid, new.subject, new.predicate, new.object, new.evidence);
254
- END;
255
-
256
- -- FTS同期トリガー: DELETE
257
- CREATE TRIGGER IF NOT EXISTS claims_fts_delete AFTER DELETE ON claims BEGIN
258
- INSERT INTO claims_fts(claims_fts, rowid, subject, predicate, object, evidence)
259
- VALUES ('delete', old.rowid, old.subject, old.predicate, old.object, old.evidence);
260
- END;
326
+ CREATE INDEX IF NOT EXISTS idx_episodes_scope ON episodes(scope);
327
+ CREATE INDEX IF NOT EXISTS idx_episodes_status ON episodes(status);
328
+ CREATE INDEX IF NOT EXISTS idx_episodes_groomed ON episodes(groomed_at);
329
+ CREATE INDEX IF NOT EXISTS idx_episodes_created ON episodes(created_at);
330
+ CREATE INDEX IF NOT EXISTS idx_episodes_updated ON episodes(updated_at);
331
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_episodes_promoted_from ON episodes(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL;
261
332
 
333
+ -- --------------------------------------------------------
262
334
  -- decisions: 意思決定ログ
335
+ -- --------------------------------------------------------
263
336
  CREATE TABLE IF NOT EXISTS decisions (
264
337
  id TEXT PRIMARY KEY,
265
338
  title TEXT NOT NULL,
266
339
  description TEXT NOT NULL,
267
- reasoning TEXT NOT NULL,
268
- alternatives TEXT NOT NULL DEFAULT '[]',
340
+ l1_content TEXT,
341
+ l2_reasoning TEXT NOT NULL,
342
+ l2_alternatives TEXT NOT NULL DEFAULT '[]',
269
343
  related_claim_ids TEXT NOT NULL DEFAULT '[]',
270
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','reversed','obsolete')),
344
+ scope TEXT NOT NULL DEFAULT 'global',
345
+ search_summary TEXT,
346
+ l1_embedding BLOB,
347
+ promoted_from_store TEXT,
348
+ promoted_from_id TEXT,
349
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','reversed','obsolete','promoted')),
271
350
  source_tool TEXT,
272
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
273
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
351
+ client_name TEXT,
352
+ client_version TEXT,
353
+ user_input TEXT,
354
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
355
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
274
356
  );
275
357
 
358
+ CREATE INDEX IF NOT EXISTS idx_decisions_scope ON decisions(scope);
276
359
  CREATE INDEX IF NOT EXISTS idx_decisions_status ON decisions(status);
277
360
  CREATE INDEX IF NOT EXISTS idx_decisions_created ON decisions(created_at);
361
+ CREATE INDEX IF NOT EXISTS idx_decisions_updated ON decisions(updated_at);
362
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_decisions_promoted_from ON decisions(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL;
363
+
364
+ -- --------------------------------------------------------
365
+ -- theories: 体系的知識・フレームワーク
366
+ -- --------------------------------------------------------
367
+ CREATE TABLE IF NOT EXISTS theories (
368
+ id TEXT PRIMARY KEY,
369
+ title TEXT NOT NULL,
370
+ description TEXT,
371
+ l1_content TEXT,
372
+ l2_core_thesis TEXT,
373
+ l2_principles TEXT NOT NULL DEFAULT '[]',
374
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
375
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
376
+ l2_applicable_context TEXT,
377
+ non_goals TEXT NOT NULL DEFAULT '[]',
378
+ open_questions TEXT NOT NULL DEFAULT '[]',
379
+ tags TEXT NOT NULL DEFAULT '[]',
380
+ scope TEXT NOT NULL DEFAULT 'global',
381
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
382
+ search_summary TEXT,
383
+ l1_embedding BLOB,
384
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
385
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
386
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
387
+ promoted_from_store TEXT,
388
+ promoted_from_id TEXT,
389
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
390
+ source_tool TEXT,
391
+ session_id TEXT,
392
+ client_name TEXT,
393
+ client_version TEXT,
394
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
395
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
396
+ );
397
+
398
+ CREATE INDEX IF NOT EXISTS idx_theories_scope ON theories(scope);
399
+ CREATE INDEX IF NOT EXISTS idx_theories_status ON theories(status);
400
+ CREATE INDEX IF NOT EXISTS idx_theories_confidence ON theories(confidence);
401
+ CREATE INDEX IF NOT EXISTS idx_theories_created ON theories(created_at);
402
+ CREATE INDEX IF NOT EXISTS idx_theories_updated ON theories(updated_at);
403
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_theories_promoted_from ON theories(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL;
404
+
405
+ -- --------------------------------------------------------
406
+ -- insights: 洞察(theories のサブセット、non_goals/open_questions なし)
407
+ -- --------------------------------------------------------
408
+ CREATE TABLE IF NOT EXISTS insights (
409
+ id TEXT PRIMARY KEY,
410
+ title TEXT NOT NULL,
411
+ description TEXT,
412
+ l1_content TEXT,
413
+ l2_core_thesis TEXT,
414
+ l2_principles TEXT NOT NULL DEFAULT '[]',
415
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
416
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
417
+ l2_applicable_context TEXT,
418
+ tags TEXT NOT NULL DEFAULT '[]',
419
+ scope TEXT NOT NULL DEFAULT 'global',
420
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
421
+ search_summary TEXT,
422
+ l1_embedding BLOB,
423
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
424
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
425
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
426
+ promoted_from_store TEXT,
427
+ promoted_from_id TEXT,
428
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
429
+ source_tool TEXT,
430
+ session_id TEXT,
431
+ client_name TEXT,
432
+ client_version TEXT,
433
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
434
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
435
+ );
436
+
437
+ CREATE INDEX IF NOT EXISTS idx_insights_scope ON insights(scope);
438
+ CREATE INDEX IF NOT EXISTS idx_insights_status ON insights(status);
439
+ CREATE INDEX IF NOT EXISTS idx_insights_confidence ON insights(confidence);
440
+ CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at);
441
+ CREATE INDEX IF NOT EXISTS idx_insights_updated ON insights(updated_at);
442
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_insights_promoted_from ON insights(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL;
443
+
444
+ -- --------------------------------------------------------
445
+ -- models: モデル(theories と同構造)
446
+ -- --------------------------------------------------------
447
+ CREATE TABLE IF NOT EXISTS models (
448
+ id TEXT PRIMARY KEY,
449
+ title TEXT NOT NULL,
450
+ description TEXT,
451
+ l1_content TEXT,
452
+ l2_core_thesis TEXT,
453
+ l2_principles TEXT NOT NULL DEFAULT '[]',
454
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
455
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
456
+ l2_applicable_context TEXT,
457
+ non_goals TEXT NOT NULL DEFAULT '[]',
458
+ open_questions TEXT NOT NULL DEFAULT '[]',
459
+ tags TEXT NOT NULL DEFAULT '[]',
460
+ scope TEXT NOT NULL DEFAULT 'global',
461
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
462
+ search_summary TEXT,
463
+ l1_embedding BLOB,
464
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
465
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
466
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
467
+ promoted_from_store TEXT,
468
+ promoted_from_id TEXT,
469
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
470
+ source_tool TEXT,
471
+ session_id TEXT,
472
+ client_name TEXT,
473
+ client_version TEXT,
474
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
475
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
476
+ );
477
+
478
+ CREATE INDEX IF NOT EXISTS idx_models_scope ON models(scope);
479
+ CREATE INDEX IF NOT EXISTS idx_models_status ON models(status);
480
+ CREATE INDEX IF NOT EXISTS idx_models_confidence ON models(confidence);
481
+ CREATE INDEX IF NOT EXISTS idx_models_created ON models(created_at);
482
+ CREATE INDEX IF NOT EXISTS idx_models_updated ON models(updated_at);
483
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_models_promoted_from ON models(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL;
484
+
485
+ -- --------------------------------------------------------
486
+ -- user_memos: ユーザーメモ
487
+ -- --------------------------------------------------------
488
+ CREATE TABLE IF NOT EXISTS user_memos (
489
+ id TEXT PRIMARY KEY,
490
+ title TEXT NOT NULL,
491
+ l1_content TEXT NOT NULL,
492
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
493
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
494
+ tags TEXT NOT NULL DEFAULT '[]',
495
+ scope TEXT NOT NULL DEFAULT 'global',
496
+ search_summary TEXT,
497
+ user_input TEXT,
498
+ status TEXT NOT NULL DEFAULT 'active'
499
+ CHECK(status IN ('active','archived')),
500
+ l1_embedding BLOB,
501
+ client_name TEXT,
502
+ client_version TEXT,
503
+ source_tool TEXT,
504
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
505
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
506
+ );
507
+
508
+ CREATE INDEX IF NOT EXISTS idx_user_memos_status ON user_memos(status);
509
+ CREATE INDEX IF NOT EXISTS idx_user_memos_scope ON user_memos(scope);
510
+ CREATE INDEX IF NOT EXISTS idx_user_memos_usage_policy ON user_memos(usage_policy);
511
+ CREATE INDEX IF NOT EXISTS idx_user_memos_updated ON user_memos(updated_at);
512
+
513
+ -- --------------------------------------------------------
514
+ -- user_plans: ユーザープラン
515
+ -- --------------------------------------------------------
516
+ CREATE TABLE IF NOT EXISTS user_plans (
517
+ id TEXT PRIMARY KEY,
518
+ title TEXT NOT NULL,
519
+ l1_content TEXT NOT NULL,
520
+ usage_policy TEXT NOT NULL DEFAULT 'auto'
521
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
522
+ tags TEXT NOT NULL DEFAULT '[]',
523
+ scope TEXT NOT NULL DEFAULT 'global',
524
+ search_summary TEXT,
525
+ user_input TEXT,
526
+ status TEXT NOT NULL DEFAULT 'active'
527
+ CHECK(status IN ('active','completed','archived')),
528
+ l1_embedding BLOB,
529
+ client_name TEXT,
530
+ client_version TEXT,
531
+ source_tool TEXT,
532
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
533
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
534
+ );
535
+
536
+ CREATE INDEX IF NOT EXISTS idx_user_plans_status ON user_plans(status);
537
+ CREATE INDEX IF NOT EXISTS idx_user_plans_scope ON user_plans(scope);
538
+ CREATE INDEX IF NOT EXISTS idx_user_plans_usage_policy ON user_plans(usage_policy);
539
+ CREATE INDEX IF NOT EXISTS idx_user_plans_updated ON user_plans(updated_at);
540
+
541
+ -- --------------------------------------------------------
542
+ -- user_issues: ユーザー課題
543
+ -- --------------------------------------------------------
544
+ CREATE TABLE IF NOT EXISTS user_issues (
545
+ id TEXT PRIMARY KEY,
546
+ title TEXT NOT NULL,
547
+ l1_content TEXT NOT NULL,
548
+ entries TEXT NOT NULL DEFAULT '[]',
549
+ kind TEXT NOT NULL DEFAULT 'issue'
550
+ CHECK(kind IN ('issue','discussion')),
551
+ priority TEXT NOT NULL DEFAULT 'medium'
552
+ CHECK(priority IN ('low','medium','high','critical')),
553
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
554
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
555
+ tags TEXT NOT NULL DEFAULT '[]',
556
+ scope TEXT NOT NULL DEFAULT 'global',
557
+ search_summary TEXT,
558
+ user_input TEXT,
559
+ status TEXT NOT NULL DEFAULT 'open'
560
+ CHECK(status IN ('open','closed','archived')),
561
+ l1_embedding BLOB,
562
+ client_name TEXT,
563
+ client_version TEXT,
564
+ source_tool TEXT,
565
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
566
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
567
+ );
568
+
569
+ CREATE INDEX IF NOT EXISTS idx_user_issues_status ON user_issues(status);
570
+ CREATE INDEX IF NOT EXISTS idx_user_issues_scope ON user_issues(scope);
571
+ CREATE INDEX IF NOT EXISTS idx_user_issues_priority ON user_issues(priority);
572
+ CREATE INDEX IF NOT EXISTS idx_user_issues_updated ON user_issues(updated_at);
573
+
574
+ -- --------------------------------------------------------
575
+ -- user_topics: 話題管理
576
+ -- --------------------------------------------------------
577
+ CREATE TABLE IF NOT EXISTS user_topics (
578
+ id TEXT PRIMARY KEY,
579
+ title TEXT NOT NULL,
580
+ summary TEXT NOT NULL,
581
+ device_id TEXT,
582
+ cwd TEXT,
583
+ conversation_id TEXT,
584
+ priority TEXT NOT NULL DEFAULT 'medium'
585
+ CHECK(priority IN ('low','medium','high','critical')),
586
+ tags TEXT NOT NULL DEFAULT '[]',
587
+ scope TEXT NOT NULL DEFAULT 'global',
588
+ search_summary TEXT,
589
+ status TEXT NOT NULL DEFAULT 'active'
590
+ CHECK(status IN ('active','closed','archived')),
591
+ l1_embedding BLOB,
592
+ client_name TEXT,
593
+ client_version TEXT,
594
+ source_tool TEXT,
595
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
596
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
597
+ );
598
+
599
+ CREATE INDEX IF NOT EXISTS idx_user_topics_status ON user_topics(status);
600
+ CREATE INDEX IF NOT EXISTS idx_user_topics_scope ON user_topics(scope);
601
+ CREATE INDEX IF NOT EXISTS idx_user_topics_priority ON user_topics(priority);
602
+ CREATE INDEX IF NOT EXISTS idx_user_topics_updated ON user_topics(updated_at);
603
+
604
+ -- --------------------------------------------------------
605
+ -- user_files: ファイル管理(File Vault機能, V31)
606
+ -- --------------------------------------------------------
607
+ CREATE TABLE IF NOT EXISTS user_files (
608
+ id TEXT PRIMARY KEY,
609
+ title TEXT NOT NULL,
610
+ description TEXT,
611
+ original_filename TEXT NOT NULL,
612
+ original_encoding TEXT NOT NULL DEFAULT 'utf-8',
613
+ file_data TEXT NOT NULL,
614
+ file_hash TEXT NOT NULL,
615
+ file_size INTEGER NOT NULL,
616
+ tags TEXT NOT NULL DEFAULT '[]',
617
+ scope TEXT NOT NULL DEFAULT 'global',
618
+ search_summary TEXT,
619
+ status TEXT NOT NULL DEFAULT 'active'
620
+ CHECK(status IN ('active','archived')),
621
+ l1_embedding BLOB,
622
+ client_name TEXT,
623
+ client_version TEXT,
624
+ source_tool TEXT,
625
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
626
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
627
+ );
628
+
629
+ CREATE INDEX IF NOT EXISTS idx_user_files_status ON user_files(status);
630
+ CREATE INDEX IF NOT EXISTS idx_user_files_scope ON user_files(scope);
631
+ CREATE INDEX IF NOT EXISTS idx_user_files_title ON user_files(title);
278
632
 
279
- -- claim_history: Claim変更履歴
633
+ -- --------------------------------------------------------
634
+ -- claim_history: Claim変更履歴 (V12: created_at 追加)
635
+ -- --------------------------------------------------------
280
636
  CREATE TABLE IF NOT EXISTS claim_history (
281
637
  id TEXT PRIMARY KEY,
282
638
  claim_id TEXT NOT NULL REFERENCES claims(id),
@@ -284,30 +640,16 @@ function applyV1(db) {
284
640
  old_value TEXT,
285
641
  new_value TEXT,
286
642
  reason TEXT NOT NULL,
287
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
643
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
644
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
288
645
  );
289
646
 
290
647
  CREATE INDEX IF NOT EXISTS idx_claim_history_claim_id ON claim_history(claim_id);
291
648
  CREATE INDEX IF NOT EXISTS idx_claim_history_changed ON claim_history(changed_at);
292
649
 
293
- -- スキーマバージョン記録
294
- INSERT INTO schema_version (version) VALUES (1);
295
- `);
296
- }
297
- /**
298
- * V2スキーマ: L2 Core v0対応
299
- * - claims に falsifier カラム追加
300
- * - claim_relations テーブル追加(推論グラフ)
301
- * - claim_evidence テーブル追加(構造化根拠参照)
302
- * - claim_checks テーブル追加(検証ログ)
303
- * 仕様: docs/specs/l2-core-v0.md
304
- */
305
- function applyV2(db) {
306
- db.exec(`
307
- -- claims に反証条件カラム追加
308
- ALTER TABLE claims ADD COLUMN falsifier TEXT;
309
-
310
- -- claim_relations: Claim間の推論関係(正規化)
650
+ -- --------------------------------------------------------
651
+ -- claim_relations: Claim間の推論関係 (V12: updated_at 追加)
652
+ -- --------------------------------------------------------
311
653
  CREATE TABLE IF NOT EXISTS claim_relations (
312
654
  id TEXT PRIMARY KEY,
313
655
  source_claim_id TEXT NOT NULL,
@@ -318,14 +660,17 @@ function applyV2(db) {
318
660
  confidence REAL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
319
661
  reasoning TEXT,
320
662
  source_tool TEXT,
321
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
663
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
664
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
322
665
  );
323
666
 
324
667
  CREATE INDEX IF NOT EXISTS idx_claim_relations_source ON claim_relations(source_claim_id);
325
668
  CREATE INDEX IF NOT EXISTS idx_claim_relations_target ON claim_relations(target_claim_id);
326
669
  CREATE INDEX IF NOT EXISTS idx_claim_relations_type ON claim_relations(relation_type);
327
670
 
328
- -- claim_evidence: 構造化された根拠参照
671
+ -- --------------------------------------------------------
672
+ -- claim_evidence: 構造化された根拠参照 (V12: updated_at 追加)
673
+ -- --------------------------------------------------------
329
674
  CREATE TABLE IF NOT EXISTS claim_evidence (
330
675
  id TEXT PRIMARY KEY,
331
676
  claim_id TEXT NOT NULL REFERENCES claims(id),
@@ -338,13 +683,16 @@ function applyV2(db) {
338
683
  description TEXT,
339
684
  verified_at TEXT,
340
685
  source_tool TEXT,
341
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
686
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
687
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
342
688
  );
343
689
 
344
690
  CREATE INDEX IF NOT EXISTS idx_claim_evidence_claim ON claim_evidence(claim_id);
345
691
  CREATE INDEX IF NOT EXISTS idx_claim_evidence_type ON claim_evidence(evidence_type);
346
692
 
347
- -- claim_checks: 検証ログ
693
+ -- --------------------------------------------------------
694
+ -- claim_checks: 検証ログ (V12: created_at 追加)
695
+ -- --------------------------------------------------------
348
696
  CREATE TABLE IF NOT EXISTS claim_checks (
349
697
  id TEXT PRIMARY KEY,
350
698
  claim_id TEXT NOT NULL REFERENCES claims(id),
@@ -354,41 +702,23 @@ function applyV2(db) {
354
702
  result TEXT NOT NULL CHECK(result IN ('passed','failed','inconclusive','skipped')),
355
703
  details TEXT,
356
704
  source_tool TEXT,
357
- checked_at TEXT NOT NULL DEFAULT (datetime('now'))
705
+ checked_at TEXT NOT NULL DEFAULT (datetime('now')),
706
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
358
707
  );
359
708
 
360
709
  CREATE INDEX IF NOT EXISTS idx_claim_checks_claim ON claim_checks(claim_id);
361
710
  CREATE INDEX IF NOT EXISTS idx_claim_checks_type ON claim_checks(check_type);
362
711
  CREATE INDEX IF NOT EXISTS idx_claim_checks_result ON claim_checks(result);
363
712
 
364
- -- スキーマバージョン記録
365
- INSERT INTO schema_version (version) VALUES (2);
366
- `);
367
- }
368
- /**
369
- * V3スキーマ: Provenance自動付与対応
370
- * - claims に client_name, client_version カラム追加
371
- * - decisions に client_name, client_version カラム追加
372
- * Codexレビュー指摘: source_tool/source_sessionが自己申告値で真正性が担保されない問題への対応
373
- * サーバー側でMCP接続元クライアント情報を自動付与する仕組み
374
- */
375
- function applyV3(db) {
376
- db.exec(`
377
- -- claims にProvenance(来歴)カラム追加
378
- ALTER TABLE claims ADD COLUMN client_name TEXT;
379
- ALTER TABLE claims ADD COLUMN client_version TEXT;
380
-
381
- -- decisions にProvenance(来歴)カラム追加
382
- ALTER TABLE decisions ADD COLUMN client_name TEXT;
383
- ALTER TABLE decisions ADD COLUMN client_version TEXT;
384
-
385
- -- audit_log: 全操作の監査ログ(誰が・何を・なぜ)
713
+ -- --------------------------------------------------------
714
+ -- audit_log: 監査ログ
715
+ -- --------------------------------------------------------
386
716
  CREATE TABLE IF NOT EXISTS audit_log (
387
717
  id TEXT PRIMARY KEY,
388
718
  operation TEXT NOT NULL CHECK(operation IN (
389
- 'create','update','retract','supersede','reverse','obsolete'
719
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
390
720
  )),
391
- entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision')),
721
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model','user_memo','user_plan','user_issue','user_topic','user_file')),
392
722
  entity_id TEXT NOT NULL,
393
723
  summary TEXT NOT NULL,
394
724
  details TEXT,
@@ -396,7 +726,8 @@ function applyV3(db) {
396
726
  client_version TEXT,
397
727
  session_id TEXT,
398
728
  source_tool TEXT,
399
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
729
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
730
+ device_id TEXT
400
731
  );
401
732
 
402
733
  CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
@@ -404,54 +735,752 @@ function applyV3(db) {
404
735
  CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
405
736
  CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id);
406
737
 
407
- -- スキーマバージョン記録
408
- INSERT INTO schema_version (version) VALUES (3);
409
- `);
410
- }
411
- /**
412
- * V4スキーマ: Episodeモデル追加
413
- * ユーザーの思考パターン・意思決定フレームワークを記録する上位構造。
414
- * 原子的事実(Claims)ではなく、文脈→問題→欲求→決定→結果→原則 の流れを格納。
415
- * ChatGPT提案のEpisode構造をベースに、~20件の実データ収集でカラム妥当性を検証する。
416
- */
417
- function applyV4(db) {
418
- db.exec(`
419
- -- audit_log のentity_type CHECK制約を拡張('episode'を追加)
420
- -- SQLiteはALTER TABLE DROP CONSTRAINTをサポートしないため、テーブル再作成で対応
421
- CREATE TABLE IF NOT EXISTS audit_log_new (
738
+ -- --------------------------------------------------------
739
+ -- store_meta: Key-Valueストア
740
+ -- --------------------------------------------------------
741
+ CREATE TABLE IF NOT EXISTS store_meta (
742
+ key TEXT PRIMARY KEY,
743
+ value TEXT NOT NULL,
744
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
745
+ );
746
+
747
+ -- --------------------------------------------------------
748
+ -- tombstones: 削除の同期伝播
749
+ -- --------------------------------------------------------
750
+ CREATE TABLE IF NOT EXISTS tombstones (
422
751
  id TEXT PRIMARY KEY,
423
- operation TEXT NOT NULL CHECK(operation IN (
424
- 'create','update','retract','supersede','reverse','obsolete','archive'
425
- )),
426
- entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode')),
752
+ entity_type TEXT NOT NULL,
427
753
  entity_id TEXT NOT NULL,
428
- summary TEXT NOT NULL,
429
- details TEXT,
430
- client_name TEXT,
431
- client_version TEXT,
432
- session_id TEXT,
433
- source_tool TEXT,
434
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
754
+ deleted_at TEXT NOT NULL,
755
+ UNIQUE(entity_type, entity_id)
435
756
  );
436
757
 
437
- INSERT INTO audit_log_new SELECT * FROM audit_log;
438
- DROP TABLE audit_log;
439
- ALTER TABLE audit_log_new RENAME TO audit_log;
440
-
441
- CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
442
- CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation);
443
- CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
444
- CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id);
758
+ -- --------------------------------------------------------
759
+ -- unified_search_items: 横断検索テーブル
760
+ -- --------------------------------------------------------
761
+ CREATE TABLE IF NOT EXISTS unified_search_items (
762
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','user_memo','user_plan','user_issue','user_topic','user_file')),
763
+ entity_id TEXT NOT NULL,
764
+ scope TEXT NOT NULL DEFAULT 'global',
765
+ category TEXT,
766
+ title_summary TEXT NOT NULL,
767
+ search_text TEXT NOT NULL,
768
+ search_summary TEXT,
769
+ tags TEXT NOT NULL DEFAULT '[]',
770
+ l1_embedding BLOB,
771
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
772
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
773
+ PRIMARY KEY (entity_type, entity_id)
774
+ );
445
775
 
446
- -- episodes: 思考パターン・意思決定エピソード
447
- CREATE TABLE IF NOT EXISTS episodes (
448
- id TEXT PRIMARY KEY,
449
- title TEXT NOT NULL,
450
- context TEXT,
451
- trigger TEXT,
452
- problems TEXT NOT NULL DEFAULT '[]',
453
- desires TEXT NOT NULL DEFAULT '[]',
454
- decisions TEXT NOT NULL DEFAULT '[]',
776
+ CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope);
777
+ CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type);
778
+ CREATE INDEX IF NOT EXISTS idx_unified_search_updated ON unified_search_items(updated_at);
779
+ CREATE INDEX IF NOT EXISTS idx_unified_search_entity_id ON unified_search_items(entity_id);
780
+ `);
781
+ // ================================================================
782
+ // 2. FTS テーブル作成
783
+ // ================================================================
784
+ // --- claims_fts (V28 形式: l2_subject, l2_predicate, l2_object, l2_evidence) ---
785
+ db.exec(`
786
+ CREATE VIRTUAL TABLE IF NOT EXISTS claims_fts USING fts5(
787
+ l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary,
788
+ content='claims', content_rowid='rowid', tokenize='trigram'
789
+ )
790
+ `);
791
+ // --- episodes_fts (V20 形式) ---
792
+ db.exec(`
793
+ CREATE VIRTUAL TABLE IF NOT EXISTS episodes_fts USING fts5(
794
+ title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input,
795
+ content='episodes', content_rowid='rowid', tokenize='trigram'
796
+ )
797
+ `);
798
+ // --- decisions_fts (V20 形式) ---
799
+ db.exec(`
800
+ CREATE VIRTUAL TABLE IF NOT EXISTS decisions_fts USING fts5(
801
+ title, description, l1_content, l2_reasoning, search_summary, user_input,
802
+ content='decisions', content_rowid='rowid', tokenize='trigram'
803
+ )
804
+ `);
805
+ // --- theories_fts (V20 形式) ---
806
+ db.exec(`
807
+ CREATE VIRTUAL TABLE IF NOT EXISTS theories_fts USING fts5(
808
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
809
+ content='theories', content_rowid='rowid', tokenize='trigram'
810
+ )
811
+ `);
812
+ // --- models_fts (同上) ---
813
+ db.exec(`
814
+ CREATE VIRTUAL TABLE IF NOT EXISTS models_fts USING fts5(
815
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
816
+ content='models', content_rowid='rowid', tokenize='trigram'
817
+ )
818
+ `);
819
+ // --- insights_fts (V19 形式) ---
820
+ db.exec(`
821
+ CREATE VIRTUAL TABLE IF NOT EXISTS insights_fts USING fts5(
822
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
823
+ content='insights', content_rowid='rowid', tokenize='trigram'
824
+ )
825
+ `);
826
+ // --- user_memos_fts (V24 形式: l1_content, user_input) ---
827
+ db.exec(`
828
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_memos_fts USING fts5(
829
+ title, l1_content, tags, search_summary, user_input,
830
+ content='user_memos', content_rowid='rowid', tokenize='trigram'
831
+ )
832
+ `);
833
+ // --- user_plans_fts (V24 形式: l1_content) ---
834
+ db.exec(`
835
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_plans_fts USING fts5(
836
+ title, l1_content, tags, search_summary,
837
+ content='user_plans', content_rowid='rowid', tokenize='trigram'
838
+ )
839
+ `);
840
+ // --- user_issues_fts (V24 形式: l1_content) ---
841
+ db.exec(`
842
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_issues_fts USING fts5(
843
+ title, l1_content, entries, tags, search_summary,
844
+ content='user_issues', content_rowid='rowid', tokenize='trigram'
845
+ )
846
+ `);
847
+ // --- user_topics_fts (V29 形式) ---
848
+ db.exec(`
849
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_topics_fts USING fts5(
850
+ title, summary, tags, search_summary,
851
+ content='user_topics', content_rowid='rowid', tokenize='trigram'
852
+ )
853
+ `);
854
+ // --- unified_search_fts (V29 形式) ---
855
+ db.exec(`
856
+ CREATE VIRTUAL TABLE IF NOT EXISTS unified_search_fts USING fts5(
857
+ search_text, title_summary, search_summary,
858
+ content='unified_search_items', content_rowid='rowid', tokenize='trigram'
859
+ )
860
+ `);
861
+ // ================================================================
862
+ // 3. FTS 同期トリガー作成
863
+ // ================================================================
864
+ // --- claims_fts triggers (V28 形式) ---
865
+ db.exec(`
866
+ CREATE TRIGGER claims_fts_insert AFTER INSERT ON claims BEGIN
867
+ INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
868
+ VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
869
+ END
870
+ `);
871
+ db.exec(`
872
+ CREATE TRIGGER claims_fts_update AFTER UPDATE ON claims BEGIN
873
+ INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
874
+ VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
875
+ INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
876
+ VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
877
+ END
878
+ `);
879
+ db.exec(`
880
+ CREATE TRIGGER claims_fts_delete AFTER DELETE ON claims BEGIN
881
+ INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
882
+ VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
883
+ END
884
+ `);
885
+ // --- episodes_fts triggers ---
886
+ db.exec(`
887
+ CREATE TRIGGER episodes_fts_insert AFTER INSERT ON episodes BEGIN
888
+ INSERT INTO episodes_fts(rowid, title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input)
889
+ VALUES (new.rowid, new.title, new.l1_content, new.l2_context, new.l2_trigger,
890
+ ${jc("new.l2_principles")}, ${jc("new.l2_problems")}, ${jc("new.l2_desires")},
891
+ ${jc("new.l2_decisions")}, ${jc("new.l2_outcomes")},
892
+ new.search_summary, new.user_input);
893
+ END
894
+ `);
895
+ db.exec(`
896
+ CREATE TRIGGER episodes_fts_update AFTER UPDATE ON episodes BEGIN
897
+ INSERT INTO episodes_fts(episodes_fts, rowid, title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input)
898
+ VALUES ('delete', old.rowid, old.title, old.l1_content, old.l2_context, old.l2_trigger,
899
+ ${jc("old.l2_principles")}, ${jc("old.l2_problems")}, ${jc("old.l2_desires")},
900
+ ${jc("old.l2_decisions")}, ${jc("old.l2_outcomes")},
901
+ old.search_summary, old.user_input);
902
+ INSERT INTO episodes_fts(rowid, title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input)
903
+ VALUES (new.rowid, new.title, new.l1_content, new.l2_context, new.l2_trigger,
904
+ ${jc("new.l2_principles")}, ${jc("new.l2_problems")}, ${jc("new.l2_desires")},
905
+ ${jc("new.l2_decisions")}, ${jc("new.l2_outcomes")},
906
+ new.search_summary, new.user_input);
907
+ END
908
+ `);
909
+ db.exec(`
910
+ CREATE TRIGGER episodes_fts_delete AFTER DELETE ON episodes BEGIN
911
+ INSERT INTO episodes_fts(episodes_fts, rowid, title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input)
912
+ VALUES ('delete', old.rowid, old.title, old.l1_content, old.l2_context, old.l2_trigger,
913
+ ${jc("old.l2_principles")}, ${jc("old.l2_problems")}, ${jc("old.l2_desires")},
914
+ ${jc("old.l2_decisions")}, ${jc("old.l2_outcomes")},
915
+ old.search_summary, old.user_input);
916
+ END
917
+ `);
918
+ // --- decisions_fts triggers ---
919
+ db.exec(`
920
+ CREATE TRIGGER decisions_fts_insert AFTER INSERT ON decisions BEGIN
921
+ INSERT INTO decisions_fts(rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
922
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_reasoning, new.search_summary, new.user_input);
923
+ END
924
+ `);
925
+ db.exec(`
926
+ CREATE TRIGGER decisions_fts_update AFTER UPDATE ON decisions BEGIN
927
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
928
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_reasoning, old.search_summary, old.user_input);
929
+ INSERT INTO decisions_fts(rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
930
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_reasoning, new.search_summary, new.user_input);
931
+ END
932
+ `);
933
+ db.exec(`
934
+ CREATE TRIGGER decisions_fts_delete AFTER DELETE ON decisions BEGIN
935
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
936
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_reasoning, old.search_summary, old.user_input);
937
+ END
938
+ `);
939
+ // --- theories_fts triggers ---
940
+ db.exec(`
941
+ CREATE TRIGGER theories_fts_insert AFTER INSERT ON theories BEGIN
942
+ INSERT INTO theories_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
943
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
944
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
945
+ END
946
+ `);
947
+ db.exec(`
948
+ CREATE TRIGGER theories_fts_update AFTER UPDATE ON theories BEGIN
949
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
950
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
951
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
952
+ INSERT INTO theories_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
953
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
954
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
955
+ END
956
+ `);
957
+ db.exec(`
958
+ CREATE TRIGGER theories_fts_delete AFTER DELETE ON theories BEGIN
959
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
960
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
961
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
962
+ END
963
+ `);
964
+ // --- models_fts triggers ---
965
+ db.exec(`
966
+ CREATE TRIGGER models_fts_insert AFTER INSERT ON models BEGIN
967
+ INSERT INTO models_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
968
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
969
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
970
+ END
971
+ `);
972
+ db.exec(`
973
+ CREATE TRIGGER models_fts_update AFTER UPDATE ON models BEGIN
974
+ INSERT INTO models_fts(models_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
975
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
976
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
977
+ INSERT INTO models_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
978
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
979
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
980
+ END
981
+ `);
982
+ db.exec(`
983
+ CREATE TRIGGER models_fts_delete AFTER DELETE ON models BEGIN
984
+ INSERT INTO models_fts(models_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
985
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
986
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
987
+ END
988
+ `);
989
+ // --- insights_fts triggers ---
990
+ db.exec(`
991
+ CREATE TRIGGER insights_fts_insert AFTER INSERT ON insights BEGIN
992
+ INSERT INTO insights_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
993
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
994
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
995
+ END
996
+ `);
997
+ db.exec(`
998
+ CREATE TRIGGER insights_fts_update AFTER UPDATE ON insights BEGIN
999
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
1000
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
1001
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
1002
+ INSERT INTO insights_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
1003
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
1004
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
1005
+ END
1006
+ `);
1007
+ db.exec(`
1008
+ CREATE TRIGGER insights_fts_delete AFTER DELETE ON insights BEGIN
1009
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
1010
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
1011
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
1012
+ END
1013
+ `);
1014
+ // --- user_memos_fts triggers (V24 形式: l1_content, user_input) ---
1015
+ db.exec(`
1016
+ CREATE TRIGGER user_memos_fts_insert AFTER INSERT ON user_memos BEGIN
1017
+ INSERT INTO user_memos_fts(rowid, title, l1_content, tags, search_summary, user_input)
1018
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary, new.user_input);
1019
+ END
1020
+ `);
1021
+ db.exec(`
1022
+ CREATE TRIGGER user_memos_fts_update AFTER UPDATE ON user_memos BEGIN
1023
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, l1_content, tags, search_summary, user_input)
1024
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary, old.user_input);
1025
+ INSERT INTO user_memos_fts(rowid, title, l1_content, tags, search_summary, user_input)
1026
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary, new.user_input);
1027
+ END
1028
+ `);
1029
+ db.exec(`
1030
+ CREATE TRIGGER user_memos_fts_delete AFTER DELETE ON user_memos BEGIN
1031
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, l1_content, tags, search_summary, user_input)
1032
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary, old.user_input);
1033
+ END
1034
+ `);
1035
+ // --- user_plans_fts triggers (V24 形式: l1_content) ---
1036
+ db.exec(`
1037
+ CREATE TRIGGER user_plans_fts_insert AFTER INSERT ON user_plans BEGIN
1038
+ INSERT INTO user_plans_fts(rowid, title, l1_content, tags, search_summary)
1039
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary);
1040
+ END
1041
+ `);
1042
+ db.exec(`
1043
+ CREATE TRIGGER user_plans_fts_update AFTER UPDATE ON user_plans BEGIN
1044
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, l1_content, tags, search_summary)
1045
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary);
1046
+ INSERT INTO user_plans_fts(rowid, title, l1_content, tags, search_summary)
1047
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary);
1048
+ END
1049
+ `);
1050
+ db.exec(`
1051
+ CREATE TRIGGER user_plans_fts_delete AFTER DELETE ON user_plans BEGIN
1052
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, l1_content, tags, search_summary)
1053
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary);
1054
+ END
1055
+ `);
1056
+ // --- user_issues_fts triggers (V24 形式: l1_content) ---
1057
+ db.exec(`
1058
+ CREATE TRIGGER user_issues_fts_insert AFTER INSERT ON user_issues BEGIN
1059
+ INSERT INTO user_issues_fts(rowid, title, l1_content, entries, tags, search_summary)
1060
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
1061
+ END
1062
+ `);
1063
+ db.exec(`
1064
+ CREATE TRIGGER user_issues_fts_update AFTER UPDATE ON user_issues BEGIN
1065
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, l1_content, entries, tags, search_summary)
1066
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
1067
+ INSERT INTO user_issues_fts(rowid, title, l1_content, entries, tags, search_summary)
1068
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
1069
+ END
1070
+ `);
1071
+ db.exec(`
1072
+ CREATE TRIGGER user_issues_fts_delete AFTER DELETE ON user_issues BEGIN
1073
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, l1_content, entries, tags, search_summary)
1074
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
1075
+ END
1076
+ `);
1077
+ // --- user_topics_fts triggers (V29 形式: REPLACE パターン) ---
1078
+ db.exec(`
1079
+ CREATE TRIGGER user_topics_fts_insert AFTER INSERT ON user_topics BEGIN
1080
+ INSERT INTO user_topics_fts(rowid, title, summary, tags, search_summary)
1081
+ VALUES (new.rowid, new.title, new.summary, ${jcTopics("new.tags")}, new.search_summary);
1082
+ END
1083
+ `);
1084
+ db.exec(`
1085
+ CREATE TRIGGER user_topics_fts_update AFTER UPDATE ON user_topics BEGIN
1086
+ INSERT INTO user_topics_fts(user_topics_fts, rowid, title, summary, tags, search_summary)
1087
+ VALUES ('delete', old.rowid, old.title, old.summary, ${jcTopics("old.tags")}, old.search_summary);
1088
+ INSERT INTO user_topics_fts(rowid, title, summary, tags, search_summary)
1089
+ VALUES (new.rowid, new.title, new.summary, ${jcTopics("new.tags")}, new.search_summary);
1090
+ END
1091
+ `);
1092
+ db.exec(`
1093
+ CREATE TRIGGER user_topics_fts_delete AFTER DELETE ON user_topics BEGIN
1094
+ INSERT INTO user_topics_fts(user_topics_fts, rowid, title, summary, tags, search_summary)
1095
+ VALUES ('delete', old.rowid, old.title, old.summary, ${jcTopics("old.tags")}, old.search_summary);
1096
+ END
1097
+ `);
1098
+ // --- unified_search_fts triggers ---
1099
+ db.exec(`
1100
+ CREATE TRIGGER unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
1101
+ INSERT INTO unified_search_fts(rowid, search_text, title_summary, search_summary)
1102
+ VALUES (new.rowid, new.search_text, new.title_summary, new.search_summary);
1103
+ END
1104
+ `);
1105
+ db.exec(`
1106
+ CREATE TRIGGER unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
1107
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, search_text, title_summary, search_summary)
1108
+ VALUES ('delete', old.rowid, old.search_text, old.title_summary, old.search_summary);
1109
+ INSERT INTO unified_search_fts(rowid, search_text, title_summary, search_summary)
1110
+ VALUES (new.rowid, new.search_text, new.title_summary, new.search_summary);
1111
+ END
1112
+ `);
1113
+ db.exec(`
1114
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
1115
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, search_text, title_summary, search_summary)
1116
+ VALUES ('delete', old.rowid, old.search_text, old.title_summary, old.search_summary);
1117
+ END
1118
+ `);
1119
+ // ================================================================
1120
+ // 4. unified_search_items の source table sync triggers (V29 形式)
1121
+ // ================================================================
1122
+ // --- claims (V28 形式: l2_evidence/l2_falsifier) ---
1123
+ const claimTitle = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object`;
1124
+ const claimSearch = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal(`${p}l2_evidence`)} || ' ' || ${coal(`${p}l2_falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`;
1125
+ db.exec(`
1126
+ CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims WHEN new.status = 'active' BEGIN
1127
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1128
+ VALUES ('claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
1129
+ END
1130
+ `);
1131
+ db.exec(`
1132
+ CREATE TRIGGER claims_unified_update AFTER UPDATE ON claims BEGIN
1133
+ DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
1134
+ INSERT INTO unified_search_items(${columns})
1135
+ SELECT 'claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at WHERE new.status = 'active';
1136
+ END
1137
+ `);
1138
+ db.exec(`
1139
+ CREATE TRIGGER claims_unified_delete AFTER DELETE ON claims BEGIN
1140
+ DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
1141
+ END
1142
+ `);
1143
+ // --- episodes ---
1144
+ db.exec(`
1145
+ CREATE TRIGGER episodes_unified_insert AFTER INSERT ON episodes WHEN new.status = 'active' BEGIN
1146
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1147
+ VALUES ('episode', new.id, new.scope, NULL, new.title, new.title || ' ' || ${coal("new.l1_content")} || ' ' || ${coal("new.l2_context")} || ' ' || ${coal("new.l2_trigger")} || ' ' || ${jc("new.l2_problems")} || ' ' || ${jc("new.l2_outcomes")} || ' ' || ${jc("new.l2_principles")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1148
+ END
1149
+ `);
1150
+ db.exec(`
1151
+ CREATE TRIGGER episodes_unified_update AFTER UPDATE ON episodes BEGIN
1152
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
1153
+ INSERT INTO unified_search_items(${columns})
1154
+ SELECT 'episode', new.id, new.scope, NULL, new.title, new.title || ' ' || ${coal("new.l1_content")} || ' ' || ${coal("new.l2_context")} || ' ' || ${coal("new.l2_trigger")} || ' ' || ${jc("new.l2_problems")} || ' ' || ${jc("new.l2_outcomes")} || ' ' || ${jc("new.l2_principles")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active';
1155
+ END
1156
+ `);
1157
+ db.exec(`
1158
+ CREATE TRIGGER episodes_unified_delete AFTER DELETE ON episodes BEGIN
1159
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
1160
+ END
1161
+ `);
1162
+ // --- decisions ---
1163
+ db.exec(`
1164
+ CREATE TRIGGER decisions_unified_insert AFTER INSERT ON decisions WHEN new.status = 'active' BEGIN
1165
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1166
+ VALUES ('decision', new.id, new.scope, NULL, new.title, new.title || ' ' || new.description || ' ' || ${coal("new.l1_content")} || ' ' || new.l2_reasoning || ' ' || ${jc("new.l2_alternatives")} || ' ' || ${coal("new.search_summary")}, new.search_summary, '[]', new.created_at, new.updated_at);
1167
+ END
1168
+ `);
1169
+ db.exec(`
1170
+ CREATE TRIGGER decisions_unified_update AFTER UPDATE ON decisions BEGIN
1171
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
1172
+ INSERT INTO unified_search_items(${columns})
1173
+ SELECT 'decision', new.id, new.scope, NULL, new.title, new.title || ' ' || new.description || ' ' || ${coal("new.l1_content")} || ' ' || new.l2_reasoning || ' ' || ${jc("new.l2_alternatives")} || ' ' || ${coal("new.search_summary")}, new.search_summary, '[]', new.created_at, new.updated_at WHERE new.status = 'active';
1174
+ END
1175
+ `);
1176
+ db.exec(`
1177
+ CREATE TRIGGER decisions_unified_delete AFTER DELETE ON decisions BEGIN
1178
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
1179
+ END
1180
+ `);
1181
+ // --- theories, insights, models: 共通パターン ---
1182
+ for (const e of [
1183
+ { type: "theory", table: "theories" },
1184
+ { type: "insight", table: "insights" },
1185
+ { type: "model", table: "models" },
1186
+ ]) {
1187
+ const searchExpr = (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}l2_core_thesis`)} || ' ' || ${jc(`${p}l2_principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`;
1188
+ db.exec(`
1189
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table} WHEN new.status = 'active' BEGIN
1190
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1191
+ VALUES ('${e.type}', new.id, new.scope, NULL, new.title, ${searchExpr("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1192
+ END
1193
+ `);
1194
+ db.exec(`
1195
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
1196
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
1197
+ INSERT INTO unified_search_items(${columns})
1198
+ SELECT '${e.type}', new.id, new.scope, NULL, new.title, ${searchExpr("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active';
1199
+ END
1200
+ `);
1201
+ db.exec(`
1202
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
1203
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
1204
+ END
1205
+ `);
1206
+ }
1207
+ // --- user_memos (V24 形式: l1_content, user_input) ---
1208
+ db.exec(`
1209
+ CREATE TRIGGER user_memos_unified_insert AFTER INSERT ON user_memos
1210
+ WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN
1211
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1212
+ VALUES ('user_memo', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${coal("new.user_input")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1213
+ END
1214
+ `);
1215
+ db.exec(`
1216
+ CREATE TRIGGER user_memos_unified_update AFTER UPDATE ON user_memos BEGIN
1217
+ DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id;
1218
+ INSERT INTO unified_search_items(${columns})
1219
+ SELECT 'user_memo', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${coal("new.user_input")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
1220
+ WHERE new.status = 'active' AND new.usage_policy != 'human_directed';
1221
+ END
1222
+ `);
1223
+ db.exec(`
1224
+ CREATE TRIGGER user_memos_unified_delete AFTER DELETE ON user_memos BEGIN
1225
+ DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id;
1226
+ END
1227
+ `);
1228
+ // --- user_plans (V24 形式: l1_content) ---
1229
+ db.exec(`
1230
+ CREATE TRIGGER user_plans_unified_insert AFTER INSERT ON user_plans
1231
+ WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN
1232
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1233
+ VALUES ('user_plan', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1234
+ END
1235
+ `);
1236
+ db.exec(`
1237
+ CREATE TRIGGER user_plans_unified_update AFTER UPDATE ON user_plans BEGIN
1238
+ DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id;
1239
+ INSERT INTO unified_search_items(${columns})
1240
+ SELECT 'user_plan', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
1241
+ WHERE new.status = 'active' AND new.usage_policy != 'human_directed';
1242
+ END
1243
+ `);
1244
+ db.exec(`
1245
+ CREATE TRIGGER user_plans_unified_delete AFTER DELETE ON user_plans BEGIN
1246
+ DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id;
1247
+ END
1248
+ `);
1249
+ // --- user_issues (V24 形式: l1_content, entries) ---
1250
+ db.exec(`
1251
+ CREATE TRIGGER user_issues_unified_insert AFTER INSERT ON user_issues WHEN new.status = 'open' BEGIN
1252
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1253
+ VALUES ('user_issue', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.entries")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1254
+ END
1255
+ `);
1256
+ db.exec(`
1257
+ CREATE TRIGGER user_issues_unified_update AFTER UPDATE ON user_issues BEGIN
1258
+ DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id;
1259
+ INSERT INTO unified_search_items(${columns})
1260
+ SELECT 'user_issue', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.entries")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
1261
+ WHERE new.status = 'open';
1262
+ END
1263
+ `);
1264
+ db.exec(`
1265
+ CREATE TRIGGER user_issues_unified_delete AFTER DELETE ON user_issues BEGIN
1266
+ DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id;
1267
+ END
1268
+ `);
1269
+ // --- user_topics (V29 形式) ---
1270
+ db.exec(`
1271
+ CREATE TRIGGER user_topics_unified_insert AFTER INSERT ON user_topics WHEN new.status = 'active' BEGIN
1272
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1273
+ VALUES ('user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || new.summary || ' ' || ${jcTopics("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1274
+ END
1275
+ `);
1276
+ db.exec(`
1277
+ CREATE TRIGGER user_topics_unified_update AFTER UPDATE ON user_topics BEGIN
1278
+ DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id;
1279
+ INSERT INTO unified_search_items(${columns})
1280
+ SELECT 'user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || new.summary || ' ' || ${jcTopics("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
1281
+ WHERE new.status = 'active';
1282
+ END
1283
+ `);
1284
+ db.exec(`
1285
+ CREATE TRIGGER user_topics_unified_delete AFTER DELETE ON user_topics BEGIN
1286
+ DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id;
1287
+ END
1288
+ `);
1289
+ // --- user_files (V31 File Vault) ---
1290
+ db.exec(`
1291
+ CREATE TRIGGER user_files_unified_insert AFTER INSERT ON user_files WHEN new.status = 'active' BEGIN
1292
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1293
+ VALUES ('user_file', new.id, new.scope, NULL, new.title, new.title || ' ' || COALESCE(new.description, '') || ' ' || new.original_filename || ' ' || ${jcTopics("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
1294
+ END
1295
+ `);
1296
+ db.exec(`
1297
+ CREATE TRIGGER user_files_unified_update AFTER UPDATE ON user_files BEGIN
1298
+ DELETE FROM unified_search_items WHERE entity_type = 'user_file' AND entity_id = old.id;
1299
+ INSERT INTO unified_search_items(${columns})
1300
+ SELECT 'user_file', new.id, new.scope, NULL, new.title, new.title || ' ' || COALESCE(new.description, '') || ' ' || new.original_filename || ' ' || ${jcTopics("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
1301
+ WHERE new.status = 'active';
1302
+ END
1303
+ `);
1304
+ db.exec(`
1305
+ CREATE TRIGGER user_files_unified_delete AFTER DELETE ON user_files BEGIN
1306
+ DELETE FROM unified_search_items WHERE entity_type = 'user_file' AND entity_id = old.id;
1307
+ END
1308
+ `);
1309
+ // ================================================================
1310
+ // 5. store_meta 初期値
1311
+ // ================================================================
1312
+ const deviceId = generateClientId();
1313
+ db.prepare("INSERT OR IGNORE INTO store_meta (key, value) VALUES ('device_id', ?)").run(deviceId);
1314
+ db.prepare("INSERT OR IGNORE INTO store_meta (key, value) VALUES ('last_synced_at', '')").run();
1315
+ // ================================================================
1316
+ // 6. schema_version 全バージョン記録(V2〜V31 をスキップさせる)
1317
+ // ================================================================
1318
+ db.exec(`
1319
+ INSERT INTO schema_version (version) VALUES
1320
+ (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),
1321
+ (11),(12),(13),(14),(15),(16),(17),(18),(19),(20),
1322
+ (21),(22),(23),(24),(25),(26),(27),(28),(29),(30),
1323
+ (31);
1324
+ `);
1325
+ }
1326
+ /**
1327
+ * V2スキーマ: L2 Core v0対応
1328
+ * - claims に falsifier カラム追加
1329
+ * - claim_relations テーブル追加(推論グラフ)
1330
+ * - claim_evidence テーブル追加(構造化根拠参照)
1331
+ * - claim_checks テーブル追加(検証ログ)
1332
+ * 仕様: docs/specs/l2-core-v0.md
1333
+ */
1334
+ function applyV2(db) {
1335
+ db.exec(`
1336
+ -- claims に反証条件カラム追加
1337
+ ALTER TABLE claims ADD COLUMN falsifier TEXT;
1338
+
1339
+ -- claim_relations: Claim間の推論関係(正規化)
1340
+ CREATE TABLE IF NOT EXISTS claim_relations (
1341
+ id TEXT PRIMARY KEY,
1342
+ source_claim_id TEXT NOT NULL,
1343
+ target_claim_id TEXT NOT NULL,
1344
+ relation_type TEXT NOT NULL CHECK(relation_type IN (
1345
+ 'supports','contradicts','derives','induces','analogizes','supersedes','depends_on'
1346
+ )),
1347
+ confidence REAL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
1348
+ reasoning TEXT,
1349
+ source_tool TEXT,
1350
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1351
+ );
1352
+
1353
+ CREATE INDEX IF NOT EXISTS idx_claim_relations_source ON claim_relations(source_claim_id);
1354
+ CREATE INDEX IF NOT EXISTS idx_claim_relations_target ON claim_relations(target_claim_id);
1355
+ CREATE INDEX IF NOT EXISTS idx_claim_relations_type ON claim_relations(relation_type);
1356
+
1357
+ -- claim_evidence: 構造化された根拠参照
1358
+ CREATE TABLE IF NOT EXISTS claim_evidence (
1359
+ id TEXT PRIMARY KEY,
1360
+ claim_id TEXT NOT NULL REFERENCES claims(id),
1361
+ evidence_type TEXT NOT NULL CHECK(evidence_type IN (
1362
+ 'url','file','claim','decision','session_log','user_statement','external'
1363
+ )),
1364
+ uri TEXT,
1365
+ span TEXT,
1366
+ content_hash TEXT,
1367
+ description TEXT,
1368
+ verified_at TEXT,
1369
+ source_tool TEXT,
1370
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1371
+ );
1372
+
1373
+ CREATE INDEX IF NOT EXISTS idx_claim_evidence_claim ON claim_evidence(claim_id);
1374
+ CREATE INDEX IF NOT EXISTS idx_claim_evidence_type ON claim_evidence(evidence_type);
1375
+
1376
+ -- claim_checks: 検証ログ
1377
+ CREATE TABLE IF NOT EXISTS claim_checks (
1378
+ id TEXT PRIMARY KEY,
1379
+ claim_id TEXT NOT NULL REFERENCES claims(id),
1380
+ check_type TEXT NOT NULL CHECK(check_type IN (
1381
+ 'fact_check','consistency','counter_example','source_verify','user_confirm','auto_expire','falsifier_eval'
1382
+ )),
1383
+ result TEXT NOT NULL CHECK(result IN ('passed','failed','inconclusive','skipped')),
1384
+ details TEXT,
1385
+ source_tool TEXT,
1386
+ checked_at TEXT NOT NULL DEFAULT (datetime('now'))
1387
+ );
1388
+
1389
+ CREATE INDEX IF NOT EXISTS idx_claim_checks_claim ON claim_checks(claim_id);
1390
+ CREATE INDEX IF NOT EXISTS idx_claim_checks_type ON claim_checks(check_type);
1391
+ CREATE INDEX IF NOT EXISTS idx_claim_checks_result ON claim_checks(result);
1392
+
1393
+ -- スキーマバージョン記録
1394
+ INSERT INTO schema_version (version) VALUES (2);
1395
+ `);
1396
+ }
1397
+ /**
1398
+ * V3スキーマ: Provenance自動付与対応
1399
+ * - claims に client_name, client_version カラム追加
1400
+ * - decisions に client_name, client_version カラム追加
1401
+ * Codexレビュー指摘: source_tool/source_sessionが自己申告値で真正性が担保されない問題への対応
1402
+ * サーバー側でMCP接続元クライアント情報を自動付与する仕組み
1403
+ */
1404
+ function applyV3(db) {
1405
+ db.exec(`
1406
+ -- claims にProvenance(来歴)カラム追加
1407
+ ALTER TABLE claims ADD COLUMN client_name TEXT;
1408
+ ALTER TABLE claims ADD COLUMN client_version TEXT;
1409
+
1410
+ -- decisions にProvenance(来歴)カラム追加
1411
+ ALTER TABLE decisions ADD COLUMN client_name TEXT;
1412
+ ALTER TABLE decisions ADD COLUMN client_version TEXT;
1413
+
1414
+ -- audit_log: 全操作の監査ログ(誰が・何を・なぜ)
1415
+ CREATE TABLE IF NOT EXISTS audit_log (
1416
+ id TEXT PRIMARY KEY,
1417
+ operation TEXT NOT NULL CHECK(operation IN (
1418
+ 'create','update','retract','supersede','reverse','obsolete'
1419
+ )),
1420
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision')),
1421
+ entity_id TEXT NOT NULL,
1422
+ summary TEXT NOT NULL,
1423
+ details TEXT,
1424
+ client_name TEXT,
1425
+ client_version TEXT,
1426
+ session_id TEXT,
1427
+ source_tool TEXT,
1428
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1429
+ );
1430
+
1431
+ CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
1432
+ CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation);
1433
+ CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
1434
+ CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id);
1435
+
1436
+ -- スキーマバージョン記録
1437
+ INSERT INTO schema_version (version) VALUES (3);
1438
+ `);
1439
+ }
1440
+ /**
1441
+ * V4スキーマ: Episodeモデル追加
1442
+ * ユーザーの思考パターン・意思決定フレームワークを記録する上位構造。
1443
+ * 原子的事実(Claims)ではなく、文脈→問題→欲求→決定→結果→原則 の流れを格納。
1444
+ * ChatGPT提案のEpisode構造をベースに、~20件の実データ収集でカラム妥当性を検証する。
1445
+ */
1446
+ function applyV4(db) {
1447
+ db.exec(`
1448
+ -- audit_log のentity_type CHECK制約を拡張('episode'を追加)
1449
+ -- SQLiteはALTER TABLE DROP CONSTRAINTをサポートしないため、テーブル再作成で対応
1450
+ CREATE TABLE IF NOT EXISTS audit_log_new (
1451
+ id TEXT PRIMARY KEY,
1452
+ operation TEXT NOT NULL CHECK(operation IN (
1453
+ 'create','update','retract','supersede','reverse','obsolete','archive'
1454
+ )),
1455
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode')),
1456
+ entity_id TEXT NOT NULL,
1457
+ summary TEXT NOT NULL,
1458
+ details TEXT,
1459
+ client_name TEXT,
1460
+ client_version TEXT,
1461
+ session_id TEXT,
1462
+ source_tool TEXT,
1463
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1464
+ );
1465
+
1466
+ INSERT INTO audit_log_new SELECT * FROM audit_log;
1467
+ DROP TABLE audit_log;
1468
+ ALTER TABLE audit_log_new RENAME TO audit_log;
1469
+
1470
+ CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
1471
+ CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation);
1472
+ CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
1473
+ CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id);
1474
+
1475
+ -- episodes: 思考パターン・意思決定エピソード
1476
+ CREATE TABLE IF NOT EXISTS episodes (
1477
+ id TEXT PRIMARY KEY,
1478
+ title TEXT NOT NULL,
1479
+ context TEXT,
1480
+ trigger TEXT,
1481
+ problems TEXT NOT NULL DEFAULT '[]',
1482
+ desires TEXT NOT NULL DEFAULT '[]',
1483
+ decisions TEXT NOT NULL DEFAULT '[]',
455
1484
  outcomes TEXT NOT NULL DEFAULT '[]',
456
1485
  principles TEXT NOT NULL DEFAULT '[]',
457
1486
  evidence_refs TEXT NOT NULL DEFAULT '[]',
@@ -569,7 +1598,7 @@ function applyV5(db) {
569
1598
  /**
570
1599
  * V6スキーマ: store_meta テーブル追加(バトンリレー用)
571
1600
  * デバイス間でDBを引き継ぐためのメタデータ管理。
572
- * client_id でデバイスを識別し、last_synced_at で最終同期日時を追跡する。
1601
+ * device_id でデバイスを識別し、last_synced_at で最終同期日時を追跡する。
573
1602
  */
574
1603
  function applyV6(db) {
575
1604
  db.exec(`
@@ -583,9 +1612,9 @@ function applyV6(db) {
583
1612
  -- スキーマバージョン記録
584
1613
  INSERT INTO schema_version (version) VALUES (6);
585
1614
  `);
586
- // client_id を初期生成(UUIDv4相当のランダムID)
587
- const clientId = generateClientId();
588
- db.prepare("INSERT OR IGNORE INTO store_meta (key, value) VALUES ('client_id', ?)").run(clientId);
1615
+ // device_id を初期生成(UUIDv4相当のランダムID)
1616
+ const deviceId = generateClientId();
1617
+ db.prepare("INSERT OR IGNORE INTO store_meta (key, value) VALUES ('device_id', ?)").run(deviceId);
589
1618
  db.prepare("INSERT OR IGNORE INTO store_meta (key, value) VALUES ('last_synced_at', '')").run();
590
1619
  }
591
1620
  /**
@@ -4004,21 +5033,25 @@ function applyV20(db) {
4004
5033
  }
4005
5034
  }
4006
5035
  /**
4007
- * V21: audit_log に client_id カラム追加 + 既存レコードの backfill
5036
+ * V21: audit_log に device_id カラム追加 + 既存レコードの backfill
4008
5037
  * どのデバイスで作られたレコードか追跡するため。
4009
5038
  * sync で複数デバイスのデータが混在する前に backfill することで、
4010
5039
  * 既存データにもデバイス情報を付与する。
4011
5040
  */
4012
5041
  function applyV21(db) {
4013
- // audit_log に client_id カラムを追加
5042
+ // audit_log に device_id カラムを追加
5043
+ // 注: 既存DBとの互換性のためカラム名は client_id のまま ALTER する
5044
+ // V29 で audit_log 再作成時に device_id にリネームされる
4014
5045
  if (!hasColumn(db, "audit_log", "client_id")) {
4015
5046
  db.exec(`ALTER TABLE audit_log ADD COLUMN client_id TEXT`);
4016
5047
  }
4017
- // 既存レコードに現在のデバイスの client_id を backfill
5048
+ // 既存レコードに現在のデバイスの device_id を backfill
4018
5049
  // (まだマージされていない = すべて自分のデバイスで作ったレコード)
4019
- const clientIdRow = db.prepare("SELECT value FROM store_meta WHERE key = 'client_id'").get();
4020
- if (clientIdRow) {
4021
- db.prepare("UPDATE audit_log SET client_id = ? WHERE client_id IS NULL").run(clientIdRow.value);
5050
+ // 注: V6 'client_id' キーで保存した既存DBのためフォールバック読み取り
5051
+ const deviceIdRow = (db.prepare("SELECT value FROM store_meta WHERE key = 'device_id'").get() ??
5052
+ db.prepare("SELECT value FROM store_meta WHERE key = 'client_id'").get());
5053
+ if (deviceIdRow) {
5054
+ db.prepare("UPDATE audit_log SET client_id = ? WHERE client_id IS NULL").run(deviceIdRow.value);
4022
5055
  }
4023
5056
  db.exec(`INSERT INTO schema_version (version) VALUES (21)`);
4024
5057
  }
@@ -5549,15 +6582,305 @@ function applyV27(db) {
5549
6582
 
5550
6583
  INSERT INTO schema_version (version) VALUES (27);
5551
6584
  `);
5552
- // unified_search_items triggers for claims(新カラム名)
5553
- const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
6585
+ // unified_search_items triggers for claims(新カラム名)
6586
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
6587
+ const coal = (col) => `COALESCE(${col}, '')`;
6588
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
6589
+ const claimTitle = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object`;
6590
+ const claimSearch = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`;
6591
+ db.exec(`
6592
+ CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims
6593
+ WHEN new.status = 'active' BEGIN
6594
+ INSERT OR REPLACE INTO unified_search_items(${columns})
6595
+ VALUES ('claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
6596
+ END
6597
+ `);
6598
+ db.exec(`
6599
+ CREATE TRIGGER claims_unified_update AFTER UPDATE ON claims BEGIN
6600
+ DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
6601
+ INSERT INTO unified_search_items(${columns})
6602
+ SELECT 'claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at
6603
+ WHERE new.status = 'active';
6604
+ END
6605
+ `);
6606
+ db.exec(`
6607
+ CREATE TRIGGER claims_unified_delete AFTER DELETE ON claims BEGIN
6608
+ DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
6609
+ END
6610
+ `);
6611
+ }
6612
+ /**
6613
+ * V28: evidence → l2_evidence, falsifier → l2_falsifier リネーム + claims_fts 再構築
6614
+ * 冪等: 既に l2_evidence が存在する場合はリネームをスキップ
6615
+ */
6616
+ function applyV28(db) {
6617
+ // カラムリネーム(冪等: 既にリネーム済みならスキップ)
6618
+ if (hasColumn(db, "claims", "evidence") && !hasColumn(db, "claims", "l2_evidence")) {
6619
+ db.exec(`ALTER TABLE claims RENAME COLUMN evidence TO l2_evidence`);
6620
+ }
6621
+ if (hasColumn(db, "claims", "falsifier") && !hasColumn(db, "claims", "l2_falsifier")) {
6622
+ db.exec(`ALTER TABLE claims RENAME COLUMN falsifier TO l2_falsifier`);
6623
+ }
6624
+ // claims_fts を再構築(l2_evidence/l2_falsifier 対応)
6625
+ db.exec(`
6626
+ DROP TRIGGER IF EXISTS claims_fts_insert;
6627
+ DROP TRIGGER IF EXISTS claims_fts_update;
6628
+ DROP TRIGGER IF EXISTS claims_fts_delete;
6629
+ DROP TABLE IF EXISTS claims_fts;
6630
+
6631
+ CREATE VIRTUAL TABLE claims_fts USING fts5(
6632
+ l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary,
6633
+ content='claims', content_rowid='rowid', tokenize='trigram'
6634
+ );
6635
+
6636
+ -- FTS同期トリガー: INSERT
6637
+ CREATE TRIGGER claims_fts_insert AFTER INSERT ON claims BEGIN
6638
+ INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
6639
+ VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
6640
+ END;
6641
+
6642
+ -- FTS同期トリガー: UPDATE
6643
+ CREATE TRIGGER claims_fts_update AFTER UPDATE ON claims BEGIN
6644
+ INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
6645
+ VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
6646
+ INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
6647
+ VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
6648
+ END;
6649
+
6650
+ -- FTS同期トリガー: DELETE
6651
+ CREATE TRIGGER claims_fts_delete AFTER DELETE ON claims BEGIN
6652
+ INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
6653
+ VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
6654
+ END;
6655
+
6656
+ -- FTS rebuild
6657
+ INSERT INTO claims_fts(claims_fts) VALUES('rebuild');
6658
+
6659
+ INSERT INTO schema_version (version) VALUES (28);
6660
+ `);
6661
+ // unified_search_items の claims トリガー再作成(l2_evidence/l2_falsifier 対応)
6662
+ const coal = (col) => `COALESCE(${col}, '')`;
6663
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
6664
+ const claimTitle = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object`;
6665
+ const claimSearch = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal(`${p}l2_evidence`)} || ' ' || ${coal(`${p}l2_falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`;
6666
+ db.exec(`DROP TRIGGER IF EXISTS claims_unified_insert`);
6667
+ db.exec(`DROP TRIGGER IF EXISTS claims_unified_update`);
6668
+ db.exec(`DROP TRIGGER IF EXISTS claims_unified_delete`);
6669
+ db.exec(`
6670
+ CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims
6671
+ WHEN new.status = 'active' BEGIN
6672
+ INSERT OR REPLACE INTO unified_search_items(${columns})
6673
+ VALUES ('claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
6674
+ END
6675
+ `);
6676
+ db.exec(`
6677
+ CREATE TRIGGER claims_unified_update AFTER UPDATE ON claims BEGIN
6678
+ DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
6679
+ INSERT INTO unified_search_items(${columns})
6680
+ SELECT 'claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at
6681
+ WHERE new.status = 'active';
6682
+ END
6683
+ `);
6684
+ db.exec(`
6685
+ CREATE TRIGGER claims_unified_delete AFTER DELETE ON claims BEGIN
6686
+ DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
6687
+ END
6688
+ `);
6689
+ }
6690
+ /**
6691
+ * V29: user_topics テーブル新設(話題管理)
6692
+ * セッション横断で議論トピックを追跡する
6693
+ */
6694
+ function applyV29(db) {
6695
+ db.exec(`
6696
+ CREATE TABLE IF NOT EXISTS user_topics (
6697
+ id TEXT PRIMARY KEY,
6698
+ title TEXT NOT NULL,
6699
+ summary TEXT NOT NULL,
6700
+ device_id TEXT,
6701
+ cwd TEXT,
6702
+ conversation_id TEXT,
6703
+ priority TEXT NOT NULL DEFAULT 'medium'
6704
+ CHECK(priority IN ('low','medium','high','critical')),
6705
+ tags TEXT NOT NULL DEFAULT '[]',
6706
+ scope TEXT NOT NULL DEFAULT 'global',
6707
+ search_summary TEXT,
6708
+ status TEXT NOT NULL DEFAULT 'active'
6709
+ CHECK(status IN ('active','closed','archived')),
6710
+ l1_embedding BLOB,
6711
+ client_name TEXT,
6712
+ client_version TEXT,
6713
+ source_tool TEXT,
6714
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
6715
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
6716
+ )
6717
+ `);
6718
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_status ON user_topics(status)`);
6719
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_scope ON user_topics(scope)`);
6720
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_priority ON user_topics(priority)`);
6721
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_updated ON user_topics(updated_at)`);
6722
+ // FTS5
6723
+ db.exec(`
6724
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_topics_fts USING fts5(
6725
+ title, summary, tags, search_summary,
6726
+ content='user_topics', content_rowid='rowid', tokenize='trigram'
6727
+ )
6728
+ `);
6729
+ const jc = (col) => `REPLACE(REPLACE(${col}, '[', ''), ']', '')`;
6730
+ // 冪等性: 部分適用からの再実行に備え、既存トリガーを先に削除
6731
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_fts_insert`);
6732
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_fts_update`);
6733
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_fts_delete`);
6734
+ db.exec(`
6735
+ CREATE TRIGGER user_topics_fts_insert AFTER INSERT ON user_topics BEGIN
6736
+ INSERT INTO user_topics_fts(rowid, title, summary, tags, search_summary)
6737
+ VALUES (new.rowid, new.title, new.summary, ${jc("new.tags")}, new.search_summary);
6738
+ END
6739
+ `);
6740
+ db.exec(`
6741
+ CREATE TRIGGER user_topics_fts_update AFTER UPDATE ON user_topics BEGIN
6742
+ INSERT INTO user_topics_fts(user_topics_fts, rowid, title, summary, tags, search_summary)
6743
+ VALUES ('delete', old.rowid, old.title, old.summary, ${jc("old.tags")}, old.search_summary);
6744
+ INSERT INTO user_topics_fts(rowid, title, summary, tags, search_summary)
6745
+ VALUES (new.rowid, new.title, new.summary, ${jc("new.tags")}, new.search_summary);
6746
+ END
6747
+ `);
6748
+ db.exec(`
6749
+ CREATE TRIGGER user_topics_fts_delete AFTER DELETE ON user_topics BEGIN
6750
+ INSERT INTO user_topics_fts(user_topics_fts, rowid, title, summary, tags, search_summary)
6751
+ VALUES ('delete', old.rowid, old.title, old.summary, ${jc("old.tags")}, old.search_summary);
6752
+ END
6753
+ `);
6754
+ // FTS rebuild
6755
+ db.exec(`INSERT INTO user_topics_fts(user_topics_fts) VALUES('rebuild')`);
6756
+ // audit_log の CHECK 制約に user_topic を追加
6757
+ // カラム順序を元のテーブル(V18 + V21 ALTER ADD COLUMN)と一致させる
6758
+ // 冪等性: 前回の部分適用で残存した _new テーブルを掃除
6759
+ db.exec(`DROP TABLE IF EXISTS audit_log_new`);
6760
+ // 冪等性: 前回の部分適用で既に完了している場合はスキップ
6761
+ if (!hasColumn(db, "audit_log", "device_id")) {
6762
+ // client_id → device_id リネーム + CHECK 制約更新
6763
+ db.exec(`
6764
+ CREATE TABLE audit_log_new (
6765
+ id TEXT PRIMARY KEY,
6766
+ operation TEXT NOT NULL CHECK(operation IN (
6767
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
6768
+ )),
6769
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model','user_memo','user_plan','user_issue','user_topic')),
6770
+ entity_id TEXT NOT NULL,
6771
+ summary TEXT NOT NULL,
6772
+ details TEXT,
6773
+ client_name TEXT,
6774
+ client_version TEXT,
6775
+ session_id TEXT,
6776
+ source_tool TEXT,
6777
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
6778
+ device_id TEXT
6779
+ )
6780
+ `);
6781
+ db.exec(`
6782
+ INSERT INTO audit_log_new (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at, device_id)
6783
+ SELECT id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at, client_id
6784
+ FROM audit_log
6785
+ `);
6786
+ db.exec(`DROP TABLE audit_log`);
6787
+ db.exec(`ALTER TABLE audit_log_new RENAME TO audit_log`);
6788
+ }
6789
+ // unified_search_items の CHECK 制約に user_topic を追加
6790
+ // SQLite では CHECK 制約の変更に ALTER TABLE が使えないため、テーブル再作成
6791
+ // 先に全ソーステーブルのunifiedトリガーを削除(DROP TABLE時の参照エラー防止)
6792
+ // user_topics も含める(部分適用からの再実行で既に作成済みの場合に備える)
6793
+ // 旧命名 (unified_search_<table>_<op>) と新命名 (<table>_unified_<op>) の両方を削除
6794
+ for (const table of ["claims", "episodes", "decisions", "theories", "insights", "models", "user_memos", "user_plans", "user_issues", "user_topics"]) {
6795
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_insert`);
6796
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_update`);
6797
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_delete`);
6798
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_${table}_insert`);
6799
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_${table}_update`);
6800
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_${table}_delete`);
6801
+ }
6802
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_insert`);
6803
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_update`);
6804
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
6805
+ // 冪等性: 部分適用からの再実行に対応
6806
+ // unified_search_items_new が残存している場合、前回の実行で unified_search_items は
6807
+ // 既に DROP 済みの可能性がある。その場合は _new をそのまま RENAME する。
6808
+ const usiExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='unified_search_items'").get();
6809
+ const usiNewExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='unified_search_items_new'").get();
6810
+ if (!usiNewExists) {
6811
+ // 通常パス: _new テーブルを作成してデータコピー
6812
+ db.exec(`
6813
+ CREATE TABLE unified_search_items_new (
6814
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','user_memo','user_plan','user_issue','user_topic')),
6815
+ entity_id TEXT NOT NULL,
6816
+ scope TEXT NOT NULL DEFAULT 'global',
6817
+ category TEXT,
6818
+ title_summary TEXT NOT NULL,
6819
+ search_text TEXT NOT NULL,
6820
+ search_summary TEXT,
6821
+ tags TEXT NOT NULL DEFAULT '[]',
6822
+ l1_embedding BLOB,
6823
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
6824
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
6825
+ PRIMARY KEY (entity_type, entity_id)
6826
+ )
6827
+ `);
6828
+ if (usiExists) {
6829
+ db.exec(`
6830
+ INSERT INTO unified_search_items_new (entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, l1_embedding, created_at, updated_at)
6831
+ SELECT entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, l1_embedding, created_at, updated_at
6832
+ FROM unified_search_items
6833
+ `);
6834
+ }
6835
+ }
6836
+ // _new が存在する状態(新規作成 or 前回残存)→ 旧テーブル削除して RENAME
6837
+ if (usiExists) {
6838
+ db.exec(`DROP TABLE unified_search_items`);
6839
+ }
6840
+ // RENAME が既に完了しているケース(_new も旧も存在しない = 既に unified_search_items になっている)はスキップ
6841
+ const usiNewStillExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='unified_search_items_new'").get();
6842
+ if (usiNewStillExists) {
6843
+ db.exec(`ALTER TABLE unified_search_items_new RENAME TO unified_search_items`);
6844
+ }
6845
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope)`);
6846
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type)`);
6847
+ // unified_search_fts 再構築
6848
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
6849
+ db.exec(`
6850
+ CREATE VIRTUAL TABLE unified_search_fts USING fts5(
6851
+ search_text, title_summary, search_summary,
6852
+ content='unified_search_items', content_rowid='rowid', tokenize='trigram'
6853
+ )
6854
+ `);
6855
+ db.exec(`
6856
+ CREATE TRIGGER unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
6857
+ INSERT INTO unified_search_fts(rowid, search_text, title_summary, search_summary)
6858
+ VALUES (new.rowid, new.search_text, new.title_summary, new.search_summary);
6859
+ END
6860
+ `);
6861
+ db.exec(`
6862
+ CREATE TRIGGER unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
6863
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, search_text, title_summary, search_summary)
6864
+ VALUES ('delete', old.rowid, old.search_text, old.title_summary, old.search_summary);
6865
+ INSERT INTO unified_search_fts(rowid, search_text, title_summary, search_summary)
6866
+ VALUES (new.rowid, new.search_text, new.title_summary, new.search_summary);
6867
+ END
6868
+ `);
6869
+ db.exec(`
6870
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
6871
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, search_text, title_summary, search_summary)
6872
+ VALUES ('delete', old.rowid, old.search_text, old.title_summary, old.search_summary);
6873
+ END
6874
+ `);
6875
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
6876
+ // 全ソーステーブルの unified トリガーを再作成
5554
6877
  const coal = (col) => `COALESCE(${col}, '')`;
5555
6878
  const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
6879
+ // claims (V28 形式: l2_evidence/l2_falsifier)
5556
6880
  const claimTitle = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object`;
5557
- const claimSearch = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`;
6881
+ const claimSearch = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal(`${p}l2_evidence`)} || ' ' || ${coal(`${p}l2_falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`;
5558
6882
  db.exec(`
5559
- CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims
5560
- WHEN new.status = 'active' BEGIN
6883
+ CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims WHEN new.status = 'active' BEGIN
5561
6884
  INSERT OR REPLACE INTO unified_search_items(${columns})
5562
6885
  VALUES ('claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
5563
6886
  END
@@ -5566,8 +6889,7 @@ function applyV27(db) {
5566
6889
  CREATE TRIGGER claims_unified_update AFTER UPDATE ON claims BEGIN
5567
6890
  DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
5568
6891
  INSERT INTO unified_search_items(${columns})
5569
- SELECT 'claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at
5570
- WHERE new.status = 'active';
6892
+ SELECT 'claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at WHERE new.status = 'active';
5571
6893
  END
5572
6894
  `);
5573
6895
  db.exec(`
@@ -5575,84 +6897,463 @@ function applyV27(db) {
5575
6897
  DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
5576
6898
  END
5577
6899
  `);
5578
- }
5579
- /**
5580
- * V28: evidence l2_evidence, falsifier l2_falsifier リネーム + claims_fts 再構築
5581
- * 冪等: 既に l2_evidence が存在する場合はリネームをスキップ
5582
- */
5583
- function applyV28(db) {
5584
- // カラムリネーム(冪等: 既にリネーム済みならスキップ)
5585
- if (hasColumn(db, "claims", "evidence") && !hasColumn(db, "claims", "l2_evidence")) {
5586
- db.exec(`ALTER TABLE claims RENAME COLUMN evidence TO l2_evidence`);
5587
- }
5588
- if (hasColumn(db, "claims", "falsifier") && !hasColumn(db, "claims", "l2_falsifier")) {
5589
- db.exec(`ALTER TABLE claims RENAME COLUMN falsifier TO l2_falsifier`);
6900
+ // episodes (V20 形式: l2_ columns)
6901
+ db.exec(`
6902
+ CREATE TRIGGER episodes_unified_insert AFTER INSERT ON episodes WHEN new.status = 'active' BEGIN
6903
+ INSERT OR REPLACE INTO unified_search_items(${columns})
6904
+ VALUES ('episode', new.id, new.scope, NULL, new.title, new.title || ' ' || ${coal("new.l1_content")} || ' ' || ${coal("new.l2_context")} || ' ' || ${coal("new.l2_trigger")} || ' ' || ${jc("new.l2_problems")} || ' ' || ${jc("new.l2_outcomes")} || ' ' || ${jc("new.l2_principles")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
6905
+ END
6906
+ `);
6907
+ db.exec(`
6908
+ CREATE TRIGGER episodes_unified_update AFTER UPDATE ON episodes BEGIN
6909
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
6910
+ INSERT INTO unified_search_items(${columns})
6911
+ SELECT 'episode', new.id, new.scope, NULL, new.title, new.title || ' ' || ${coal("new.l1_content")} || ' ' || ${coal("new.l2_context")} || ' ' || ${coal("new.l2_trigger")} || ' ' || ${jc("new.l2_problems")} || ' ' || ${jc("new.l2_outcomes")} || ' ' || ${jc("new.l2_principles")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active';
6912
+ END
6913
+ `);
6914
+ db.exec(`
6915
+ CREATE TRIGGER episodes_unified_delete AFTER DELETE ON episodes BEGIN
6916
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
6917
+ END
6918
+ `);
6919
+ // decisions (V20 形式: l2_ columns)
6920
+ db.exec(`
6921
+ CREATE TRIGGER decisions_unified_insert AFTER INSERT ON decisions WHEN new.status = 'active' BEGIN
6922
+ INSERT OR REPLACE INTO unified_search_items(${columns})
6923
+ VALUES ('decision', new.id, new.scope, NULL, new.title, new.title || ' ' || new.description || ' ' || ${coal("new.l1_content")} || ' ' || new.l2_reasoning || ' ' || ${jc("new.l2_alternatives")} || ' ' || ${coal("new.search_summary")}, new.search_summary, '[]', new.created_at, new.updated_at);
6924
+ END
6925
+ `);
6926
+ db.exec(`
6927
+ CREATE TRIGGER decisions_unified_update AFTER UPDATE ON decisions BEGIN
6928
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
6929
+ INSERT INTO unified_search_items(${columns})
6930
+ SELECT 'decision', new.id, new.scope, NULL, new.title, new.title || ' ' || new.description || ' ' || ${coal("new.l1_content")} || ' ' || new.l2_reasoning || ' ' || ${jc("new.l2_alternatives")} || ' ' || ${coal("new.search_summary")}, new.search_summary, '[]', new.created_at, new.updated_at WHERE new.status = 'active';
6931
+ END
6932
+ `);
6933
+ db.exec(`
6934
+ CREATE TRIGGER decisions_unified_delete AFTER DELETE ON decisions BEGIN
6935
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
6936
+ END
6937
+ `);
6938
+ // theories, insights, models: 共通パターン(l2_ columns)
6939
+ for (const e of [
6940
+ { type: "theory", table: "theories" },
6941
+ { type: "insight", table: "insights" },
6942
+ { type: "model", table: "models" },
6943
+ ]) {
6944
+ const searchExpr = (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}l2_core_thesis`)} || ' ' || ${jc(`${p}l2_principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`;
6945
+ db.exec(`
6946
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table} WHEN new.status = 'active' BEGIN
6947
+ INSERT OR REPLACE INTO unified_search_items(${columns})
6948
+ VALUES ('${e.type}', new.id, new.scope, NULL, new.title, ${searchExpr("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
6949
+ END
6950
+ `);
6951
+ db.exec(`
6952
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
6953
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
6954
+ INSERT INTO unified_search_items(${columns})
6955
+ SELECT '${e.type}', new.id, new.scope, NULL, new.title, ${searchExpr("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active';
6956
+ END
6957
+ `);
6958
+ db.exec(`
6959
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
6960
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
6961
+ END
6962
+ `);
5590
6963
  }
5591
- // claims_fts を再構築(l2_evidence/l2_falsifier 対応)
6964
+ // user_memos (V24 形式: l1_content, user_input)
5592
6965
  db.exec(`
5593
- DROP TRIGGER IF EXISTS claims_fts_insert;
5594
- DROP TRIGGER IF EXISTS claims_fts_update;
5595
- DROP TRIGGER IF EXISTS claims_fts_delete;
5596
- DROP TABLE IF EXISTS claims_fts;
5597
-
5598
- CREATE VIRTUAL TABLE claims_fts USING fts5(
5599
- l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary,
5600
- content='claims', content_rowid='rowid', tokenize='trigram'
5601
- );
5602
-
5603
- -- FTS同期トリガー: INSERT
5604
- CREATE TRIGGER claims_fts_insert AFTER INSERT ON claims BEGIN
5605
- INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
5606
- VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
5607
- END;
5608
-
5609
- -- FTS同期トリガー: UPDATE
5610
- CREATE TRIGGER claims_fts_update AFTER UPDATE ON claims BEGIN
5611
- INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
5612
- VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
5613
- INSERT INTO claims_fts(rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
5614
- VALUES (new.rowid, new.l2_subject, new.l2_predicate, new.l2_object, new.l2_evidence, new.l1_content, new.search_summary);
5615
- END;
5616
-
5617
- -- FTS同期トリガー: DELETE
5618
- CREATE TRIGGER claims_fts_delete AFTER DELETE ON claims BEGIN
5619
- INSERT INTO claims_fts(claims_fts, rowid, l2_subject, l2_predicate, l2_object, l2_evidence, l1_content, search_summary)
5620
- VALUES ('delete', old.rowid, old.l2_subject, old.l2_predicate, old.l2_object, old.l2_evidence, old.l1_content, old.search_summary);
5621
- END;
5622
-
5623
- -- FTS rebuild
5624
- INSERT INTO claims_fts(claims_fts) VALUES('rebuild');
5625
-
5626
- INSERT INTO schema_version (version) VALUES (28);
6966
+ CREATE TRIGGER user_memos_unified_insert AFTER INSERT ON user_memos
6967
+ WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN
6968
+ INSERT OR REPLACE INTO unified_search_items(${columns})
6969
+ VALUES ('user_memo', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${coal("new.user_input")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
6970
+ END
5627
6971
  `);
5628
- // unified_search_items の claims トリガー再作成(l2_evidence/l2_falsifier 対応)
5629
- const coal = (col) => `COALESCE(${col}, '')`;
5630
- const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
5631
- const claimTitle = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object`;
5632
- const claimSearch = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal(`${p}l2_evidence`)} || ' ' || ${coal(`${p}l2_falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`;
5633
- db.exec(`DROP TRIGGER IF EXISTS claims_unified_insert`);
5634
- db.exec(`DROP TRIGGER IF EXISTS claims_unified_update`);
5635
- db.exec(`DROP TRIGGER IF EXISTS claims_unified_delete`);
5636
6972
  db.exec(`
5637
- CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims
5638
- WHEN new.status = 'active' BEGIN
6973
+ CREATE TRIGGER user_memos_unified_update AFTER UPDATE ON user_memos BEGIN
6974
+ DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id;
6975
+ INSERT INTO unified_search_items(${columns})
6976
+ SELECT 'user_memo', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${coal("new.user_input")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
6977
+ WHERE new.status = 'active' AND new.usage_policy != 'human_directed';
6978
+ END
6979
+ `);
6980
+ db.exec(`
6981
+ CREATE TRIGGER user_memos_unified_delete AFTER DELETE ON user_memos BEGIN
6982
+ DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id;
6983
+ END
6984
+ `);
6985
+ // user_plans (V24 形式: l1_content)
6986
+ db.exec(`
6987
+ CREATE TRIGGER user_plans_unified_insert AFTER INSERT ON user_plans
6988
+ WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN
5639
6989
  INSERT OR REPLACE INTO unified_search_items(${columns})
5640
- VALUES ('claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
6990
+ VALUES ('user_plan', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
5641
6991
  END
5642
6992
  `);
5643
6993
  db.exec(`
5644
- CREATE TRIGGER claims_unified_update AFTER UPDATE ON claims BEGIN
5645
- DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
6994
+ CREATE TRIGGER user_plans_unified_update AFTER UPDATE ON user_plans BEGIN
6995
+ DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id;
5646
6996
  INSERT INTO unified_search_items(${columns})
5647
- SELECT 'claim', new.id, new.scope, new.category, ${claimTitle("new.")}, ${claimSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at
6997
+ SELECT 'user_plan', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
6998
+ WHERE new.status = 'active' AND new.usage_policy != 'human_directed';
6999
+ END
7000
+ `);
7001
+ db.exec(`
7002
+ CREATE TRIGGER user_plans_unified_delete AFTER DELETE ON user_plans BEGIN
7003
+ DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id;
7004
+ END
7005
+ `);
7006
+ // user_issues (V24 形式: l1_content, entries)
7007
+ db.exec(`
7008
+ CREATE TRIGGER user_issues_unified_insert AFTER INSERT ON user_issues WHEN new.status = 'open' BEGIN
7009
+ INSERT OR REPLACE INTO unified_search_items(${columns})
7010
+ VALUES ('user_issue', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.entries")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
7011
+ END
7012
+ `);
7013
+ db.exec(`
7014
+ CREATE TRIGGER user_issues_unified_update AFTER UPDATE ON user_issues BEGIN
7015
+ DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id;
7016
+ INSERT INTO unified_search_items(${columns})
7017
+ SELECT 'user_issue', new.id, new.scope, NULL, new.title, new.title || ' ' || new.l1_content || ' ' || ${jc("new.entries")} || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
7018
+ WHERE new.status = 'open';
7019
+ END
7020
+ `);
7021
+ db.exec(`
7022
+ CREATE TRIGGER user_issues_unified_delete AFTER DELETE ON user_issues BEGIN
7023
+ DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id;
7024
+ END
7025
+ `);
7026
+ // user_topics
7027
+ db.exec(`
7028
+ CREATE TRIGGER user_topics_unified_insert AFTER INSERT ON user_topics WHEN new.status = 'active' BEGIN
7029
+ INSERT OR REPLACE INTO unified_search_items(${columns})
7030
+ VALUES ('user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || new.summary || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
7031
+ END
7032
+ `);
7033
+ db.exec(`
7034
+ CREATE TRIGGER user_topics_unified_update AFTER UPDATE ON user_topics BEGIN
7035
+ DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id;
7036
+ INSERT INTO unified_search_items(${columns})
7037
+ SELECT 'user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || new.summary || ' ' || ${jc("new.tags")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at
5648
7038
  WHERE new.status = 'active';
5649
7039
  END
5650
7040
  `);
5651
7041
  db.exec(`
5652
- CREATE TRIGGER claims_unified_delete AFTER DELETE ON claims BEGIN
5653
- DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id;
7042
+ CREATE TRIGGER user_topics_unified_delete AFTER DELETE ON user_topics BEGIN
7043
+ DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id;
5654
7044
  END
5655
7045
  `);
7046
+ db.exec(`INSERT INTO schema_version (version) VALUES (29)`);
7047
+ }
7048
+ /**
7049
+ * V30: client_id → device_id リネーム
7050
+ * audit_log, user_topics の client_id カラムを device_id に変更。
7051
+ * store_meta のキーも client_id → device_id に変更。
7052
+ * 既存DBで V29 が旧コードで適用済みの場合にカラムをリネームする。
7053
+ */
7054
+ function applyV30(db) {
7055
+ // store_meta の key リネーム(V29 で既に実施済みの場合は no-op)
7056
+ db.exec(`UPDATE store_meta SET key = 'device_id' WHERE key = 'client_id'`);
7057
+ // audit_log: client_id → device_id(テーブル再作成)
7058
+ if (hasColumn(db, "audit_log", "client_id")) {
7059
+ db.exec(`
7060
+ CREATE TABLE audit_log_v30 (
7061
+ id TEXT PRIMARY KEY,
7062
+ operation TEXT NOT NULL CHECK(operation IN (
7063
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
7064
+ )),
7065
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model','user_memo','user_plan','user_issue','user_topic')),
7066
+ entity_id TEXT NOT NULL,
7067
+ summary TEXT NOT NULL,
7068
+ details TEXT,
7069
+ client_name TEXT,
7070
+ client_version TEXT,
7071
+ session_id TEXT,
7072
+ source_tool TEXT,
7073
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7074
+ device_id TEXT
7075
+ )
7076
+ `);
7077
+ db.exec(`
7078
+ INSERT INTO audit_log_v30 (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at, device_id)
7079
+ SELECT id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at, client_id
7080
+ FROM audit_log
7081
+ `);
7082
+ db.exec(`DROP TABLE audit_log`);
7083
+ db.exec(`ALTER TABLE audit_log_v30 RENAME TO audit_log`);
7084
+ }
7085
+ // user_topics: client_id → device_id(テーブル再作成)
7086
+ // client_id がある場合はリネーム、device_id が欠落している場合は追加
7087
+ const hasUserTopics = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='user_topics'").get();
7088
+ if (hasUserTopics && hasColumn(db, "user_topics", "client_id")) {
7089
+ // FTSトリガーを削除(DROP TABLE前)
7090
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_fts_insert`);
7091
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_fts_update`);
7092
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_fts_delete`);
7093
+ // unified トリガーも削除
7094
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_unified_insert`);
7095
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_unified_update`);
7096
+ db.exec(`DROP TRIGGER IF EXISTS user_topics_unified_delete`);
7097
+ db.exec(`
7098
+ CREATE TABLE user_topics_v30 (
7099
+ id TEXT PRIMARY KEY,
7100
+ title TEXT NOT NULL,
7101
+ summary TEXT NOT NULL,
7102
+ device_id TEXT,
7103
+ cwd TEXT,
7104
+ conversation_id TEXT,
7105
+ priority TEXT NOT NULL DEFAULT 'medium'
7106
+ CHECK(priority IN ('low','medium','high','critical')),
7107
+ tags TEXT NOT NULL DEFAULT '[]',
7108
+ scope TEXT NOT NULL DEFAULT 'global',
7109
+ search_summary TEXT,
7110
+ status TEXT NOT NULL DEFAULT 'active'
7111
+ CHECK(status IN ('active','closed','archived')),
7112
+ l1_embedding BLOB,
7113
+ client_name TEXT,
7114
+ client_version TEXT,
7115
+ source_tool TEXT,
7116
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7117
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7118
+ )
7119
+ `);
7120
+ db.exec(`
7121
+ INSERT INTO user_topics_v30 (id, title, summary, device_id, cwd, conversation_id, priority, tags, scope, search_summary, status, l1_embedding, client_name, client_version, source_tool, created_at, updated_at)
7122
+ SELECT id, title, summary, client_id, cwd, conversation_id, priority, tags, scope, search_summary, status, l1_embedding, client_name, client_version, source_tool, created_at, updated_at
7123
+ FROM user_topics
7124
+ `);
7125
+ db.exec(`DROP TABLE user_topics`);
7126
+ db.exec(`ALTER TABLE user_topics_v30 RENAME TO user_topics`);
7127
+ // インデックス再作成
7128
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_status ON user_topics(status)`);
7129
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_scope ON user_topics(scope)`);
7130
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_priority ON user_topics(priority)`);
7131
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_topics_updated ON user_topics(updated_at)`);
7132
+ // FTS再構築
7133
+ db.exec(`DROP TABLE IF EXISTS user_topics_fts`);
7134
+ db.exec(`
7135
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_topics_fts USING fts5(
7136
+ title, summary, tags, search_summary,
7137
+ content='user_topics', content_rowid='rowid', tokenize='trigram'
7138
+ )
7139
+ `);
7140
+ const jc = (col) => `REPLACE(REPLACE(${col}, '[', ''), ']', '')`;
7141
+ db.exec(`
7142
+ CREATE TRIGGER user_topics_fts_insert AFTER INSERT ON user_topics BEGIN
7143
+ INSERT INTO user_topics_fts(rowid, title, summary, tags, search_summary)
7144
+ VALUES (new.rowid, new.title, new.summary, ${jc("new.tags")}, new.search_summary);
7145
+ END
7146
+ `);
7147
+ db.exec(`
7148
+ CREATE TRIGGER user_topics_fts_update AFTER UPDATE ON user_topics BEGIN
7149
+ INSERT INTO user_topics_fts(user_topics_fts, rowid, title, summary, tags, search_summary)
7150
+ VALUES ('delete', old.rowid, old.title, old.summary, ${jc("old.tags")}, old.search_summary);
7151
+ INSERT INTO user_topics_fts(rowid, title, summary, tags, search_summary)
7152
+ VALUES (new.rowid, new.title, new.summary, ${jc("new.tags")}, new.search_summary);
7153
+ END
7154
+ `);
7155
+ db.exec(`
7156
+ CREATE TRIGGER user_topics_fts_delete AFTER DELETE ON user_topics BEGIN
7157
+ INSERT INTO user_topics_fts(user_topics_fts, rowid, title, summary, tags, search_summary)
7158
+ VALUES ('delete', old.rowid, old.title, old.summary, ${jc("old.tags")}, old.search_summary);
7159
+ END
7160
+ `);
7161
+ db.exec(`INSERT INTO user_topics_fts(user_topics_fts) VALUES('rebuild')`);
7162
+ // unified トリガー再作成
7163
+ const coal = (col) => `COALESCE(${col}, '')`;
7164
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
7165
+ db.exec(`
7166
+ CREATE TRIGGER user_topics_unified_insert AFTER INSERT ON user_topics WHEN new.status = 'active' BEGIN
7167
+ INSERT OR REPLACE INTO unified_search_items(${columns})
7168
+ VALUES ('user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || ${coal("new.summary")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at);
7169
+ END
7170
+ `);
7171
+ db.exec(`
7172
+ CREATE TRIGGER user_topics_unified_update AFTER UPDATE ON user_topics BEGIN
7173
+ DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id;
7174
+ INSERT INTO unified_search_items(${columns})
7175
+ SELECT 'user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || ${coal("new.summary")} || ' ' || ${coal("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active';
7176
+ END
7177
+ `);
7178
+ db.exec(`
7179
+ CREATE TRIGGER user_topics_unified_delete AFTER DELETE ON user_topics BEGIN
7180
+ DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id;
7181
+ END
7182
+ `);
7183
+ }
7184
+ else if (hasUserTopics && !hasColumn(db, "user_topics", "device_id")) {
7185
+ // device_id カラムが欠落している場合(中間バージョンのV29で作成されたDB)
7186
+ db.exec(`ALTER TABLE user_topics ADD COLUMN device_id TEXT`);
7187
+ const deviceIdRow = db.prepare("SELECT value FROM store_meta WHERE key = 'device_id'").get();
7188
+ if (deviceIdRow) {
7189
+ db.prepare("UPDATE user_topics SET device_id = ? WHERE device_id IS NULL").run(deviceIdRow.value);
7190
+ }
7191
+ }
7192
+ db.exec(`INSERT INTO schema_version (version) VALUES (30)`);
7193
+ }
7194
+ // ============================================================
7195
+ // V31: user_files テーブル新設(File Vault機能)
7196
+ // ============================================================
7197
+ function applyV31(db) {
7198
+ const transaction = db.transaction(() => {
7199
+ // user_files テーブル
7200
+ db.exec(`
7201
+ CREATE TABLE IF NOT EXISTS user_files (
7202
+ id TEXT PRIMARY KEY,
7203
+ title TEXT NOT NULL,
7204
+ description TEXT,
7205
+ original_filename TEXT NOT NULL,
7206
+ original_encoding TEXT NOT NULL DEFAULT 'utf-8',
7207
+ file_data TEXT NOT NULL,
7208
+ file_hash TEXT NOT NULL,
7209
+ file_size INTEGER NOT NULL,
7210
+ tags TEXT NOT NULL DEFAULT '[]',
7211
+ scope TEXT NOT NULL DEFAULT 'global',
7212
+ search_summary TEXT,
7213
+ status TEXT NOT NULL DEFAULT 'active'
7214
+ CHECK(status IN ('active','archived')),
7215
+ l1_embedding BLOB,
7216
+ client_name TEXT,
7217
+ client_version TEXT,
7218
+ source_tool TEXT,
7219
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7220
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7221
+ )
7222
+ `);
7223
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_files_status ON user_files(status)`);
7224
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_files_scope ON user_files(scope)`);
7225
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_files_title ON user_files(title)`);
7226
+ // audit_log: entity_type CHECK に 'user_file' 追加(テーブル再作成)
7227
+ // 一時テーブル名は audit_log_new に統一(残骸掃除パターンと一致させる)
7228
+ db.exec(`DROP TABLE IF EXISTS audit_log_new`);
7229
+ db.exec(`
7230
+ CREATE TABLE audit_log_new (
7231
+ id TEXT PRIMARY KEY,
7232
+ operation TEXT NOT NULL CHECK(operation IN (
7233
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
7234
+ )),
7235
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model','user_memo','user_plan','user_issue','user_topic','user_file')),
7236
+ entity_id TEXT NOT NULL,
7237
+ summary TEXT NOT NULL,
7238
+ details TEXT,
7239
+ client_name TEXT,
7240
+ client_version TEXT,
7241
+ session_id TEXT,
7242
+ source_tool TEXT,
7243
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7244
+ device_id TEXT
7245
+ )
7246
+ `);
7247
+ db.exec(`
7248
+ INSERT INTO audit_log_new (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at, device_id)
7249
+ SELECT id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at, device_id
7250
+ FROM audit_log
7251
+ `);
7252
+ db.exec(`DROP TABLE audit_log`);
7253
+ db.exec(`ALTER TABLE audit_log_new RENAME TO audit_log`);
7254
+ // audit_log インデックス再作成(4本すべて)
7255
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id)`);
7256
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation)`);
7257
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`);
7258
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id)`);
7259
+ // unified search統合: CHECK制約更新 + 全トリガー再作成 + user_filesトリガー追加
7260
+ // 全トリガーを削除
7261
+ const triggerTables = ["claims", "episodes", "decisions", "theories", "insights", "models", "user_memos", "user_plans", "user_issues", "user_topics"];
7262
+ for (const t of triggerTables) {
7263
+ db.exec(`DROP TRIGGER IF EXISTS ${t}_unified_insert`);
7264
+ db.exec(`DROP TRIGGER IF EXISTS ${t}_unified_update`);
7265
+ db.exec(`DROP TRIGGER IF EXISTS ${t}_unified_delete`);
7266
+ }
7267
+ // FTS5 削除(content テーブル参照のため先に)
7268
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
7269
+ // unified_search_items 再作成(CHECK に 'user_file' 追加)
7270
+ db.exec(`DROP TABLE IF EXISTS unified_search_items_new`);
7271
+ db.exec(`
7272
+ CREATE TABLE unified_search_items_new (
7273
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','user_memo','user_plan','user_issue','user_topic','user_file')),
7274
+ entity_id TEXT NOT NULL,
7275
+ scope TEXT NOT NULL DEFAULT 'global',
7276
+ category TEXT,
7277
+ title_summary TEXT NOT NULL,
7278
+ search_text TEXT NOT NULL,
7279
+ search_summary TEXT,
7280
+ tags TEXT NOT NULL DEFAULT '[]',
7281
+ l1_embedding BLOB,
7282
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7283
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
7284
+ PRIMARY KEY (entity_type, entity_id)
7285
+ )
7286
+ `);
7287
+ db.exec(`INSERT INTO unified_search_items_new SELECT * FROM unified_search_items`);
7288
+ db.exec(`DROP TABLE unified_search_items`);
7289
+ db.exec(`ALTER TABLE unified_search_items_new RENAME TO unified_search_items`);
7290
+ // インデックス再作成
7291
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope)`);
7292
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type)`);
7293
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_updated ON unified_search_items(updated_at)`);
7294
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_id ON unified_search_items(entity_id)`);
7295
+ // FTS5 再構築
7296
+ db.exec(`
7297
+ CREATE VIRTUAL TABLE IF NOT EXISTS unified_search_fts USING fts5(
7298
+ search_text, title_summary, search_summary,
7299
+ content='unified_search_items', content_rowid='rowid', tokenize='trigram'
7300
+ )
7301
+ `);
7302
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
7303
+ // 全トリガー再作成
7304
+ const jc2 = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
7305
+ const jcT = (col) => `REPLACE(REPLACE(${col}, '[', ''), ']', '')`;
7306
+ const coal2 = (col) => `COALESCE(${col}, '')`;
7307
+ const cols = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
7308
+ // claims
7309
+ const ct = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object`;
7310
+ const cs = (p) => `${p}l2_subject || ' ' || ${p}l2_predicate || ' ' || ${p}l2_object || ' ' || ${coal2(`${p}l2_evidence`)} || ' ' || ${coal2(`${p}l2_falsifier`)} || ' ' || ${coal2(`${p}l1_content`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7311
+ db.exec(`CREATE TRIGGER claims_unified_insert AFTER INSERT ON claims WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('claim', new.id, new.scope, new.category, ${ct("new.")}, ${cs("new.")}, new.search_summary, '[]', new.created_at, new.updated_at); END`);
7312
+ db.exec(`CREATE TRIGGER claims_unified_update AFTER UPDATE ON claims BEGIN DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'claim', new.id, new.scope, new.category, ${ct("new.")}, ${cs("new.")}, new.search_summary, '[]', new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7313
+ db.exec(`CREATE TRIGGER claims_unified_delete AFTER DELETE ON claims BEGIN DELETE FROM unified_search_items WHERE entity_type = 'claim' AND entity_id = old.id; END`);
7314
+ // episodes
7315
+ const es = (p) => `${p}title || ' ' || ${coal2(`${p}l1_content`)} || ' ' || ${coal2(`${p}l2_context`)} || ' ' || ${coal2(`${p}l2_trigger`)} || ' ' || ${jc2(`${p}l2_problems`)} || ' ' || ${jc2(`${p}l2_outcomes`)} || ' ' || ${jc2(`${p}l2_principles`)} || ' ' || ${jc2(`${p}tags`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7316
+ db.exec(`CREATE TRIGGER episodes_unified_insert AFTER INSERT ON episodes WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('episode', new.id, new.scope, NULL, new.title, ${es("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7317
+ db.exec(`CREATE TRIGGER episodes_unified_update AFTER UPDATE ON episodes BEGIN DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'episode', new.id, new.scope, NULL, new.title, ${es("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7318
+ db.exec(`CREATE TRIGGER episodes_unified_delete AFTER DELETE ON episodes BEGIN DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id; END`);
7319
+ // decisions
7320
+ const ds = (p) => `${p}title || ' ' || ${p}description || ' ' || ${coal2(`${p}l1_content`)} || ' ' || ${p}l2_reasoning || ' ' || ${jc2(`${p}l2_alternatives`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7321
+ db.exec(`CREATE TRIGGER decisions_unified_insert AFTER INSERT ON decisions WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('decision', new.id, new.scope, NULL, new.title, ${ds("new.")}, new.search_summary, '[]', new.created_at, new.updated_at); END`);
7322
+ db.exec(`CREATE TRIGGER decisions_unified_update AFTER UPDATE ON decisions BEGIN DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'decision', new.id, new.scope, NULL, new.title, ${ds("new.")}, new.search_summary, '[]', new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7323
+ db.exec(`CREATE TRIGGER decisions_unified_delete AFTER DELETE ON decisions BEGIN DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id; END`);
7324
+ // theories, insights, models
7325
+ for (const e of [{ type: "theory", table: "theories" }, { type: "insight", table: "insights" }, { type: "model", table: "models" }]) {
7326
+ const s = (p) => `${p}title || ' ' || ${coal2(`${p}description`)} || ' ' || ${coal2(`${p}l1_content`)} || ' ' || ${coal2(`${p}l2_core_thesis`)} || ' ' || ${jc2(`${p}l2_principles`)} || ' ' || ${jc2(`${p}evidence_refs`)} || ' ' || ${jc2(`${p}tags`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7327
+ db.exec(`CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table} WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('${e.type}', new.id, new.scope, NULL, new.title, ${s("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7328
+ db.exec(`CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT '${e.type}', new.id, new.scope, NULL, new.title, ${s("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7329
+ db.exec(`CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id; END`);
7330
+ }
7331
+ // user_memos
7332
+ const ms = (p) => `${p}title || ' ' || ${p}l1_content || ' ' || ${coal2(`${p}user_input`)} || ' ' || ${jc2(`${p}tags`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7333
+ db.exec(`CREATE TRIGGER user_memos_unified_insert AFTER INSERT ON user_memos WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('user_memo', new.id, new.scope, NULL, new.title, ${ms("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7334
+ db.exec(`CREATE TRIGGER user_memos_unified_update AFTER UPDATE ON user_memos BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'user_memo', new.id, new.scope, NULL, new.title, ${ms("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active' AND new.usage_policy != 'human_directed'; END`);
7335
+ db.exec(`CREATE TRIGGER user_memos_unified_delete AFTER DELETE ON user_memos BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id; END`);
7336
+ // user_plans
7337
+ const ps = (p) => `${p}title || ' ' || ${p}l1_content || ' ' || ${jc2(`${p}tags`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7338
+ db.exec(`CREATE TRIGGER user_plans_unified_insert AFTER INSERT ON user_plans WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('user_plan', new.id, new.scope, NULL, new.title, ${ps("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7339
+ db.exec(`CREATE TRIGGER user_plans_unified_update AFTER UPDATE ON user_plans BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'user_plan', new.id, new.scope, NULL, new.title, ${ps("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7340
+ db.exec(`CREATE TRIGGER user_plans_unified_delete AFTER DELETE ON user_plans BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id; END`);
7341
+ // user_issues
7342
+ const is2 = (p) => `${p}title || ' ' || ${p}l1_content || ' ' || ${jc2(`${p}entries`)} || ' ' || ${jc2(`${p}tags`)} || ' ' || ${coal2(`${p}search_summary`)}`;
7343
+ db.exec(`CREATE TRIGGER user_issues_unified_insert AFTER INSERT ON user_issues WHEN new.status = 'open' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('user_issue', new.id, new.scope, NULL, new.title, ${is2("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7344
+ db.exec(`CREATE TRIGGER user_issues_unified_update AFTER UPDATE ON user_issues BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'user_issue', new.id, new.scope, NULL, new.title, ${is2("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'open'; END`);
7345
+ db.exec(`CREATE TRIGGER user_issues_unified_delete AFTER DELETE ON user_issues BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id; END`);
7346
+ // user_topics
7347
+ db.exec(`CREATE TRIGGER user_topics_unified_insert AFTER INSERT ON user_topics WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || new.summary || ' ' || ${jcT("new.tags")} || ' ' || ${coal2("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7348
+ db.exec(`CREATE TRIGGER user_topics_unified_update AFTER UPDATE ON user_topics BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'user_topic', new.id, new.scope, NULL, new.title, new.title || ' ' || new.summary || ' ' || ${jcT("new.tags")} || ' ' || ${coal2("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7349
+ db.exec(`CREATE TRIGGER user_topics_unified_delete AFTER DELETE ON user_topics BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_topic' AND entity_id = old.id; END`);
7350
+ // user_files
7351
+ db.exec(`CREATE TRIGGER user_files_unified_insert AFTER INSERT ON user_files WHEN new.status = 'active' BEGIN INSERT OR REPLACE INTO unified_search_items(${cols}) VALUES ('user_file', new.id, new.scope, NULL, new.title, new.title || ' ' || COALESCE(new.description, '') || ' ' || new.original_filename || ' ' || ${jcT("new.tags")} || ' ' || ${coal2("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at); END`);
7352
+ db.exec(`CREATE TRIGGER user_files_unified_update AFTER UPDATE ON user_files BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_file' AND entity_id = old.id; INSERT INTO unified_search_items(${cols}) SELECT 'user_file', new.id, new.scope, NULL, new.title, new.title || ' ' || COALESCE(new.description, '') || ' ' || new.original_filename || ' ' || ${jcT("new.tags")} || ' ' || ${coal2("new.search_summary")}, new.search_summary, new.tags, new.created_at, new.updated_at WHERE new.status = 'active'; END`);
7353
+ db.exec(`CREATE TRIGGER user_files_unified_delete AFTER DELETE ON user_files BEGIN DELETE FROM unified_search_items WHERE entity_type = 'user_file' AND entity_id = old.id; END`);
7354
+ db.exec(`INSERT INTO schema_version (version) VALUES (31)`);
7355
+ });
7356
+ transaction();
5656
7357
  }
5657
7358
  /** テーブルに指定カラムが存在するかチェック */
5658
7359
  function hasColumn(db, table, column) {