watashi-db 0.0.13 → 0.0.15

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