watashi-db 0.0.7 → 0.0.8

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 (174) hide show
  1. package/README.md +120 -63
  2. package/Usersj.miyamoto.watashi-dbdefault.db +0 -0
  3. package/cowork-plugin/.claude-plugin/plugin.json +1 -1
  4. package/cowork-plugin/.mcp.json +1 -1
  5. package/cowork-plugin/hooks/hooks.json +7 -0
  6. package/cowork-plugin/skills/recall/SKILL.md +6 -3
  7. package/dist/config/loader.js +25 -1
  8. package/dist/config/loader.js.map +1 -1
  9. package/dist/config/schema.d.ts +53 -0
  10. package/dist/config/schema.js +9 -0
  11. package/dist/config/schema.js.map +1 -1
  12. package/dist/config/validator.js +11 -0
  13. package/dist/config/validator.js.map +1 -1
  14. package/dist/constants.d.ts +9 -1
  15. package/dist/constants.js +51 -3
  16. package/dist/constants.js.map +1 -1
  17. package/dist/context-pack.d.ts +7 -2
  18. package/dist/context-pack.js +137 -31
  19. package/dist/context-pack.js.map +1 -1
  20. package/dist/database/archive.d.ts +39 -0
  21. package/dist/database/archive.js +180 -0
  22. package/dist/database/archive.js.map +1 -0
  23. package/dist/database/groom.d.ts +39 -0
  24. package/dist/database/groom.js +246 -0
  25. package/dist/database/groom.js.map +1 -0
  26. package/dist/database/maintain.d.ts +39 -0
  27. package/dist/database/maintain.js +62 -0
  28. package/dist/database/maintain.js.map +1 -0
  29. package/dist/database/queries-core.d.ts +320 -12
  30. package/dist/database/queries-core.js +1834 -56
  31. package/dist/database/queries-core.js.map +1 -1
  32. package/dist/database/queries.d.ts +213 -12
  33. package/dist/database/queries.js +109 -0
  34. package/dist/database/queries.js.map +1 -1
  35. package/dist/database/schema.js +4199 -1
  36. package/dist/database/schema.js.map +1 -1
  37. package/dist/embedding/embed-on-write.d.ts +98 -0
  38. package/dist/embedding/embed-on-write.js +104 -0
  39. package/dist/embedding/embed-on-write.js.map +1 -0
  40. package/dist/embedding/ollama.d.ts +18 -0
  41. package/dist/embedding/ollama.js +67 -0
  42. package/dist/embedding/ollama.js.map +1 -0
  43. package/dist/embedding/openai.d.ts +17 -0
  44. package/dist/embedding/openai.js +77 -0
  45. package/dist/embedding/openai.js.map +1 -0
  46. package/dist/embedding/provider.d.ts +34 -0
  47. package/dist/embedding/provider.js +92 -0
  48. package/dist/embedding/provider.js.map +1 -0
  49. package/dist/embedding/search.d.ts +61 -0
  50. package/dist/embedding/search.js +165 -0
  51. package/dist/embedding/search.js.map +1 -0
  52. package/dist/embedding/similarity.d.ts +21 -0
  53. package/dist/embedding/similarity.js +48 -0
  54. package/dist/embedding/similarity.js.map +1 -0
  55. package/dist/embedding/transformers-local.d.ts +24 -0
  56. package/dist/embedding/transformers-local.js +65 -0
  57. package/dist/embedding/transformers-local.js.map +1 -0
  58. package/dist/embedding/types.d.ts +27 -0
  59. package/dist/embedding/types.js +6 -0
  60. package/dist/embedding/types.js.map +1 -0
  61. package/dist/hook.d.ts +54 -1
  62. package/dist/hook.js +560 -7
  63. package/dist/hook.js.map +1 -1
  64. package/dist/index.js +4 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/policy.d.ts +7 -1
  67. package/dist/policy.js +30 -0
  68. package/dist/policy.js.map +1 -1
  69. package/dist/project-detect.d.ts +8 -0
  70. package/dist/project-detect.js +36 -0
  71. package/dist/project-detect.js.map +1 -0
  72. package/dist/prompts/bootstrap-prompts.js +48 -0
  73. package/dist/prompts/bootstrap-prompts.js.map +1 -1
  74. package/dist/resources/config-guide-content.d.ts +7 -0
  75. package/dist/resources/config-guide-content.js +180 -0
  76. package/dist/resources/config-guide-content.js.map +1 -0
  77. package/dist/resources/context-resources.js +18 -1
  78. package/dist/resources/context-resources.js.map +1 -1
  79. package/dist/server-instructions.js +60 -3
  80. package/dist/server-instructions.js.map +1 -1
  81. package/dist/server.js +26 -18
  82. package/dist/server.js.map +1 -1
  83. package/dist/setup.d.ts +14 -0
  84. package/dist/setup.js +132 -10
  85. package/dist/setup.js.map +1 -1
  86. package/dist/store/federation.d.ts +126 -2
  87. package/dist/store/federation.js +719 -2
  88. package/dist/store/federation.js.map +1 -1
  89. package/dist/store/merge.d.ts +11 -0
  90. package/dist/store/merge.js +38 -8
  91. package/dist/store/merge.js.map +1 -1
  92. package/dist/store/session-state.d.ts +12 -0
  93. package/dist/store/session-state.js +27 -1
  94. package/dist/store/session-state.js.map +1 -1
  95. package/dist/store/sync-manager.d.ts +7 -5
  96. package/dist/store/sync-manager.js +134 -58
  97. package/dist/store/sync-manager.js.map +1 -1
  98. package/dist/tools/claim-tools.d.ts +7 -1
  99. package/dist/tools/claim-tools.js +328 -178
  100. package/dist/tools/claim-tools.js.map +1 -1
  101. package/dist/tools/decision-tools.d.ts +3 -2
  102. package/dist/tools/decision-tools.js +48 -131
  103. package/dist/tools/decision-tools.js.map +1 -1
  104. package/dist/tools/episode-tools.d.ts +2 -3
  105. package/dist/tools/episode-tools.js +59 -130
  106. package/dist/tools/episode-tools.js.map +1 -1
  107. package/dist/tools/export-tools.d.ts +7 -1
  108. package/dist/tools/export-tools.js +90 -68
  109. package/dist/tools/export-tools.js.map +1 -1
  110. package/dist/tools/get-tools.d.ts +11 -0
  111. package/dist/tools/get-tools.js +282 -0
  112. package/dist/tools/get-tools.js.map +1 -0
  113. package/dist/tools/groom-tools.d.ts +13 -0
  114. package/dist/tools/groom-tools.js +197 -0
  115. package/dist/tools/groom-tools.js.map +1 -0
  116. package/dist/tools/helpers.d.ts +10 -0
  117. package/dist/tools/helpers.js +10 -0
  118. package/dist/tools/helpers.js.map +1 -0
  119. package/dist/tools/insight-tools.d.ts +11 -0
  120. package/dist/tools/insight-tools.js +167 -0
  121. package/dist/tools/insight-tools.js.map +1 -0
  122. package/dist/tools/knowledge-tools.d.ts +7 -0
  123. package/dist/tools/knowledge-tools.js +151 -0
  124. package/dist/tools/knowledge-tools.js.map +1 -0
  125. package/dist/tools/maintenance-tools.d.ts +11 -0
  126. package/dist/tools/maintenance-tools.js +1074 -0
  127. package/dist/tools/maintenance-tools.js.map +1 -0
  128. package/dist/tools/memo-tools.d.ts +15 -0
  129. package/dist/tools/memo-tools.js +370 -0
  130. package/dist/tools/memo-tools.js.map +1 -0
  131. package/dist/tools/model-tools.d.ts +11 -0
  132. package/dist/tools/model-tools.js +175 -0
  133. package/dist/tools/model-tools.js.map +1 -0
  134. package/dist/tools/profile-tools.d.ts +13 -1
  135. package/dist/tools/profile-tools.js +80 -113
  136. package/dist/tools/profile-tools.js.map +1 -1
  137. package/dist/tools/promote-tools.js +91 -4
  138. package/dist/tools/promote-tools.js.map +1 -1
  139. package/dist/tools/query-tools.d.ts +13 -1
  140. package/dist/tools/query-tools.js +358 -121
  141. package/dist/tools/query-tools.js.map +1 -1
  142. package/dist/tools/store-tools.d.ts +5 -2
  143. package/dist/tools/store-tools.js +94 -74
  144. package/dist/tools/store-tools.js.map +1 -1
  145. package/dist/tools/theory-tools.js +24 -0
  146. package/dist/tools/theory-tools.js.map +1 -1
  147. package/dist/types.d.ts +641 -229
  148. package/dist/types.js +186 -134
  149. package/dist/types.js.map +1 -1
  150. package/misc/work-notes/main 20260224.txt +98 -0
  151. package/misc/work-notes/plans/20260220084456_UNKNOWN_Issue-#24-Episode/343/202/277/343/202/244/343/203/210/343/203/253/343/201/256-Qiita/346/226/271/345/274/217/343/202/254/343/202/244.md +52 -0
  152. package/misc/work-notes/plans/20260220095602_UNKNOWN_Issue-#10-/346/233/270/343/201/215/350/276/274/343/201/277/346/231/202/343/201/256/343/202/263/343/203/263/343/203/206/343/203/263/343/203/204Lint/346/251/237.md +120 -0
  153. package/misc/work-notes/plans/20260220100439_UNKNOWN_Issue-#10-/346/233/270/343/201/215/350/276/274/343/201/277/346/231/202/343/201/256/343/202/263/343/203/263/343/203/206/343/203/263/343/203/204Lint/346/251/237.md +156 -0
  154. package/misc/work-notes/plans/20260220110445_UNKNOWN_Issue-#11-/343/203/227/343/203/255/343/203/225/343/202/241/343/202/244/343/203/253/350/246/201/347/264/204/343/201/256/343/202/271/343/202/263/343/202/242/343/203/252/303/243.md +154 -0
  155. package/misc/work-notes/plans/20260220114035_UNKNOWN_Issue-#15-/343/202/263/343/203/263/343/203/206/343/202/255/343/202/271/343/203/210/344/272/210/347/256/227API/357/274/210token-budg.md +178 -0
  156. package/misc/work-notes/plans/20260220133902_UNKNOWN_Issue-#16-/343/202/257/343/203/251/343/202/244/343/202/242/343/203/263/343/203/210/345/210/245/343/203/235/343/203/252/343/202/267/343/203/274/345/261/244/357/274/210r.md +284 -0
  157. package/misc/work-notes/plans/20260220151356_UNKNOWN_Issue-#28-V9/343/203/236/343/202/244/343/202/260/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/347/265/261/345/220/210/357/274/210#23-+.md +178 -0
  158. package/misc/work-notes/plans/20260227042805_UNKNOWN_config.json-/343/202/254/343/202/244/343/203/211/346/225/264/345/202/231-+-MCP-resourceprompt-/303/250.md +90 -0
  159. package/misc/work-notes/plans/20260302113135_UNKNOWN_MCP/343/203/204/343/203/274/343/203/253/343/203/210/343/203/274/343/202/257/343/203/263/343/203/200/343/202/244/343/202/250/343/203/203/343/203/210-(~17,370-/303/242.md +152 -0
  160. package/misc/work-notes/plans/20260302151940_UNKNOWN_README.md-/347/217/276/350/241/214API/345/217/215/346/230/240/343/202/242/343/203/203/343/203/227/343/203/207/343/203/274/343/203/210.md +87 -0
  161. package/misc/work-notes/plans/20260302173113_UNKNOWN_watashi-manage-claim-/343/202/250/343/203/251/343/203/274/346/224/271/345/226/204-+-search-summ.md +70 -0
  162. package/misc/work-notes/plans/20260303091511_UNKNOWN_Issue-#60-entity-type-/347/234/201/347/225/245/345/257/276/345/277/234-/342/200/224-ID/345/206/205/351/203/250/350/247/243.md +205 -0
  163. package/misc/work-notes/plans/20260304152700_UNKNOWN_Issue-#61-setup-/343/202/263/343/203/236/343/203/263/343/203/211/343/201/247-MCP-/343/202/265/343/203/274/343/203/220/343/203/274/303/247/302/231.md +67 -0
  164. package/misc/work-notes/plans/20260304170153_UNKNOWN_Issue-#54-user-memoplanissue-/343/201/256-content-/342/206/222-l1-co.md +76 -0
  165. package/package.json +12 -2
  166. package/scripts/backfill-search-summary.mjs +219 -0
  167. package/scripts/migrate-theories.mjs +231 -0
  168. package/{watashi-db-cowork-plugin.zip → watashi-db-plugin.zip} +0 -0
  169. package/dist/config/instance.d.ts +0 -10
  170. package/dist/config/instance.js +0 -39
  171. package/dist/config/instance.js.map +0 -1
  172. package/dist/config/types.d.ts +0 -116
  173. package/dist/config/types.js +0 -37
  174. package/dist/config/types.js.map +0 -1
@@ -11,7 +11,24 @@
11
11
  // 2026-02-17 修正: V7追加(昇格ツール: promoted status + audit_log拡張)
12
12
  // 2026-02-20 修正: V8追加(プロファイルスコアリング: hit_count/last_hit_at カラム追加)
13
13
  // 2026-02-20 修正: V9追加(V9マイグレーション統合: Episode FTS拡張 + search_summary + Theory新設 + タイムスタンプ統一)
14
- const SCHEMA_VERSION = 9;
14
+ // 2026-02-22 修正: V10追加(FTS5 trigram tokenizer 対応 — 日本語検索改善 Issue #20)
15
+ // 2026-02-22 修正: V11追加(Embedding BLOBカラム追加 — ベクトル検索 Issue #29)
16
+ // 2026-02-23 修正: V12追加(エンティティ分類体系の再設計 Issue #40)
17
+ // 元の実装: SCHEMA_VERSION = 11
18
+ // 2026-02-25 修正: V13追加(横断検索テーブル Issue #30)
19
+ // 2026-02-25 修正: V14追加(evidence_refs カラム追加 Issue #44)
20
+ // 元の実装: SCHEMA_VERSION = 13
21
+ // 2026-02-25 修正: V15追加(Memoテーブル新設 Issue #45)
22
+ // 2026-02-26 修正: V16追加(memos → user_memos リネーム, audit_log CHECK拡張 — Issue #45 Phase 1)
23
+ // 2026-02-28 修正: V17追加(user_input カラム追加 — Issue #56)
24
+ // 2026-02-28 修正: V18追加(user_plans + user_issues テーブル新設 — Issue #57)
25
+ // 2026-03-01 修正: V19追加(Insight l1/l2 拡張 — Issue #53)
26
+ // 2026-03-01 修正: V20追加(全エンティティ l1/l2 カラム拡張 — Issue #59)
27
+ // 2026-03-03 修正: V21追加(audit_log に client_id カラム追加 + backfill)
28
+ // 2026-03-03 修正: V22追加(entity_id インデックス追加 — Issue #60)
29
+ // 2026-03-03 修正: V23追加(Claim に l1_content 追加 + embedding → l1_embedding リネーム — Issue #64)
30
+ // 2026-03-04 修正: V24追加(user_memo/plan/issue の content → l1_content + user_memos に user_input 追加 — Issue #54)
31
+ const SCHEMA_VERSION = 24;
15
32
  /**
16
33
  * データベーススキーマの初期化とマイグレーション
17
34
  */
@@ -52,6 +69,51 @@ export function initializeSchema(db) {
52
69
  if (version < 9) {
53
70
  applyV9(db);
54
71
  }
72
+ if (version < 10) {
73
+ applyV10(db);
74
+ }
75
+ if (version < 11) {
76
+ applyV11(db);
77
+ }
78
+ if (version < 12) {
79
+ applyV12(db);
80
+ }
81
+ if (version < 13) {
82
+ applyV13(db);
83
+ }
84
+ if (version < 14) {
85
+ applyV14(db);
86
+ }
87
+ if (version < 15) {
88
+ applyV15(db);
89
+ }
90
+ if (version < 16) {
91
+ applyV16(db);
92
+ }
93
+ if (version < 17) {
94
+ applyV17(db);
95
+ }
96
+ if (version < 18) {
97
+ applyV18(db);
98
+ }
99
+ if (version < 19) {
100
+ applyV19(db);
101
+ }
102
+ if (version < 20) {
103
+ applyV20(db);
104
+ }
105
+ if (version < 21) {
106
+ applyV21(db);
107
+ }
108
+ if (version < 22) {
109
+ applyV22(db);
110
+ }
111
+ if (version < 23) {
112
+ applyV23(db);
113
+ }
114
+ if (version < 24) {
115
+ applyV24(db);
116
+ }
55
117
  }
56
118
  /**
57
119
  * V1スキーマ: 初期テーブル構成
@@ -1184,6 +1246,4142 @@ function applyV9(db) {
1184
1246
  }
1185
1247
  }
1186
1248
  }
1249
+ /**
1250
+ * V10マイグレーション: FTS5 trigram tokenizer への移行(Issue #20)
1251
+ * unicode61 → trigram に変更することで日本語CJK文字の部分文字列検索が可能になる。
1252
+ * SQLite 3.34.0+ で trigram tokenizer が利用可能(better-sqlite3 同梱の 3.49.2 で対応済み)。
1253
+ *
1254
+ * 手順:
1255
+ * 1. 12個のFTSトリガーをDROP
1256
+ * 2. 4つのFTSテーブルをDROP
1257
+ * 3. tokenize='trigram' で4テーブルを再作成(カラム構成はV9と同一)
1258
+ * 4. 12個のトリガーを再作成(V9と同構造)
1259
+ * 5. 4テーブルの rebuild コマンド実行
1260
+ * 6. schema_version (10) を記録
1261
+ */
1262
+ function applyV10(db) {
1263
+ const transaction = db.transaction(() => {
1264
+ // --- 1. 既存FTSトリガーをDROP ---
1265
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_insert`);
1266
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_update`);
1267
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_delete`);
1268
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_insert`);
1269
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_update`);
1270
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_delete`);
1271
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_insert`);
1272
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_update`);
1273
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_delete`);
1274
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_insert`);
1275
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_update`);
1276
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_delete`);
1277
+ // --- 2. 既存FTSテーブルをDROP ---
1278
+ db.exec(`DROP TABLE IF EXISTS claims_fts`);
1279
+ db.exec(`DROP TABLE IF EXISTS episodes_fts`);
1280
+ db.exec(`DROP TABLE IF EXISTS decisions_fts`);
1281
+ db.exec(`DROP TABLE IF EXISTS theories_fts`);
1282
+ // --- 3. trigram tokenizer でFTSテーブルを再作成 ---
1283
+ db.exec(`
1284
+ CREATE VIRTUAL TABLE claims_fts USING fts5(
1285
+ subject, predicate, object, evidence, search_summary,
1286
+ content='claims',
1287
+ content_rowid='rowid',
1288
+ tokenize='trigram'
1289
+ )
1290
+ `);
1291
+ db.exec(`
1292
+ CREATE VIRTUAL TABLE episodes_fts USING fts5(
1293
+ title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary,
1294
+ content='episodes',
1295
+ content_rowid='rowid',
1296
+ tokenize='trigram'
1297
+ )
1298
+ `);
1299
+ db.exec(`
1300
+ CREATE VIRTUAL TABLE decisions_fts USING fts5(
1301
+ title, description, reasoning, search_summary,
1302
+ content='decisions',
1303
+ content_rowid='rowid',
1304
+ tokenize='trigram'
1305
+ )
1306
+ `);
1307
+ db.exec(`
1308
+ CREATE VIRTUAL TABLE theories_fts USING fts5(
1309
+ title, description, core_thesis, principles, tags, search_summary,
1310
+ content='theories',
1311
+ content_rowid='rowid',
1312
+ tokenize='trigram'
1313
+ )
1314
+ `);
1315
+ // --- 4. FTSトリガー再作成(V9と同構造) ---
1316
+ // claims FTSトリガー
1317
+ db.exec(`
1318
+ CREATE TRIGGER claims_fts_insert AFTER INSERT ON claims BEGIN
1319
+ INSERT INTO claims_fts(rowid, subject, predicate, object, evidence, search_summary)
1320
+ VALUES (new.rowid, new.subject, new.predicate, new.object, new.evidence, new.search_summary);
1321
+ END
1322
+ `);
1323
+ db.exec(`
1324
+ CREATE TRIGGER claims_fts_update AFTER UPDATE ON claims BEGIN
1325
+ INSERT INTO claims_fts(claims_fts, rowid, subject, predicate, object, evidence, search_summary)
1326
+ VALUES ('delete', old.rowid, old.subject, old.predicate, old.object, old.evidence, old.search_summary);
1327
+ INSERT INTO claims_fts(rowid, subject, predicate, object, evidence, search_summary)
1328
+ VALUES (new.rowid, new.subject, new.predicate, new.object, new.evidence, new.search_summary);
1329
+ END
1330
+ `);
1331
+ db.exec(`
1332
+ CREATE TRIGGER claims_fts_delete AFTER DELETE ON claims BEGIN
1333
+ INSERT INTO claims_fts(claims_fts, rowid, subject, predicate, object, evidence, search_summary)
1334
+ VALUES ('delete', old.rowid, old.subject, old.predicate, old.object, old.evidence, old.search_summary);
1335
+ END
1336
+ `);
1337
+ // episodes FTSトリガー(JSON配列クリーニング付き)
1338
+ db.exec(`
1339
+ CREATE TRIGGER episodes_fts_insert AFTER INSERT ON episodes BEGIN
1340
+ INSERT INTO episodes_fts(rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary)
1341
+ VALUES (new.rowid, new.title, new.context, new.trigger,
1342
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1343
+ replace(replace(replace(new.problems, '["',''), '"]',''), '","',' '),
1344
+ replace(replace(replace(new.desires, '["',''), '"]',''), '","',' '),
1345
+ replace(replace(replace(new.decisions, '["',''), '"]',''), '","',' '),
1346
+ replace(replace(replace(new.outcomes, '["',''), '"]',''), '","',' '),
1347
+ new.search_summary);
1348
+ END
1349
+ `);
1350
+ db.exec(`
1351
+ CREATE TRIGGER episodes_fts_update AFTER UPDATE ON episodes BEGIN
1352
+ INSERT INTO episodes_fts(episodes_fts, rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary)
1353
+ VALUES ('delete', old.rowid, old.title, old.context, old.trigger,
1354
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1355
+ replace(replace(replace(old.problems, '["',''), '"]',''), '","',' '),
1356
+ replace(replace(replace(old.desires, '["',''), '"]',''), '","',' '),
1357
+ replace(replace(replace(old.decisions, '["',''), '"]',''), '","',' '),
1358
+ replace(replace(replace(old.outcomes, '["',''), '"]',''), '","',' '),
1359
+ old.search_summary);
1360
+ INSERT INTO episodes_fts(rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary)
1361
+ VALUES (new.rowid, new.title, new.context, new.trigger,
1362
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1363
+ replace(replace(replace(new.problems, '["',''), '"]',''), '","',' '),
1364
+ replace(replace(replace(new.desires, '["',''), '"]',''), '","',' '),
1365
+ replace(replace(replace(new.decisions, '["',''), '"]',''), '","',' '),
1366
+ replace(replace(replace(new.outcomes, '["',''), '"]',''), '","',' '),
1367
+ new.search_summary);
1368
+ END
1369
+ `);
1370
+ db.exec(`
1371
+ CREATE TRIGGER episodes_fts_delete AFTER DELETE ON episodes BEGIN
1372
+ INSERT INTO episodes_fts(episodes_fts, rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary)
1373
+ VALUES ('delete', old.rowid, old.title, old.context, old.trigger,
1374
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1375
+ replace(replace(replace(old.problems, '["',''), '"]',''), '","',' '),
1376
+ replace(replace(replace(old.desires, '["',''), '"]',''), '","',' '),
1377
+ replace(replace(replace(old.decisions, '["',''), '"]',''), '","',' '),
1378
+ replace(replace(replace(old.outcomes, '["',''), '"]',''), '","',' '),
1379
+ old.search_summary);
1380
+ END
1381
+ `);
1382
+ // decisions FTSトリガー
1383
+ db.exec(`
1384
+ CREATE TRIGGER decisions_fts_insert AFTER INSERT ON decisions BEGIN
1385
+ INSERT INTO decisions_fts(rowid, title, description, reasoning, search_summary)
1386
+ VALUES (new.rowid, new.title, new.description, new.reasoning, new.search_summary);
1387
+ END
1388
+ `);
1389
+ db.exec(`
1390
+ CREATE TRIGGER decisions_fts_update AFTER UPDATE ON decisions BEGIN
1391
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, reasoning, search_summary)
1392
+ VALUES ('delete', old.rowid, old.title, old.description, old.reasoning, old.search_summary);
1393
+ INSERT INTO decisions_fts(rowid, title, description, reasoning, search_summary)
1394
+ VALUES (new.rowid, new.title, new.description, new.reasoning, new.search_summary);
1395
+ END
1396
+ `);
1397
+ db.exec(`
1398
+ CREATE TRIGGER decisions_fts_delete AFTER DELETE ON decisions BEGIN
1399
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, reasoning, search_summary)
1400
+ VALUES ('delete', old.rowid, old.title, old.description, old.reasoning, old.search_summary);
1401
+ END
1402
+ `);
1403
+ // theories FTSトリガー(JSON配列クリーニング付き)
1404
+ db.exec(`
1405
+ CREATE TRIGGER theories_fts_insert AFTER INSERT ON theories BEGIN
1406
+ INSERT INTO theories_fts(rowid, title, description, core_thesis, principles, tags, search_summary)
1407
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1408
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1409
+ replace(replace(replace(new.tags, '["',''), '"]',''), '","',' '),
1410
+ new.search_summary);
1411
+ END
1412
+ `);
1413
+ db.exec(`
1414
+ CREATE TRIGGER theories_fts_update AFTER UPDATE ON theories BEGIN
1415
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, core_thesis, principles, tags, search_summary)
1416
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1417
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1418
+ replace(replace(replace(old.tags, '["',''), '"]',''), '","',' '),
1419
+ old.search_summary);
1420
+ INSERT INTO theories_fts(rowid, title, description, core_thesis, principles, tags, search_summary)
1421
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1422
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1423
+ replace(replace(replace(new.tags, '["',''), '"]',''), '","',' '),
1424
+ new.search_summary);
1425
+ END
1426
+ `);
1427
+ db.exec(`
1428
+ CREATE TRIGGER theories_fts_delete AFTER DELETE ON theories BEGIN
1429
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, core_thesis, principles, tags, search_summary)
1430
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1431
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1432
+ replace(replace(replace(old.tags, '["',''), '"]',''), '","',' '),
1433
+ old.search_summary);
1434
+ END
1435
+ `);
1436
+ // --- 5. 全FTS rebuild ---
1437
+ db.exec(`INSERT INTO claims_fts(claims_fts) VALUES('rebuild')`);
1438
+ db.exec(`INSERT INTO episodes_fts(episodes_fts) VALUES('rebuild')`);
1439
+ db.exec(`INSERT INTO decisions_fts(decisions_fts) VALUES('rebuild')`);
1440
+ db.exec(`INSERT INTO theories_fts(theories_fts) VALUES('rebuild')`);
1441
+ // --- 6. スキーマバージョン記録 ---
1442
+ db.exec(`INSERT INTO schema_version (version) VALUES (10)`);
1443
+ });
1444
+ transaction();
1445
+ }
1446
+ /**
1447
+ * V11スキーマ: Embedding BLOBカラム追加(Issue #29: ベクトル検索)
1448
+ * claims, decisions, episodes, theories の4テーブルに embedding BLOB カラムを追加。
1449
+ * NULL許容(Embedding未設定時やAPI失敗時は NULL のまま動作)。
1450
+ */
1451
+ function applyV11(db) {
1452
+ const transaction = db.transaction(() => {
1453
+ // 各テーブルに embedding カラム追加(存在しない場合のみ)
1454
+ if (!hasColumn(db, "claims", "embedding")) {
1455
+ db.exec(`ALTER TABLE claims ADD COLUMN embedding BLOB`);
1456
+ }
1457
+ if (!hasColumn(db, "decisions", "embedding")) {
1458
+ db.exec(`ALTER TABLE decisions ADD COLUMN embedding BLOB`);
1459
+ }
1460
+ if (!hasColumn(db, "episodes", "embedding")) {
1461
+ db.exec(`ALTER TABLE episodes ADD COLUMN embedding BLOB`);
1462
+ }
1463
+ if (!hasColumn(db, "theories", "embedding")) {
1464
+ db.exec(`ALTER TABLE theories ADD COLUMN embedding BLOB`);
1465
+ }
1466
+ // スキーマバージョン記録
1467
+ db.exec(`INSERT INTO schema_version (version) VALUES (11)`);
1468
+ });
1469
+ transaction();
1470
+ }
1471
+ /**
1472
+ * V12スキーマ: エンティティ分類体系の再設計(Issue #40)
1473
+ * - insights テーブル新規作成(Theoryから non_goals, open_questions を除外した軽量版)
1474
+ * - models テーブル新規作成(Theoryと同構造)
1475
+ * - episodes に groomed_at カラム追加
1476
+ * - insights_fts / models_fts(trigram tokenizer)
1477
+ * - FTS同期トリガー(INSERT/UPDATE/DELETE × 2テーブル)
1478
+ * - audit_log の entity_type CHECK に 'insight','model' 追加
1479
+ */
1480
+ function applyV12(db) {
1481
+ const fkState = db.pragma("foreign_keys");
1482
+ db.pragma("foreign_keys = OFF");
1483
+ try {
1484
+ const transaction = db.transaction(() => {
1485
+ // --- 1. insights テーブル(Theoryから non_goals, open_questions を除外) ---
1486
+ db.exec(`
1487
+ CREATE TABLE IF NOT EXISTS insights (
1488
+ id TEXT PRIMARY KEY,
1489
+ title TEXT NOT NULL,
1490
+ description TEXT,
1491
+ core_thesis TEXT,
1492
+ principles TEXT NOT NULL DEFAULT '[]',
1493
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
1494
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
1495
+ tags TEXT NOT NULL DEFAULT '[]',
1496
+ scope TEXT NOT NULL DEFAULT 'global',
1497
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
1498
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
1499
+ search_summary TEXT,
1500
+ embedding BLOB,
1501
+ promoted_from_store TEXT,
1502
+ promoted_from_id TEXT,
1503
+ source_tool TEXT,
1504
+ session_id TEXT,
1505
+ client_name TEXT,
1506
+ client_version TEXT,
1507
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1508
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1509
+ )
1510
+ `);
1511
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_status ON insights(status)`);
1512
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_scope ON insights(scope)`);
1513
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`);
1514
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_updated ON insights(updated_at)`);
1515
+ db.exec(`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`);
1516
+ // --- 2. models テーブル(Theoryと同構造) ---
1517
+ db.exec(`
1518
+ CREATE TABLE IF NOT EXISTS models (
1519
+ id TEXT PRIMARY KEY,
1520
+ title TEXT NOT NULL,
1521
+ description TEXT,
1522
+ core_thesis TEXT,
1523
+ principles TEXT NOT NULL DEFAULT '[]',
1524
+ non_goals TEXT NOT NULL DEFAULT '[]',
1525
+ open_questions TEXT NOT NULL DEFAULT '[]',
1526
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
1527
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
1528
+ tags TEXT NOT NULL DEFAULT '[]',
1529
+ scope TEXT NOT NULL DEFAULT 'global',
1530
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
1531
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
1532
+ search_summary TEXT,
1533
+ embedding BLOB,
1534
+ promoted_from_store TEXT,
1535
+ promoted_from_id TEXT,
1536
+ source_tool TEXT,
1537
+ session_id TEXT,
1538
+ client_name TEXT,
1539
+ client_version TEXT,
1540
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1541
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1542
+ )
1543
+ `);
1544
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_models_status ON models(status)`);
1545
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_models_scope ON models(scope)`);
1546
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_models_created ON models(created_at)`);
1547
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_models_updated ON models(updated_at)`);
1548
+ db.exec(`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`);
1549
+ // --- 3. episodes に groomed_at カラム追加 ---
1550
+ if (!hasColumn(db, "episodes", "groomed_at")) {
1551
+ db.exec(`ALTER TABLE episodes ADD COLUMN groomed_at TEXT`);
1552
+ }
1553
+ // --- 4. insights_fts / models_fts(trigram tokenizer) ---
1554
+ db.exec(`DROP TABLE IF EXISTS insights_fts`);
1555
+ db.exec(`
1556
+ CREATE VIRTUAL TABLE insights_fts USING fts5(
1557
+ title, description, core_thesis, principles, tags, search_summary,
1558
+ content='insights',
1559
+ content_rowid='rowid',
1560
+ tokenize='trigram'
1561
+ )
1562
+ `);
1563
+ db.exec(`DROP TABLE IF EXISTS models_fts`);
1564
+ db.exec(`
1565
+ CREATE VIRTUAL TABLE models_fts USING fts5(
1566
+ title, description, core_thesis, principles, tags, search_summary,
1567
+ content='models',
1568
+ content_rowid='rowid',
1569
+ tokenize='trigram'
1570
+ )
1571
+ `);
1572
+ // --- 5. insights FTS同期トリガー ---
1573
+ db.exec(`
1574
+ CREATE TRIGGER IF NOT EXISTS insights_fts_insert AFTER INSERT ON insights BEGIN
1575
+ INSERT INTO insights_fts(rowid, title, description, core_thesis, principles, tags, search_summary)
1576
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1577
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1578
+ replace(replace(replace(new.tags, '["',''), '"]',''), '","',' '),
1579
+ new.search_summary);
1580
+ END
1581
+ `);
1582
+ db.exec(`
1583
+ CREATE TRIGGER IF NOT EXISTS insights_fts_update AFTER UPDATE ON insights BEGIN
1584
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, core_thesis, principles, tags, search_summary)
1585
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1586
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1587
+ replace(replace(replace(old.tags, '["',''), '"]',''), '","',' '),
1588
+ old.search_summary);
1589
+ INSERT INTO insights_fts(rowid, title, description, core_thesis, principles, tags, search_summary)
1590
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1591
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1592
+ replace(replace(replace(new.tags, '["',''), '"]',''), '","',' '),
1593
+ new.search_summary);
1594
+ END
1595
+ `);
1596
+ db.exec(`
1597
+ CREATE TRIGGER IF NOT EXISTS insights_fts_delete AFTER DELETE ON insights BEGIN
1598
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, core_thesis, principles, tags, search_summary)
1599
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1600
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1601
+ replace(replace(replace(old.tags, '["',''), '"]',''), '","',' '),
1602
+ old.search_summary);
1603
+ END
1604
+ `);
1605
+ // --- 6. models FTS同期トリガー ---
1606
+ db.exec(`
1607
+ CREATE TRIGGER IF NOT EXISTS models_fts_insert AFTER INSERT ON models BEGIN
1608
+ INSERT INTO models_fts(rowid, title, description, core_thesis, principles, tags, search_summary)
1609
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1610
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1611
+ replace(replace(replace(new.tags, '["',''), '"]',''), '","',' '),
1612
+ new.search_summary);
1613
+ END
1614
+ `);
1615
+ db.exec(`
1616
+ CREATE TRIGGER IF NOT EXISTS models_fts_update AFTER UPDATE ON models BEGIN
1617
+ INSERT INTO models_fts(models_fts, rowid, title, description, core_thesis, principles, tags, search_summary)
1618
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1619
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1620
+ replace(replace(replace(old.tags, '["',''), '"]',''), '","',' '),
1621
+ old.search_summary);
1622
+ INSERT INTO models_fts(rowid, title, description, core_thesis, principles, tags, search_summary)
1623
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1624
+ replace(replace(replace(new.principles, '["',''), '"]',''), '","',' '),
1625
+ replace(replace(replace(new.tags, '["',''), '"]',''), '","',' '),
1626
+ new.search_summary);
1627
+ END
1628
+ `);
1629
+ db.exec(`
1630
+ CREATE TRIGGER IF NOT EXISTS models_fts_delete AFTER DELETE ON models BEGIN
1631
+ INSERT INTO models_fts(models_fts, rowid, title, description, core_thesis, principles, tags, search_summary)
1632
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1633
+ replace(replace(replace(old.principles, '["',''), '"]',''), '","',' '),
1634
+ replace(replace(replace(old.tags, '["',''), '"]',''), '","',' '),
1635
+ old.search_summary);
1636
+ END
1637
+ `);
1638
+ // --- 7. audit_log テーブル再作成(entity_type CHECK に 'insight','model' 追加) ---
1639
+ db.exec(`
1640
+ CREATE TABLE audit_log_new (
1641
+ id TEXT PRIMARY KEY,
1642
+ operation TEXT NOT NULL CHECK(operation IN (
1643
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
1644
+ )),
1645
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model')),
1646
+ entity_id TEXT NOT NULL,
1647
+ summary TEXT NOT NULL,
1648
+ details TEXT,
1649
+ client_name TEXT,
1650
+ client_version TEXT,
1651
+ session_id TEXT,
1652
+ source_tool TEXT,
1653
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1654
+ )
1655
+ `);
1656
+ db.exec(`INSERT INTO audit_log_new SELECT * FROM audit_log`);
1657
+ db.exec(`DROP TABLE audit_log`);
1658
+ db.exec(`ALTER TABLE audit_log_new RENAME TO audit_log`);
1659
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id)`);
1660
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation)`);
1661
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`);
1662
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id)`);
1663
+ // --- 8. FTS rebuild ---
1664
+ db.exec(`INSERT INTO insights_fts(insights_fts) VALUES('rebuild')`);
1665
+ db.exec(`INSERT INTO models_fts(models_fts) VALUES('rebuild')`);
1666
+ // --- 9. 外部キー整合性チェック ---
1667
+ const fkViolations = db.pragma("foreign_key_check");
1668
+ if (fkViolations.length > 0) {
1669
+ throw new Error(`V12マイグレーション後に外部キー違反が検出されました: ${JSON.stringify(fkViolations)}`);
1670
+ }
1671
+ // --- 10. スキーマバージョン記録 ---
1672
+ db.exec(`INSERT INTO schema_version (version) VALUES (12)`);
1673
+ });
1674
+ transaction();
1675
+ }
1676
+ finally {
1677
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
1678
+ db.pragma("foreign_keys = ON");
1679
+ }
1680
+ }
1681
+ }
1682
+ /**
1683
+ * V13スキーマ: 横断検索テーブル (Issue #30)
1684
+ * unified_search_items + unified_search_fts (trigram)
1685
+ * 全6エンティティを横断してFTS5で一括検索するための2次インデックス。
1686
+ * 元テーブルのINSERT/UPDATE/DELETEトリガーで自動同期。
1687
+ */
1688
+ function applyV13(db) {
1689
+ const fkState = db.pragma("foreign_keys");
1690
+ db.pragma("foreign_keys = OFF");
1691
+ try {
1692
+ const transaction = db.transaction(() => {
1693
+ // --- 1. unified_search_items テーブル ---
1694
+ db.exec(`
1695
+ CREATE TABLE IF NOT EXISTS unified_search_items (
1696
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model')),
1697
+ entity_id TEXT NOT NULL,
1698
+ scope TEXT NOT NULL DEFAULT 'global',
1699
+ category TEXT,
1700
+ title_summary TEXT NOT NULL,
1701
+ search_text TEXT NOT NULL,
1702
+ search_summary TEXT,
1703
+ tags TEXT NOT NULL DEFAULT '[]',
1704
+ embedding BLOB,
1705
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1706
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1707
+ PRIMARY KEY (entity_type, entity_id)
1708
+ )
1709
+ `);
1710
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope)`);
1711
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type)`);
1712
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_updated ON unified_search_items(updated_at)`);
1713
+ // --- 2. unified_search_fts(trigram tokenizer) ---
1714
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
1715
+ db.exec(`
1716
+ CREATE VIRTUAL TABLE unified_search_fts USING fts5(
1717
+ title_summary, search_text, search_summary,
1718
+ content='unified_search_items',
1719
+ content_rowid='rowid',
1720
+ tokenize='trigram'
1721
+ )
1722
+ `);
1723
+ // --- 3. FTS同期トリガー(unified_search_items → unified_search_fts) ---
1724
+ db.exec(`
1725
+ CREATE TRIGGER IF NOT EXISTS unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
1726
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
1727
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
1728
+ END
1729
+ `);
1730
+ db.exec(`
1731
+ CREATE TRIGGER IF NOT EXISTS unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
1732
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
1733
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
1734
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
1735
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
1736
+ END
1737
+ `);
1738
+ db.exec(`
1739
+ CREATE TRIGGER IF NOT EXISTS unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
1740
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
1741
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
1742
+ END
1743
+ `);
1744
+ // --- 4. Source table sync triggers ---
1745
+ // JSON配列をスペース区切りテキストに変換するSQLヘルパー
1746
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
1747
+ const coal = (col) => `COALESCE(${col}, '')`;
1748
+ const entities = [
1749
+ {
1750
+ type: "claim",
1751
+ table: "claims",
1752
+ category: (p) => `${p}category`,
1753
+ title: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object`,
1754
+ search: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}search_summary`)}`,
1755
+ searchSummary: (p) => `${p}search_summary`,
1756
+ tags: () => `'[]'`,
1757
+ },
1758
+ {
1759
+ type: "episode",
1760
+ table: "episodes",
1761
+ category: () => `NULL`,
1762
+ title: (p) => `${p}title`,
1763
+ search: (p) => `${p}title || ' ' || ${coal(`${p}context`)} || ' ' || ${coal(`${p}trigger`)} || ' ' || ${jc(`${p}problems`)} || ' ' || ${jc(`${p}desires`)} || ' ' || ${jc(`${p}decisions`)} || ' ' || ${jc(`${p}outcomes`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
1764
+ searchSummary: (p) => `${p}search_summary`,
1765
+ tags: (p) => `${p}tags`,
1766
+ },
1767
+ {
1768
+ type: "decision",
1769
+ table: "decisions",
1770
+ category: () => `NULL`,
1771
+ title: (p) => `${p}title`,
1772
+ search: (p) => `${p}title || ' ' || ${p}description || ' ' || ${p}reasoning || ' ' || ${jc(`${p}alternatives`)} || ' ' || ${coal(`${p}search_summary`)}`,
1773
+ searchSummary: (p) => `${p}search_summary`,
1774
+ tags: () => `'[]'`,
1775
+ },
1776
+ {
1777
+ type: "theory",
1778
+ table: "theories",
1779
+ category: () => `NULL`,
1780
+ title: (p) => `${p}title`,
1781
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
1782
+ searchSummary: (p) => `${p}search_summary`,
1783
+ tags: (p) => `${p}tags`,
1784
+ },
1785
+ {
1786
+ type: "insight",
1787
+ table: "insights",
1788
+ category: () => `NULL`,
1789
+ title: (p) => `${p}title`,
1790
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
1791
+ searchSummary: (p) => `${p}search_summary`,
1792
+ tags: (p) => `${p}tags`,
1793
+ },
1794
+ {
1795
+ type: "model",
1796
+ table: "models",
1797
+ category: () => `NULL`,
1798
+ title: (p) => `${p}title`,
1799
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
1800
+ searchSummary: (p) => `${p}search_summary`,
1801
+ tags: (p) => `${p}tags`,
1802
+ },
1803
+ ];
1804
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
1805
+ for (const e of entities) {
1806
+ // INSERT trigger: アクティブなレコードのみ横断検索に追加
1807
+ db.exec(`
1808
+ CREATE TRIGGER IF NOT EXISTS ${e.table}_unified_insert AFTER INSERT ON ${e.table}
1809
+ WHEN new.status = 'active' BEGIN
1810
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1811
+ VALUES ('${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, ${e.searchSummary("new.")}, ${e.tags("new.")}, new.created_at, new.updated_at);
1812
+ END
1813
+ `);
1814
+ // UPDATE trigger: 旧エントリを削除 + アクティブなら再挿入(単一トリガーで実行順序を保証)
1815
+ db.exec(`
1816
+ CREATE TRIGGER IF NOT EXISTS ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
1817
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
1818
+ INSERT INTO unified_search_items(${columns})
1819
+ SELECT '${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, ${e.searchSummary("new.")}, ${e.tags("new.")}, new.created_at, new.updated_at
1820
+ WHERE new.status = 'active';
1821
+ END
1822
+ `);
1823
+ // DELETE trigger: 横断検索から削除
1824
+ db.exec(`
1825
+ CREATE TRIGGER IF NOT EXISTS ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
1826
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
1827
+ END
1828
+ `);
1829
+ }
1830
+ // --- 5. 既存データからの初期構築 ---
1831
+ for (const e of entities) {
1832
+ db.exec(`
1833
+ INSERT OR REPLACE INTO unified_search_items(${columns})
1834
+ SELECT '${e.type}', id, scope, ${e.category("")}, ${e.title("")}, ${e.search("")}, ${e.searchSummary("")}, ${e.tags("")}, created_at, updated_at
1835
+ FROM ${e.table}
1836
+ WHERE status = 'active'
1837
+ `);
1838
+ }
1839
+ // --- 6. FTS rebuild ---
1840
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
1841
+ // --- 7. 外部キー整合性チェック ---
1842
+ const fkViolations = db.pragma("foreign_key_check");
1843
+ if (fkViolations.length > 0) {
1844
+ throw new Error(`V13マイグレーション後に外部キー違反が検出されました: ${JSON.stringify(fkViolations)}`);
1845
+ }
1846
+ // --- 8. スキーマバージョン記録 ---
1847
+ db.exec(`INSERT INTO schema_version (version) VALUES (13)`);
1848
+ });
1849
+ transaction();
1850
+ }
1851
+ finally {
1852
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
1853
+ db.pragma("foreign_keys = ON");
1854
+ }
1855
+ }
1856
+ }
1857
+ /**
1858
+ * V14マイグレーション: Theory/Insight/Model に evidence_refs カラム追加 (Issue #44)
1859
+ */
1860
+ function applyV14(db) {
1861
+ const fkState = db.pragma("foreign_keys");
1862
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
1863
+ db.pragma("foreign_keys = OFF");
1864
+ }
1865
+ try {
1866
+ const transaction = db.transaction(() => {
1867
+ // --- 1. evidence_refs カラム追加 ---
1868
+ for (const table of ["theories", "insights", "models"]) {
1869
+ if (!hasColumn(db, table, "evidence_refs")) {
1870
+ db.exec(`ALTER TABLE ${table} ADD COLUMN evidence_refs TEXT NOT NULL DEFAULT '[]'`);
1871
+ }
1872
+ }
1873
+ // --- 2. FTSテーブル再作成(evidence_refs カラム追加) ---
1874
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
1875
+ // theories_fts
1876
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_insert`);
1877
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_update`);
1878
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_delete`);
1879
+ db.exec(`DROP TABLE IF EXISTS theories_fts`);
1880
+ db.exec(`
1881
+ CREATE VIRTUAL TABLE theories_fts USING fts5(
1882
+ title, description, core_thesis, principles, evidence_refs, tags, search_summary,
1883
+ content='theories',
1884
+ content_rowid='rowid',
1885
+ tokenize='trigram'
1886
+ )
1887
+ `);
1888
+ // insights_fts
1889
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_insert`);
1890
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_update`);
1891
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_delete`);
1892
+ db.exec(`DROP TABLE IF EXISTS insights_fts`);
1893
+ db.exec(`
1894
+ CREATE VIRTUAL TABLE insights_fts USING fts5(
1895
+ title, description, core_thesis, principles, evidence_refs, tags, search_summary,
1896
+ content='insights',
1897
+ content_rowid='rowid',
1898
+ tokenize='trigram'
1899
+ )
1900
+ `);
1901
+ // models_fts
1902
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_insert`);
1903
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_update`);
1904
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_delete`);
1905
+ db.exec(`DROP TABLE IF EXISTS models_fts`);
1906
+ db.exec(`
1907
+ CREATE VIRTUAL TABLE models_fts USING fts5(
1908
+ title, description, core_thesis, principles, evidence_refs, tags, search_summary,
1909
+ content='models',
1910
+ content_rowid='rowid',
1911
+ tokenize='trigram'
1912
+ )
1913
+ `);
1914
+ // --- 3. FTS同期トリガー再作成 ---
1915
+ // theories
1916
+ db.exec(`
1917
+ CREATE TRIGGER theories_fts_insert AFTER INSERT ON theories BEGIN
1918
+ INSERT INTO theories_fts(rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1919
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1920
+ ${jc("new.principles")}, ${jc("new.evidence_refs")},
1921
+ ${jc("new.tags")}, new.search_summary);
1922
+ END
1923
+ `);
1924
+ db.exec(`
1925
+ CREATE TRIGGER theories_fts_update AFTER UPDATE ON theories BEGIN
1926
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1927
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1928
+ ${jc("old.principles")}, ${jc("old.evidence_refs")},
1929
+ ${jc("old.tags")}, old.search_summary);
1930
+ INSERT INTO theories_fts(rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1931
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1932
+ ${jc("new.principles")}, ${jc("new.evidence_refs")},
1933
+ ${jc("new.tags")}, new.search_summary);
1934
+ END
1935
+ `);
1936
+ db.exec(`
1937
+ CREATE TRIGGER theories_fts_delete AFTER DELETE ON theories BEGIN
1938
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1939
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1940
+ ${jc("old.principles")}, ${jc("old.evidence_refs")},
1941
+ ${jc("old.tags")}, old.search_summary);
1942
+ END
1943
+ `);
1944
+ // insights
1945
+ db.exec(`
1946
+ CREATE TRIGGER insights_fts_insert AFTER INSERT ON insights BEGIN
1947
+ INSERT INTO insights_fts(rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1948
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1949
+ ${jc("new.principles")}, ${jc("new.evidence_refs")},
1950
+ ${jc("new.tags")}, new.search_summary);
1951
+ END
1952
+ `);
1953
+ db.exec(`
1954
+ CREATE TRIGGER insights_fts_update AFTER UPDATE ON insights BEGIN
1955
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1956
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1957
+ ${jc("old.principles")}, ${jc("old.evidence_refs")},
1958
+ ${jc("old.tags")}, old.search_summary);
1959
+ INSERT INTO insights_fts(rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1960
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1961
+ ${jc("new.principles")}, ${jc("new.evidence_refs")},
1962
+ ${jc("new.tags")}, new.search_summary);
1963
+ END
1964
+ `);
1965
+ db.exec(`
1966
+ CREATE TRIGGER insights_fts_delete AFTER DELETE ON insights BEGIN
1967
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1968
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1969
+ ${jc("old.principles")}, ${jc("old.evidence_refs")},
1970
+ ${jc("old.tags")}, old.search_summary);
1971
+ END
1972
+ `);
1973
+ // models
1974
+ db.exec(`
1975
+ CREATE TRIGGER models_fts_insert AFTER INSERT ON models BEGIN
1976
+ INSERT INTO models_fts(rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1977
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1978
+ ${jc("new.principles")}, ${jc("new.evidence_refs")},
1979
+ ${jc("new.tags")}, new.search_summary);
1980
+ END
1981
+ `);
1982
+ db.exec(`
1983
+ CREATE TRIGGER models_fts_update AFTER UPDATE ON models BEGIN
1984
+ INSERT INTO models_fts(models_fts, rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1985
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1986
+ ${jc("old.principles")}, ${jc("old.evidence_refs")},
1987
+ ${jc("old.tags")}, old.search_summary);
1988
+ INSERT INTO models_fts(rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1989
+ VALUES (new.rowid, new.title, new.description, new.core_thesis,
1990
+ ${jc("new.principles")}, ${jc("new.evidence_refs")},
1991
+ ${jc("new.tags")}, new.search_summary);
1992
+ END
1993
+ `);
1994
+ db.exec(`
1995
+ CREATE TRIGGER models_fts_delete AFTER DELETE ON models BEGIN
1996
+ INSERT INTO models_fts(models_fts, rowid, title, description, core_thesis, principles, evidence_refs, tags, search_summary)
1997
+ VALUES ('delete', old.rowid, old.title, old.description, old.core_thesis,
1998
+ ${jc("old.principles")}, ${jc("old.evidence_refs")},
1999
+ ${jc("old.tags")}, old.search_summary);
2000
+ END
2001
+ `);
2002
+ // --- 4. unified_search_items トリガー更新(theory/insight/model の search_text に evidence_refs 追加) ---
2003
+ const coal = (col) => `COALESCE(${col}, '')`;
2004
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
2005
+ const updatedEntities = [
2006
+ {
2007
+ type: "theory", table: "theories",
2008
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2009
+ },
2010
+ {
2011
+ type: "insight", table: "insights",
2012
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2013
+ },
2014
+ {
2015
+ type: "model", table: "models",
2016
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2017
+ },
2018
+ ];
2019
+ for (const e of updatedEntities) {
2020
+ // Drop old triggers
2021
+ db.exec(`DROP TRIGGER IF EXISTS ${e.table}_unified_insert`);
2022
+ db.exec(`DROP TRIGGER IF EXISTS ${e.table}_unified_update`);
2023
+ db.exec(`DROP TRIGGER IF EXISTS ${e.table}_unified_delete`);
2024
+ // Recreate with evidence_refs in search text
2025
+ db.exec(`
2026
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table}
2027
+ WHEN new.status = 'active' BEGIN
2028
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2029
+ VALUES ('${e.type}', new.id, new.scope, NULL, new.title, ${e.search("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
2030
+ END
2031
+ `);
2032
+ db.exec(`
2033
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
2034
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
2035
+ INSERT INTO unified_search_items(${columns})
2036
+ SELECT '${e.type}', new.id, new.scope, NULL, new.title, ${e.search("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at
2037
+ WHERE new.status = 'active';
2038
+ END
2039
+ `);
2040
+ db.exec(`
2041
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
2042
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
2043
+ END
2044
+ `);
2045
+ }
2046
+ // --- 5. FTS + unified_search_items リビルド ---
2047
+ db.exec(`INSERT INTO theories_fts(theories_fts) VALUES('rebuild')`);
2048
+ db.exec(`INSERT INTO insights_fts(insights_fts) VALUES('rebuild')`);
2049
+ db.exec(`INSERT INTO models_fts(models_fts) VALUES('rebuild')`);
2050
+ // unified_search_items の theory/insight/model を再構築
2051
+ for (const e of updatedEntities) {
2052
+ db.exec(`DELETE FROM unified_search_items WHERE entity_type = '${e.type}'`);
2053
+ db.exec(`
2054
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2055
+ SELECT '${e.type}', id, scope, NULL, title, ${e.search("")}, search_summary, tags, created_at, updated_at
2056
+ FROM ${e.table}
2057
+ WHERE status = 'active'
2058
+ `);
2059
+ }
2060
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
2061
+ // --- 6. スキーマバージョン記録 ---
2062
+ db.exec(`INSERT INTO schema_version (version) VALUES (14)`);
2063
+ });
2064
+ transaction();
2065
+ }
2066
+ finally {
2067
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2068
+ db.pragma("foreign_keys = ON");
2069
+ }
2070
+ }
2071
+ }
2072
+ /**
2073
+ * V15マイグレーション: Memo テーブル新設 (Issue #45)
2074
+ * 「わたし」の本質ではない参照用知識(秘書の手帳メタファー)を記録するエンティティ。
2075
+ * usage_policy による可視性制御あり。
2076
+ */
2077
+ function applyV15(db) {
2078
+ const fkState = db.pragma("foreign_keys");
2079
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2080
+ db.pragma("foreign_keys = OFF");
2081
+ }
2082
+ try {
2083
+ const transaction = db.transaction(() => {
2084
+ // --- 1. memos テーブル ---
2085
+ db.exec(`
2086
+ CREATE TABLE IF NOT EXISTS memos (
2087
+ id TEXT PRIMARY KEY,
2088
+ title TEXT NOT NULL,
2089
+ content TEXT NOT NULL,
2090
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
2091
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
2092
+ tags TEXT NOT NULL DEFAULT '[]',
2093
+ scope TEXT NOT NULL DEFAULT 'global',
2094
+ search_summary TEXT,
2095
+ status TEXT NOT NULL DEFAULT 'active'
2096
+ CHECK(status IN ('active','archived')),
2097
+ embedding BLOB,
2098
+ client_name TEXT,
2099
+ client_version TEXT,
2100
+ source_tool TEXT,
2101
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2102
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2103
+ )
2104
+ `);
2105
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memos_status ON memos(status)`);
2106
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memos_scope ON memos(scope)`);
2107
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memos_usage_policy ON memos(usage_policy)`);
2108
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memos_updated ON memos(updated_at)`);
2109
+ // --- 2. memos_fts (trigram tokenizer) ---
2110
+ db.exec(`
2111
+ CREATE VIRTUAL TABLE IF NOT EXISTS memos_fts USING fts5(
2112
+ title, content, tags, search_summary,
2113
+ content='memos',
2114
+ content_rowid='rowid',
2115
+ tokenize='trigram'
2116
+ )
2117
+ `);
2118
+ // --- 3. FTS同期トリガー ---
2119
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
2120
+ db.exec(`
2121
+ CREATE TRIGGER IF NOT EXISTS memos_fts_insert AFTER INSERT ON memos BEGIN
2122
+ INSERT INTO memos_fts(rowid, title, content, tags, search_summary)
2123
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
2124
+ END
2125
+ `);
2126
+ db.exec(`
2127
+ CREATE TRIGGER IF NOT EXISTS memos_fts_update AFTER UPDATE ON memos BEGIN
2128
+ INSERT INTO memos_fts(memos_fts, rowid, title, content, tags, search_summary)
2129
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
2130
+ INSERT INTO memos_fts(rowid, title, content, tags, search_summary)
2131
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
2132
+ END
2133
+ `);
2134
+ db.exec(`
2135
+ CREATE TRIGGER IF NOT EXISTS memos_fts_delete AFTER DELETE ON memos BEGIN
2136
+ INSERT INTO memos_fts(memos_fts, rowid, title, content, tags, search_summary)
2137
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
2138
+ END
2139
+ `);
2140
+ // --- 4. unified_search_items: CHECK制約を拡張して 'memo' を追加 ---
2141
+ // 既存のFTS・source triggerを全削除→テーブル再作成→全trigger再作成
2142
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_insert`);
2143
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_update`);
2144
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
2145
+ for (const table of ["claims", "episodes", "decisions", "theories", "insights", "models"]) {
2146
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_insert`);
2147
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_update`);
2148
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_delete`);
2149
+ }
2150
+ db.exec(`
2151
+ CREATE TABLE unified_search_items_new (
2152
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','memo')),
2153
+ entity_id TEXT NOT NULL,
2154
+ scope TEXT NOT NULL DEFAULT 'global',
2155
+ category TEXT,
2156
+ title_summary TEXT NOT NULL,
2157
+ search_text TEXT NOT NULL,
2158
+ search_summary TEXT,
2159
+ tags TEXT NOT NULL DEFAULT '[]',
2160
+ embedding BLOB,
2161
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2162
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2163
+ PRIMARY KEY (entity_type, entity_id)
2164
+ )
2165
+ `);
2166
+ db.exec(`INSERT INTO unified_search_items_new SELECT * FROM unified_search_items`);
2167
+ db.exec(`DROP TABLE unified_search_items`);
2168
+ db.exec(`ALTER TABLE unified_search_items_new RENAME TO unified_search_items`);
2169
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope)`);
2170
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type)`);
2171
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_updated ON unified_search_items(updated_at)`);
2172
+ // unified_search_fts 再作成
2173
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
2174
+ db.exec(`
2175
+ CREATE VIRTUAL TABLE unified_search_fts USING fts5(
2176
+ title_summary, search_text, search_summary,
2177
+ content='unified_search_items',
2178
+ content_rowid='rowid',
2179
+ tokenize='trigram'
2180
+ )
2181
+ `);
2182
+ // FTS同期トリガー再作成
2183
+ db.exec(`
2184
+ CREATE TRIGGER unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
2185
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
2186
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
2187
+ END
2188
+ `);
2189
+ db.exec(`
2190
+ CREATE TRIGGER unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
2191
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
2192
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
2193
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
2194
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
2195
+ END
2196
+ `);
2197
+ db.exec(`
2198
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
2199
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
2200
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
2201
+ END
2202
+ `);
2203
+ // --- 5. Source table sync triggers 再作成(V13+V14のパターンを統合) ---
2204
+ const coal = (col) => `COALESCE(${col}, '')`;
2205
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
2206
+ const entities = [
2207
+ {
2208
+ type: "claim", table: "claims",
2209
+ category: (p) => `${p}category`,
2210
+ title: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object`,
2211
+ search: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}search_summary`)}`,
2212
+ tags: () => `'[]'`,
2213
+ },
2214
+ {
2215
+ type: "episode", table: "episodes",
2216
+ category: () => `NULL`,
2217
+ title: (p) => `${p}title`,
2218
+ search: (p) => `${p}title || ' ' || ${coal(`${p}context`)} || ' ' || ${coal(`${p}trigger`)} || ' ' || ${jc(`${p}problems`)} || ' ' || ${jc(`${p}desires`)} || ' ' || ${jc(`${p}decisions`)} || ' ' || ${jc(`${p}outcomes`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2219
+ tags: (p) => `${p}tags`,
2220
+ },
2221
+ {
2222
+ type: "decision", table: "decisions",
2223
+ category: () => `NULL`,
2224
+ title: (p) => `${p}title`,
2225
+ search: (p) => `${p}title || ' ' || ${p}description || ' ' || ${p}reasoning || ' ' || ${jc(`${p}alternatives`)} || ' ' || ${coal(`${p}search_summary`)}`,
2226
+ tags: () => `'[]'`,
2227
+ },
2228
+ {
2229
+ type: "theory", table: "theories",
2230
+ category: () => `NULL`,
2231
+ title: (p) => `${p}title`,
2232
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2233
+ tags: (p) => `${p}tags`,
2234
+ },
2235
+ {
2236
+ type: "insight", table: "insights",
2237
+ category: () => `NULL`,
2238
+ title: (p) => `${p}title`,
2239
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2240
+ tags: (p) => `${p}tags`,
2241
+ },
2242
+ {
2243
+ type: "model", table: "models",
2244
+ category: () => `NULL`,
2245
+ title: (p) => `${p}title`,
2246
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2247
+ tags: (p) => `${p}tags`,
2248
+ },
2249
+ {
2250
+ type: "memo", table: "memos",
2251
+ category: () => `NULL`,
2252
+ title: (p) => `${p}title`,
2253
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2254
+ tags: (p) => `${p}tags`,
2255
+ statusCondition: "AND new.usage_policy != 'human_directed'",
2256
+ },
2257
+ ];
2258
+ for (const e of entities) {
2259
+ const insertWhen = e.statusCondition
2260
+ ? `WHEN new.status = 'active' ${e.statusCondition}`
2261
+ : `WHEN new.status = 'active'`;
2262
+ const updateWhere = e.statusCondition
2263
+ ? `WHERE new.status = 'active' ${e.statusCondition}`
2264
+ : `WHERE new.status = 'active'`;
2265
+ db.exec(`
2266
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table}
2267
+ ${insertWhen} BEGIN
2268
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2269
+ VALUES ('${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at);
2270
+ END
2271
+ `);
2272
+ db.exec(`
2273
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
2274
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
2275
+ INSERT INTO unified_search_items(${columns})
2276
+ SELECT '${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at
2277
+ ${updateWhere};
2278
+ END
2279
+ `);
2280
+ db.exec(`
2281
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
2282
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
2283
+ END
2284
+ `);
2285
+ }
2286
+ // --- 6. FTS rebuild ---
2287
+ db.exec(`INSERT INTO memos_fts(memos_fts) VALUES('rebuild')`);
2288
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
2289
+ // --- 7. スキーマバージョン記録 ---
2290
+ db.exec(`INSERT INTO schema_version (version) VALUES (15)`);
2291
+ });
2292
+ transaction();
2293
+ }
2294
+ finally {
2295
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2296
+ db.pragma("foreign_keys = ON");
2297
+ }
2298
+ }
2299
+ }
2300
+ /**
2301
+ * V16マイグレーション: memos → user_memos リネーム (Issue #45)
2302
+ * user_系テーブル統一のため、memosテーブルをuser_memosにリネーム。
2303
+ * FTS・トリガー・unified_search_items・audit_logsのentity_typeも更新。
2304
+ */
2305
+ function applyV16(db) {
2306
+ const fkState = db.pragma("foreign_keys");
2307
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2308
+ db.pragma("foreign_keys = OFF");
2309
+ }
2310
+ try {
2311
+ const transaction = db.transaction(() => {
2312
+ // --- 1. テーブルリネーム ---
2313
+ db.exec(`ALTER TABLE memos RENAME TO user_memos`);
2314
+ // --- 2. インデックス再作成(SQLiteはインデックスリネーム不可) ---
2315
+ db.exec(`DROP INDEX IF EXISTS idx_memos_status`);
2316
+ db.exec(`DROP INDEX IF EXISTS idx_memos_scope`);
2317
+ db.exec(`DROP INDEX IF EXISTS idx_memos_usage_policy`);
2318
+ db.exec(`DROP INDEX IF EXISTS idx_memos_updated`);
2319
+ db.exec(`CREATE INDEX idx_user_memos_status ON user_memos(status)`);
2320
+ db.exec(`CREATE INDEX idx_user_memos_scope ON user_memos(scope)`);
2321
+ db.exec(`CREATE INDEX idx_user_memos_usage_policy ON user_memos(usage_policy)`);
2322
+ db.exec(`CREATE INDEX idx_user_memos_updated ON user_memos(updated_at)`);
2323
+ // --- 3. 旧FTSトリガー・テーブル削除 ---
2324
+ db.exec(`DROP TRIGGER IF EXISTS memos_fts_insert`);
2325
+ db.exec(`DROP TRIGGER IF EXISTS memos_fts_update`);
2326
+ db.exec(`DROP TRIGGER IF EXISTS memos_fts_delete`);
2327
+ db.exec(`DROP TABLE IF EXISTS memos_fts`);
2328
+ // --- 4. 新FTSテーブル作成 ---
2329
+ db.exec(`
2330
+ CREATE VIRTUAL TABLE user_memos_fts USING fts5(
2331
+ title, content, tags, search_summary,
2332
+ content='user_memos',
2333
+ content_rowid='rowid',
2334
+ tokenize='trigram'
2335
+ )
2336
+ `);
2337
+ // --- 5. 新FTSトリガー作成 ---
2338
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
2339
+ db.exec(`
2340
+ CREATE TRIGGER user_memos_fts_insert AFTER INSERT ON user_memos BEGIN
2341
+ INSERT INTO user_memos_fts(rowid, title, content, tags, search_summary)
2342
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
2343
+ END
2344
+ `);
2345
+ db.exec(`
2346
+ CREATE TRIGGER user_memos_fts_update AFTER UPDATE ON user_memos BEGIN
2347
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, content, tags, search_summary)
2348
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
2349
+ INSERT INTO user_memos_fts(rowid, title, content, tags, search_summary)
2350
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
2351
+ END
2352
+ `);
2353
+ db.exec(`
2354
+ CREATE TRIGGER user_memos_fts_delete AFTER DELETE ON user_memos BEGIN
2355
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, content, tags, search_summary)
2356
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
2357
+ END
2358
+ `);
2359
+ // --- 6. FTSリビルド ---
2360
+ db.exec(`INSERT INTO user_memos_fts(user_memos_fts) VALUES('rebuild')`);
2361
+ // --- 7. unified_search_items: entity_type 'memo' → 'user_memo' ---
2362
+ // unified_search_itemsのCHECK制約を更新するためテーブル再作成が必要
2363
+ // 2026-02-26 修正: 旧CHECK制約を持つテーブルにUPDATEすると制約違反になるため、
2364
+ // 新テーブルに INSERT ... SELECT で変換コピーする方式に変更
2365
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_insert`);
2366
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_update`);
2367
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
2368
+ for (const table of ["claims", "episodes", "decisions", "theories", "insights", "models", "memos"]) {
2369
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_insert`);
2370
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_update`);
2371
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_delete`);
2372
+ }
2373
+ db.exec(`
2374
+ CREATE TABLE unified_search_items_new (
2375
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','user_memo')),
2376
+ entity_id TEXT NOT NULL,
2377
+ scope TEXT NOT NULL DEFAULT 'global',
2378
+ category TEXT,
2379
+ title_summary TEXT NOT NULL,
2380
+ search_text TEXT NOT NULL,
2381
+ search_summary TEXT,
2382
+ tags TEXT NOT NULL DEFAULT '[]',
2383
+ embedding BLOB,
2384
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2385
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2386
+ PRIMARY KEY (entity_type, entity_id)
2387
+ )
2388
+ `);
2389
+ // entity_type 'memo' → 'user_memo' に変換しながらコピー
2390
+ db.exec(`
2391
+ INSERT INTO unified_search_items_new
2392
+ SELECT
2393
+ CASE WHEN entity_type = 'memo' THEN 'user_memo' ELSE entity_type END,
2394
+ entity_id, scope, category, title_summary, search_text, search_summary, tags, embedding, created_at, updated_at
2395
+ FROM unified_search_items
2396
+ `);
2397
+ db.exec(`DROP TABLE unified_search_items`);
2398
+ db.exec(`ALTER TABLE unified_search_items_new RENAME TO unified_search_items`);
2399
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope)`);
2400
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type)`);
2401
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_updated ON unified_search_items(updated_at)`);
2402
+ // unified_search_fts 再作成
2403
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
2404
+ db.exec(`
2405
+ CREATE VIRTUAL TABLE unified_search_fts USING fts5(
2406
+ title_summary, search_text, search_summary,
2407
+ content='unified_search_items',
2408
+ content_rowid='rowid',
2409
+ tokenize='trigram'
2410
+ )
2411
+ `);
2412
+ // FTS同期トリガー再作成
2413
+ db.exec(`
2414
+ CREATE TRIGGER unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
2415
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
2416
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
2417
+ END
2418
+ `);
2419
+ db.exec(`
2420
+ CREATE TRIGGER unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
2421
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
2422
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
2423
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
2424
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
2425
+ END
2426
+ `);
2427
+ db.exec(`
2428
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
2429
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
2430
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
2431
+ END
2432
+ `);
2433
+ // --- 8. Source table sync triggers 再作成 ---
2434
+ const coal = (col) => `COALESCE(${col}, '')`;
2435
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
2436
+ const entities = [
2437
+ {
2438
+ type: "claim", table: "claims",
2439
+ category: (p) => `${p}category`,
2440
+ title: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object`,
2441
+ search: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}search_summary`)}`,
2442
+ tags: () => `'[]'`,
2443
+ },
2444
+ {
2445
+ type: "episode", table: "episodes",
2446
+ category: () => `NULL`,
2447
+ title: (p) => `${p}title`,
2448
+ search: (p) => `${p}title || ' ' || ${coal(`${p}context`)} || ' ' || ${coal(`${p}trigger`)} || ' ' || ${jc(`${p}problems`)} || ' ' || ${jc(`${p}desires`)} || ' ' || ${jc(`${p}decisions`)} || ' ' || ${jc(`${p}outcomes`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2449
+ tags: (p) => `${p}tags`,
2450
+ },
2451
+ {
2452
+ type: "decision", table: "decisions",
2453
+ category: () => `NULL`,
2454
+ title: (p) => `${p}title`,
2455
+ search: (p) => `${p}title || ' ' || ${p}description || ' ' || ${p}reasoning || ' ' || ${jc(`${p}alternatives`)} || ' ' || ${coal(`${p}search_summary`)}`,
2456
+ tags: () => `'[]'`,
2457
+ },
2458
+ {
2459
+ type: "theory", table: "theories",
2460
+ category: () => `NULL`,
2461
+ title: (p) => `${p}title`,
2462
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2463
+ tags: (p) => `${p}tags`,
2464
+ },
2465
+ {
2466
+ type: "insight", table: "insights",
2467
+ category: () => `NULL`,
2468
+ title: (p) => `${p}title`,
2469
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2470
+ tags: (p) => `${p}tags`,
2471
+ },
2472
+ {
2473
+ type: "model", table: "models",
2474
+ category: () => `NULL`,
2475
+ title: (p) => `${p}title`,
2476
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2477
+ tags: (p) => `${p}tags`,
2478
+ },
2479
+ {
2480
+ // 2026-02-25 リネーム: memo → user_memo (Issue #45 Phase 1)
2481
+ type: "user_memo", table: "user_memos",
2482
+ category: () => `NULL`,
2483
+ title: (p) => `${p}title`,
2484
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2485
+ tags: (p) => `${p}tags`,
2486
+ statusCondition: "AND new.usage_policy != 'human_directed'",
2487
+ },
2488
+ ];
2489
+ for (const e of entities) {
2490
+ const insertWhen = e.statusCondition
2491
+ ? `WHEN new.status = 'active' ${e.statusCondition}`
2492
+ : `WHEN new.status = 'active'`;
2493
+ const updateWhere = e.statusCondition
2494
+ ? `WHERE new.status = 'active' ${e.statusCondition}`
2495
+ : `WHERE new.status = 'active'`;
2496
+ db.exec(`
2497
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table}
2498
+ ${insertWhen} BEGIN
2499
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2500
+ VALUES ('${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at);
2501
+ END
2502
+ `);
2503
+ db.exec(`
2504
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
2505
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
2506
+ INSERT INTO unified_search_items(${columns})
2507
+ SELECT '${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at
2508
+ ${updateWhere};
2509
+ END
2510
+ `);
2511
+ db.exec(`
2512
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
2513
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
2514
+ END
2515
+ `);
2516
+ }
2517
+ // --- 9. unified_search_fts リビルド ---
2518
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
2519
+ // --- 10. audit_log: CHECK制約に 'user_memo' を追加 ---
2520
+ // V15でmemoをaudit_log CHECK制約に追加し忘れていたバグも合わせて修正
2521
+ // SQLiteはCHECK制約変更不可のため、テーブル再作成で対応
2522
+ db.exec(`
2523
+ CREATE TABLE audit_log_new (
2524
+ id TEXT PRIMARY KEY,
2525
+ operation TEXT NOT NULL CHECK(operation IN (
2526
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
2527
+ )),
2528
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model','user_memo')),
2529
+ entity_id TEXT NOT NULL,
2530
+ summary TEXT NOT NULL,
2531
+ details TEXT,
2532
+ client_name TEXT,
2533
+ client_version TEXT,
2534
+ session_id TEXT,
2535
+ source_tool TEXT,
2536
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2537
+ )
2538
+ `);
2539
+ // entity_type 'memo' → 'user_memo' に変換しながらコピー
2540
+ db.exec(`
2541
+ INSERT INTO audit_log_new
2542
+ SELECT id, operation,
2543
+ CASE WHEN entity_type = 'memo' THEN 'user_memo' ELSE entity_type END,
2544
+ entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at
2545
+ FROM audit_log
2546
+ `);
2547
+ db.exec(`DROP TABLE audit_log`);
2548
+ db.exec(`ALTER TABLE audit_log_new RENAME TO audit_log`);
2549
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id)`);
2550
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation)`);
2551
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`);
2552
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id)`);
2553
+ // --- 11. スキーマバージョン記録 ---
2554
+ db.exec(`INSERT INTO schema_version (version) VALUES (16)`);
2555
+ });
2556
+ transaction();
2557
+ }
2558
+ finally {
2559
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2560
+ db.pragma("foreign_keys = ON");
2561
+ }
2562
+ }
2563
+ }
2564
+ /**
2565
+ * V17スキーマ: user_input カラム追加(Issue #56)
2566
+ * episodes, decisions テーブルにユーザー原文保存用カラムを追加。
2567
+ * FTSインデックスを再構築し user_input を検索対象に含める。
2568
+ * unified_search_items の search_text にも user_input を含める。
2569
+ */
2570
+ function applyV17(db) {
2571
+ const fkState = db.pragma("foreign_keys");
2572
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2573
+ db.pragma("foreign_keys = OFF");
2574
+ }
2575
+ try {
2576
+ const transaction = db.transaction(() => {
2577
+ // --- 1. user_input カラム追加 ---
2578
+ if (!hasColumn(db, "episodes", "user_input")) {
2579
+ db.exec(`ALTER TABLE episodes ADD COLUMN user_input TEXT`);
2580
+ }
2581
+ if (!hasColumn(db, "decisions", "user_input")) {
2582
+ db.exec(`ALTER TABLE decisions ADD COLUMN user_input TEXT`);
2583
+ }
2584
+ // --- 2. episodes_fts 再構築(user_input 追加) ---
2585
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_insert`);
2586
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_update`);
2587
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_delete`);
2588
+ db.exec(`DROP TABLE IF EXISTS episodes_fts`);
2589
+ db.exec(`
2590
+ CREATE VIRTUAL TABLE episodes_fts USING fts5(
2591
+ title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary, user_input,
2592
+ content='episodes',
2593
+ content_rowid='rowid',
2594
+ tokenize='trigram'
2595
+ )
2596
+ `);
2597
+ // --- 3. decisions_fts 再構築(user_input 追加) ---
2598
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_insert`);
2599
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_update`);
2600
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_delete`);
2601
+ db.exec(`DROP TABLE IF EXISTS decisions_fts`);
2602
+ db.exec(`
2603
+ CREATE VIRTUAL TABLE decisions_fts USING fts5(
2604
+ title, description, reasoning, search_summary, user_input,
2605
+ content='decisions',
2606
+ content_rowid='rowid',
2607
+ tokenize='trigram'
2608
+ )
2609
+ `);
2610
+ // --- 4. FTS同期トリガー再作成 ---
2611
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
2612
+ // episodes FTSトリガー
2613
+ db.exec(`
2614
+ CREATE TRIGGER episodes_fts_insert AFTER INSERT ON episodes BEGIN
2615
+ INSERT INTO episodes_fts(rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary, user_input)
2616
+ VALUES (new.rowid, new.title, new.context, new.trigger,
2617
+ ${jc("new.principles")}, ${jc("new.problems")}, ${jc("new.desires")},
2618
+ ${jc("new.decisions")}, ${jc("new.outcomes")},
2619
+ new.search_summary, new.user_input);
2620
+ END
2621
+ `);
2622
+ db.exec(`
2623
+ CREATE TRIGGER episodes_fts_update AFTER UPDATE ON episodes BEGIN
2624
+ INSERT INTO episodes_fts(episodes_fts, rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary, user_input)
2625
+ VALUES ('delete', old.rowid, old.title, old.context, old.trigger,
2626
+ ${jc("old.principles")}, ${jc("old.problems")}, ${jc("old.desires")},
2627
+ ${jc("old.decisions")}, ${jc("old.outcomes")},
2628
+ old.search_summary, old.user_input);
2629
+ INSERT INTO episodes_fts(rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary, user_input)
2630
+ VALUES (new.rowid, new.title, new.context, new.trigger,
2631
+ ${jc("new.principles")}, ${jc("new.problems")}, ${jc("new.desires")},
2632
+ ${jc("new.decisions")}, ${jc("new.outcomes")},
2633
+ new.search_summary, new.user_input);
2634
+ END
2635
+ `);
2636
+ db.exec(`
2637
+ CREATE TRIGGER episodes_fts_delete AFTER DELETE ON episodes BEGIN
2638
+ INSERT INTO episodes_fts(episodes_fts, rowid, title, context, trigger, principles, problems, desires, decisions, outcomes, search_summary, user_input)
2639
+ VALUES ('delete', old.rowid, old.title, old.context, old.trigger,
2640
+ ${jc("old.principles")}, ${jc("old.problems")}, ${jc("old.desires")},
2641
+ ${jc("old.decisions")}, ${jc("old.outcomes")},
2642
+ old.search_summary, old.user_input);
2643
+ END
2644
+ `);
2645
+ // decisions FTSトリガー
2646
+ db.exec(`
2647
+ CREATE TRIGGER decisions_fts_insert AFTER INSERT ON decisions BEGIN
2648
+ INSERT INTO decisions_fts(rowid, title, description, reasoning, search_summary, user_input)
2649
+ VALUES (new.rowid, new.title, new.description, new.reasoning, new.search_summary, new.user_input);
2650
+ END
2651
+ `);
2652
+ db.exec(`
2653
+ CREATE TRIGGER decisions_fts_update AFTER UPDATE ON decisions BEGIN
2654
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, reasoning, search_summary, user_input)
2655
+ VALUES ('delete', old.rowid, old.title, old.description, old.reasoning, old.search_summary, old.user_input);
2656
+ INSERT INTO decisions_fts(rowid, title, description, reasoning, search_summary, user_input)
2657
+ VALUES (new.rowid, new.title, new.description, new.reasoning, new.search_summary, new.user_input);
2658
+ END
2659
+ `);
2660
+ db.exec(`
2661
+ CREATE TRIGGER decisions_fts_delete AFTER DELETE ON decisions BEGIN
2662
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, reasoning, search_summary, user_input)
2663
+ VALUES ('delete', old.rowid, old.title, old.description, old.reasoning, old.search_summary, old.user_input);
2664
+ END
2665
+ `);
2666
+ // --- 5. unified_search_items トリガー更新(episode/decision の search_text に user_input 追加) ---
2667
+ const coal = (col) => `COALESCE(${col}, '')`;
2668
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
2669
+ // episode unified トリガー再作成
2670
+ db.exec(`DROP TRIGGER IF EXISTS episodes_unified_insert`);
2671
+ db.exec(`DROP TRIGGER IF EXISTS episodes_unified_update`);
2672
+ db.exec(`DROP TRIGGER IF EXISTS episodes_unified_delete`);
2673
+ const epSearch = (p) => `${p}title || ' ' || ${coal(`${p}context`)} || ' ' || ${coal(`${p}trigger`)} || ' ' || ${jc(`${p}problems`)} || ' ' || ${jc(`${p}desires`)} || ' ' || ${jc(`${p}decisions`)} || ' ' || ${jc(`${p}outcomes`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`;
2674
+ db.exec(`
2675
+ CREATE TRIGGER episodes_unified_insert AFTER INSERT ON episodes
2676
+ WHEN new.status = 'active' BEGIN
2677
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2678
+ VALUES ('episode', new.id, new.scope, NULL, new.title, ${epSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
2679
+ END
2680
+ `);
2681
+ db.exec(`
2682
+ CREATE TRIGGER episodes_unified_update AFTER UPDATE ON episodes BEGIN
2683
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
2684
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2685
+ SELECT 'episode', new.id, new.scope, NULL, new.title, ${epSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at
2686
+ WHERE new.status = 'active';
2687
+ END
2688
+ `);
2689
+ db.exec(`
2690
+ CREATE TRIGGER episodes_unified_delete AFTER DELETE ON episodes BEGIN
2691
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
2692
+ END
2693
+ `);
2694
+ // decision unified トリガー再作成
2695
+ db.exec(`DROP TRIGGER IF EXISTS decisions_unified_insert`);
2696
+ db.exec(`DROP TRIGGER IF EXISTS decisions_unified_update`);
2697
+ db.exec(`DROP TRIGGER IF EXISTS decisions_unified_delete`);
2698
+ const decSearch = (p) => `${p}title || ' ' || ${p}description || ' ' || ${p}reasoning || ' ' || ${jc(`${p}alternatives`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`;
2699
+ db.exec(`
2700
+ CREATE TRIGGER decisions_unified_insert AFTER INSERT ON decisions
2701
+ WHEN new.status = 'active' BEGIN
2702
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2703
+ VALUES ('decision', new.id, new.scope, NULL, new.title, ${decSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
2704
+ END
2705
+ `);
2706
+ db.exec(`
2707
+ CREATE TRIGGER decisions_unified_update AFTER UPDATE ON decisions BEGIN
2708
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
2709
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2710
+ SELECT 'decision', new.id, new.scope, NULL, new.title, ${decSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at
2711
+ WHERE new.status = 'active';
2712
+ END
2713
+ `);
2714
+ db.exec(`
2715
+ CREATE TRIGGER decisions_unified_delete AFTER DELETE ON decisions BEGIN
2716
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
2717
+ END
2718
+ `);
2719
+ // --- 6. FTSリビルド ---
2720
+ db.exec(`INSERT INTO episodes_fts(episodes_fts) VALUES('rebuild')`);
2721
+ db.exec(`INSERT INTO decisions_fts(decisions_fts) VALUES('rebuild')`);
2722
+ // --- 7. スキーマバージョン記録 ---
2723
+ db.exec(`INSERT INTO schema_version (version) VALUES (17)`);
2724
+ });
2725
+ transaction();
2726
+ }
2727
+ finally {
2728
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2729
+ db.pragma("foreign_keys = ON");
2730
+ }
2731
+ }
2732
+ }
2733
+ /**
2734
+ * V18スキーマ: user_plans + user_issues テーブル新設(Issue #57)
2735
+ * セッション間で作業計画を永続化する仕組み + Issue #45 Phase 2-3。
2736
+ * - user_plans: Markdownチェックリスト形式の作業計画(active/completed/archived)
2737
+ * - user_issues: 課題・問題の蓄積(open/closed/archived、コメントスレッド付き)
2738
+ * - FTS5 + unified_search_items 対応
2739
+ * - audit_log CHECK制約拡張
2740
+ */
2741
+ function applyV18(db) {
2742
+ const fkState = db.pragma("foreign_keys");
2743
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
2744
+ db.pragma("foreign_keys = OFF");
2745
+ }
2746
+ try {
2747
+ const transaction = db.transaction(() => {
2748
+ // --- 1. user_plans テーブル ---
2749
+ db.exec(`
2750
+ CREATE TABLE IF NOT EXISTS user_plans (
2751
+ id TEXT PRIMARY KEY,
2752
+ title TEXT NOT NULL,
2753
+ content TEXT NOT NULL,
2754
+ usage_policy TEXT NOT NULL DEFAULT 'auto'
2755
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
2756
+ tags TEXT NOT NULL DEFAULT '[]',
2757
+ scope TEXT NOT NULL DEFAULT 'global',
2758
+ search_summary TEXT,
2759
+ user_input TEXT,
2760
+ status TEXT NOT NULL DEFAULT 'active'
2761
+ CHECK(status IN ('active','completed','archived')),
2762
+ embedding BLOB,
2763
+ client_name TEXT,
2764
+ client_version TEXT,
2765
+ source_tool TEXT,
2766
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2767
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2768
+ )
2769
+ `);
2770
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_status ON user_plans(status)`);
2771
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_scope ON user_plans(scope)`);
2772
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_usage_policy ON user_plans(usage_policy)`);
2773
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_updated ON user_plans(updated_at)`);
2774
+ // --- 2. user_issues テーブル ---
2775
+ db.exec(`
2776
+ CREATE TABLE IF NOT EXISTS user_issues (
2777
+ id TEXT PRIMARY KEY,
2778
+ title TEXT NOT NULL,
2779
+ content TEXT NOT NULL,
2780
+ entries TEXT NOT NULL DEFAULT '[]',
2781
+ priority TEXT NOT NULL DEFAULT 'medium'
2782
+ CHECK(priority IN ('low','medium','high','critical')),
2783
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
2784
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
2785
+ tags TEXT NOT NULL DEFAULT '[]',
2786
+ scope TEXT NOT NULL DEFAULT 'global',
2787
+ search_summary TEXT,
2788
+ user_input TEXT,
2789
+ status TEXT NOT NULL DEFAULT 'open'
2790
+ CHECK(status IN ('open','closed','archived')),
2791
+ embedding BLOB,
2792
+ client_name TEXT,
2793
+ client_version TEXT,
2794
+ source_tool TEXT,
2795
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2796
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2797
+ )
2798
+ `);
2799
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_status ON user_issues(status)`);
2800
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_scope ON user_issues(scope)`);
2801
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_priority ON user_issues(priority)`);
2802
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_updated ON user_issues(updated_at)`);
2803
+ // --- 3. FTS5テーブル(trigram tokenizer) ---
2804
+ db.exec(`
2805
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_plans_fts USING fts5(
2806
+ title, content, tags, search_summary,
2807
+ content='user_plans',
2808
+ content_rowid='rowid',
2809
+ tokenize='trigram'
2810
+ )
2811
+ `);
2812
+ db.exec(`
2813
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_issues_fts USING fts5(
2814
+ title, content, entries, tags, search_summary,
2815
+ content='user_issues',
2816
+ content_rowid='rowid',
2817
+ tokenize='trigram'
2818
+ )
2819
+ `);
2820
+ // --- 4. FTS同期トリガー ---
2821
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
2822
+ // user_plans FTSトリガー
2823
+ db.exec(`
2824
+ CREATE TRIGGER user_plans_fts_insert AFTER INSERT ON user_plans BEGIN
2825
+ INSERT INTO user_plans_fts(rowid, title, content, tags, search_summary)
2826
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
2827
+ END
2828
+ `);
2829
+ db.exec(`
2830
+ CREATE TRIGGER user_plans_fts_update AFTER UPDATE ON user_plans BEGIN
2831
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, content, tags, search_summary)
2832
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
2833
+ INSERT INTO user_plans_fts(rowid, title, content, tags, search_summary)
2834
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
2835
+ END
2836
+ `);
2837
+ db.exec(`
2838
+ CREATE TRIGGER user_plans_fts_delete AFTER DELETE ON user_plans BEGIN
2839
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, content, tags, search_summary)
2840
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
2841
+ END
2842
+ `);
2843
+ // user_issues FTSトリガー
2844
+ db.exec(`
2845
+ CREATE TRIGGER user_issues_fts_insert AFTER INSERT ON user_issues BEGIN
2846
+ INSERT INTO user_issues_fts(rowid, title, content, entries, tags, search_summary)
2847
+ VALUES (new.rowid, new.title, new.content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
2848
+ END
2849
+ `);
2850
+ db.exec(`
2851
+ CREATE TRIGGER user_issues_fts_update AFTER UPDATE ON user_issues BEGIN
2852
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, content, entries, tags, search_summary)
2853
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
2854
+ INSERT INTO user_issues_fts(rowid, title, content, entries, tags, search_summary)
2855
+ VALUES (new.rowid, new.title, new.content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
2856
+ END
2857
+ `);
2858
+ db.exec(`
2859
+ CREATE TRIGGER user_issues_fts_delete AFTER DELETE ON user_issues BEGIN
2860
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, content, entries, tags, search_summary)
2861
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
2862
+ END
2863
+ `);
2864
+ // --- 5. unified_search_items: CHECK制約を拡張して user_plan, user_issue を追加 ---
2865
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_insert`);
2866
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_update`);
2867
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
2868
+ for (const table of ["claims", "episodes", "decisions", "theories", "insights", "models", "user_memos"]) {
2869
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_insert`);
2870
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_update`);
2871
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_delete`);
2872
+ }
2873
+ db.exec(`
2874
+ CREATE TABLE unified_search_items_new (
2875
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','user_memo','user_plan','user_issue')),
2876
+ entity_id TEXT NOT NULL,
2877
+ scope TEXT NOT NULL DEFAULT 'global',
2878
+ category TEXT,
2879
+ title_summary TEXT NOT NULL,
2880
+ search_text TEXT NOT NULL,
2881
+ search_summary TEXT,
2882
+ tags TEXT NOT NULL DEFAULT '[]',
2883
+ embedding BLOB,
2884
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2885
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2886
+ PRIMARY KEY (entity_type, entity_id)
2887
+ )
2888
+ `);
2889
+ db.exec(`INSERT INTO unified_search_items_new SELECT * FROM unified_search_items`);
2890
+ db.exec(`DROP TABLE unified_search_items`);
2891
+ db.exec(`ALTER TABLE unified_search_items_new RENAME TO unified_search_items`);
2892
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_scope ON unified_search_items(scope)`);
2893
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_entity_type ON unified_search_items(entity_type)`);
2894
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_unified_search_updated ON unified_search_items(updated_at)`);
2895
+ // unified_search_fts 再作成
2896
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
2897
+ db.exec(`
2898
+ CREATE VIRTUAL TABLE unified_search_fts USING fts5(
2899
+ title_summary, search_text, search_summary,
2900
+ content='unified_search_items',
2901
+ content_rowid='rowid',
2902
+ tokenize='trigram'
2903
+ )
2904
+ `);
2905
+ // FTS同期トリガー再作成
2906
+ db.exec(`
2907
+ CREATE TRIGGER unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
2908
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
2909
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
2910
+ END
2911
+ `);
2912
+ db.exec(`
2913
+ CREATE TRIGGER unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
2914
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
2915
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
2916
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
2917
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
2918
+ END
2919
+ `);
2920
+ db.exec(`
2921
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
2922
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
2923
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
2924
+ END
2925
+ `);
2926
+ // --- 6. Source table sync triggers 再作成(全エンティティ) ---
2927
+ const coal = (col) => `COALESCE(${col}, '')`;
2928
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
2929
+ const entities = [
2930
+ {
2931
+ type: "claim", table: "claims",
2932
+ category: (p) => `${p}category`,
2933
+ title: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object`,
2934
+ search: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}search_summary`)}`,
2935
+ tags: () => `'[]'`,
2936
+ },
2937
+ {
2938
+ type: "episode", table: "episodes",
2939
+ category: () => `NULL`,
2940
+ title: (p) => `${p}title`,
2941
+ search: (p) => `${p}title || ' ' || ${coal(`${p}context`)} || ' ' || ${coal(`${p}trigger`)} || ' ' || ${jc(`${p}problems`)} || ' ' || ${jc(`${p}desires`)} || ' ' || ${jc(`${p}decisions`)} || ' ' || ${jc(`${p}outcomes`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`,
2942
+ tags: (p) => `${p}tags`,
2943
+ },
2944
+ {
2945
+ type: "decision", table: "decisions",
2946
+ category: () => `NULL`,
2947
+ title: (p) => `${p}title`,
2948
+ search: (p) => `${p}title || ' ' || ${p}description || ' ' || ${p}reasoning || ' ' || ${jc(`${p}alternatives`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`,
2949
+ tags: () => `'[]'`,
2950
+ },
2951
+ {
2952
+ type: "theory", table: "theories",
2953
+ category: () => `NULL`,
2954
+ title: (p) => `${p}title`,
2955
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2956
+ tags: (p) => `${p}tags`,
2957
+ },
2958
+ {
2959
+ type: "insight", table: "insights",
2960
+ category: () => `NULL`,
2961
+ title: (p) => `${p}title`,
2962
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2963
+ tags: (p) => `${p}tags`,
2964
+ },
2965
+ {
2966
+ type: "model", table: "models",
2967
+ category: () => `NULL`,
2968
+ title: (p) => `${p}title`,
2969
+ search: (p) => `${p}title || ' ' || ${coal(`${p}description`)} || ' ' || ${coal(`${p}core_thesis`)} || ' ' || ${jc(`${p}principles`)} || ' ' || ${jc(`${p}evidence_refs`)} || ' ' || ${jc(`${p}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2970
+ tags: (p) => `${p}tags`,
2971
+ },
2972
+ {
2973
+ type: "user_memo", table: "user_memos",
2974
+ category: () => `NULL`,
2975
+ title: (p) => `${p}title`,
2976
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2977
+ tags: (p) => `${p}tags`,
2978
+ statusCondition: "AND new.usage_policy != 'human_directed'",
2979
+ },
2980
+ {
2981
+ type: "user_plan", table: "user_plans",
2982
+ category: () => `NULL`,
2983
+ title: (p) => `${p}title`,
2984
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2985
+ tags: (p) => `${p}tags`,
2986
+ statusCondition: "AND new.usage_policy != 'human_directed'",
2987
+ },
2988
+ {
2989
+ type: "user_issue", table: "user_issues",
2990
+ category: () => `NULL`,
2991
+ title: (p) => `${p}title`,
2992
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}entries`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
2993
+ tags: (p) => `${p}tags`,
2994
+ statusField: "open",
2995
+ },
2996
+ ];
2997
+ for (const e of entities) {
2998
+ const activeStatus = e.statusField ?? "active";
2999
+ const insertWhen = e.statusCondition
3000
+ ? `WHEN new.status = '${activeStatus}' ${e.statusCondition}`
3001
+ : `WHEN new.status = '${activeStatus}'`;
3002
+ const updateWhere = e.statusCondition
3003
+ ? `WHERE new.status = '${activeStatus}' ${e.statusCondition}`
3004
+ : `WHERE new.status = '${activeStatus}'`;
3005
+ db.exec(`
3006
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table}
3007
+ ${insertWhen} BEGIN
3008
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3009
+ VALUES ('${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at);
3010
+ END
3011
+ `);
3012
+ db.exec(`
3013
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
3014
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
3015
+ INSERT INTO unified_search_items(${columns})
3016
+ SELECT '${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at
3017
+ ${updateWhere};
3018
+ END
3019
+ `);
3020
+ db.exec(`
3021
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
3022
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
3023
+ END
3024
+ `);
3025
+ }
3026
+ // --- 7. audit_log: CHECK制約に user_plan, user_issue を追加 ---
3027
+ db.exec(`
3028
+ CREATE TABLE audit_log_new (
3029
+ id TEXT PRIMARY KEY,
3030
+ operation TEXT NOT NULL CHECK(operation IN (
3031
+ 'create','update','retract','supersede','reverse','obsolete','archive','promote'
3032
+ )),
3033
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','decision','episode','theory','insight','model','user_memo','user_plan','user_issue')),
3034
+ entity_id TEXT NOT NULL,
3035
+ summary TEXT NOT NULL,
3036
+ details TEXT,
3037
+ client_name TEXT,
3038
+ client_version TEXT,
3039
+ session_id TEXT,
3040
+ source_tool TEXT,
3041
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3042
+ )
3043
+ `);
3044
+ db.exec(`
3045
+ INSERT INTO audit_log_new
3046
+ SELECT id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at
3047
+ FROM audit_log
3048
+ `);
3049
+ db.exec(`DROP TABLE audit_log`);
3050
+ db.exec(`ALTER TABLE audit_log_new RENAME TO audit_log`);
3051
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id)`);
3052
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON audit_log(operation)`);
3053
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`);
3054
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_session ON audit_log(session_id)`);
3055
+ // --- 8. FTSリビルド ---
3056
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
3057
+ // --- 9. スキーマバージョン記録 ---
3058
+ db.exec(`INSERT INTO schema_version (version) VALUES (18)`);
3059
+ });
3060
+ transaction();
3061
+ }
3062
+ finally {
3063
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
3064
+ db.pragma("foreign_keys = ON");
3065
+ }
3066
+ }
3067
+ }
3068
+ /**
3069
+ * V19マイグレーション: Insight l1/l2 拡張 (Issue #53)
3070
+ * - insights テーブル再作成(l1/l2 カラム追加、core_thesis → l2_core_thesis、principles → l2_principles リネーム)
3071
+ * - insights_fts 再作成(l1_content を検索対象に追加)
3072
+ * - unified_search_items の insights トリガー更新(l1_content を search_text に追加)
3073
+ */
3074
+ function applyV19(db) {
3075
+ const fkState = db.pragma("foreign_keys");
3076
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
3077
+ db.pragma("foreign_keys = OFF");
3078
+ }
3079
+ try {
3080
+ const transaction = db.transaction(() => {
3081
+ // --- 1. insights テーブル再作成(INSERT...SELECT方式) ---
3082
+ db.exec(`ALTER TABLE insights RENAME TO insights_old`);
3083
+ db.exec(`
3084
+ CREATE TABLE insights (
3085
+ -- 1. 識別
3086
+ id TEXT PRIMARY KEY,
3087
+ title TEXT NOT NULL,
3088
+ description TEXT,
3089
+
3090
+ -- 2. L1: 自然言語コンテンツ
3091
+ l1_content TEXT,
3092
+
3093
+ -- 3. L2: 構造化コンテンツ
3094
+ l2_core_thesis TEXT,
3095
+ l2_principles TEXT NOT NULL DEFAULT '[]',
3096
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
3097
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
3098
+ l2_applicable_context TEXT,
3099
+
3100
+ -- 4. メタデータ
3101
+ tags TEXT NOT NULL DEFAULT '[]',
3102
+ scope TEXT NOT NULL DEFAULT 'global',
3103
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
3104
+ search_summary TEXT,
3105
+ embedding BLOB,
3106
+
3107
+ -- 5. 参照
3108
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
3109
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
3110
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
3111
+
3112
+ -- 6. 分類(昇格元)
3113
+ promoted_from_store TEXT,
3114
+ promoted_from_id TEXT,
3115
+
3116
+ -- 7. ライフサイクル
3117
+ status TEXT NOT NULL DEFAULT 'active'
3118
+ CHECK(status IN ('active','archived','promoted')),
3119
+
3120
+ -- 8. プロヴナンス
3121
+ source_tool TEXT,
3122
+ session_id TEXT,
3123
+ client_name TEXT,
3124
+ client_version TEXT,
3125
+
3126
+ -- 9. タイムスタンプ
3127
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
3128
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
3129
+ )
3130
+ `);
3131
+ // データ移行: core_thesis → l2_core_thesis, principles → l2_principles, 新カラムはデフォルト値
3132
+ db.exec(`
3133
+ INSERT INTO insights (
3134
+ id, title, description,
3135
+ l1_content,
3136
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
3137
+ tags, scope, confidence, search_summary, embedding,
3138
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
3139
+ promoted_from_store, promoted_from_id,
3140
+ status,
3141
+ source_tool, session_id, client_name, client_version,
3142
+ created_at, updated_at
3143
+ )
3144
+ SELECT
3145
+ id, title, description,
3146
+ NULL,
3147
+ core_thesis, principles, '[]', '[]', NULL,
3148
+ tags, scope, confidence, search_summary, embedding,
3149
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
3150
+ promoted_from_store, promoted_from_id,
3151
+ status,
3152
+ source_tool, session_id, client_name, client_version,
3153
+ created_at, updated_at
3154
+ FROM insights_old
3155
+ `);
3156
+ db.exec(`DROP TABLE insights_old`);
3157
+ // --- 2. インデックス再作成 ---
3158
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_scope ON insights(scope)`);
3159
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_status ON insights(status)`);
3160
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_confidence ON insights(confidence)`);
3161
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`);
3162
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_insights_updated ON insights(updated_at)`);
3163
+ db.exec(`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`);
3164
+ // --- 3. insights_fts 再構築(l1_content を検索対象に追加) ---
3165
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
3166
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_insert`);
3167
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_update`);
3168
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_delete`);
3169
+ db.exec(`DROP TABLE IF EXISTS insights_fts`);
3170
+ db.exec(`
3171
+ CREATE VIRTUAL TABLE insights_fts USING fts5(
3172
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
3173
+ content='insights',
3174
+ content_rowid='rowid',
3175
+ tokenize='trigram'
3176
+ )
3177
+ `);
3178
+ db.exec(`
3179
+ CREATE TRIGGER insights_fts_insert AFTER INSERT ON insights BEGIN
3180
+ INSERT INTO insights_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3181
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
3182
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")},
3183
+ ${jc("new.tags")}, new.search_summary);
3184
+ END
3185
+ `);
3186
+ db.exec(`
3187
+ CREATE TRIGGER insights_fts_update AFTER UPDATE ON insights BEGIN
3188
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3189
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
3190
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")},
3191
+ ${jc("old.tags")}, old.search_summary);
3192
+ INSERT INTO insights_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3193
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
3194
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")},
3195
+ ${jc("new.tags")}, new.search_summary);
3196
+ END
3197
+ `);
3198
+ db.exec(`
3199
+ CREATE TRIGGER insights_fts_delete AFTER DELETE ON insights BEGIN
3200
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3201
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
3202
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")},
3203
+ ${jc("old.tags")}, old.search_summary);
3204
+ END
3205
+ `);
3206
+ // --- 4. unified_search_items の insights トリガー更新(l1_content を search_text に追加) ---
3207
+ const coal = (col) => `COALESCE(${col}, '')`;
3208
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
3209
+ db.exec(`DROP TRIGGER IF EXISTS insights_unified_insert`);
3210
+ db.exec(`DROP TRIGGER IF EXISTS insights_unified_update`);
3211
+ db.exec(`DROP TRIGGER IF EXISTS insights_unified_delete`);
3212
+ const insightSearch = (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`)}`;
3213
+ db.exec(`
3214
+ CREATE TRIGGER insights_unified_insert AFTER INSERT ON insights
3215
+ WHEN new.status = 'active' BEGIN
3216
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3217
+ VALUES ('insight', new.id, new.scope, NULL, new.title, ${insightSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
3218
+ END
3219
+ `);
3220
+ db.exec(`
3221
+ CREATE TRIGGER insights_unified_update AFTER UPDATE ON insights BEGIN
3222
+ DELETE FROM unified_search_items WHERE entity_type = 'insight' AND entity_id = old.id;
3223
+ INSERT INTO unified_search_items(${columns})
3224
+ SELECT 'insight', new.id, new.scope, NULL, new.title, ${insightSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at
3225
+ WHERE new.status = 'active';
3226
+ END
3227
+ `);
3228
+ db.exec(`
3229
+ CREATE TRIGGER insights_unified_delete AFTER DELETE ON insights BEGIN
3230
+ DELETE FROM unified_search_items WHERE entity_type = 'insight' AND entity_id = old.id;
3231
+ END
3232
+ `);
3233
+ // --- 5. FTS + unified_search_items リビルド ---
3234
+ db.exec(`INSERT INTO insights_fts(insights_fts) VALUES('rebuild')`);
3235
+ // insight の unified_search_items を再構築
3236
+ db.exec(`DELETE FROM unified_search_items WHERE entity_type = 'insight'`);
3237
+ db.exec(`
3238
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3239
+ SELECT 'insight', id, scope, NULL, title, ${insightSearch("")}, search_summary, tags, created_at, updated_at
3240
+ FROM insights
3241
+ WHERE status = 'active'
3242
+ `);
3243
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
3244
+ // --- 6. 外部キー整合性チェック ---
3245
+ const fkViolations = db.pragma("foreign_key_check");
3246
+ if (fkViolations.length > 0) {
3247
+ throw new Error(`V19マイグレーション後に外部キー違反が検出されました: ${JSON.stringify(fkViolations)}`);
3248
+ }
3249
+ // --- 7. スキーマバージョン記録 ---
3250
+ db.exec(`INSERT INTO schema_version (version) VALUES (19)`);
3251
+ });
3252
+ transaction();
3253
+ }
3254
+ finally {
3255
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
3256
+ db.pragma("foreign_keys = ON");
3257
+ }
3258
+ }
3259
+ }
3260
+ /**
3261
+ * V20スキーマ: 全エンティティ l1/l2 カラム拡張(Issue #59)
3262
+ * - Theories/Models: core_thesis → l2_core_thesis, principles → l2_principles + 新l2カラム + l1_content + embedding(Theories)
3263
+ * - Episodes: context → l2_context, trigger → l2_trigger, 他5フィールド → l2_* + l1_content
3264
+ * - Decisions: reasoning → l2_reasoning, alternatives → l2_alternatives + l1_content
3265
+ * - 各テーブルの FTS5 / unified_search_items トリガー再構築
3266
+ */
3267
+ function applyV20(db) {
3268
+ const fkState = db.pragma("foreign_keys");
3269
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
3270
+ db.pragma("foreign_keys = OFF");
3271
+ }
3272
+ try {
3273
+ const transaction = db.transaction(() => {
3274
+ // JSON配列クリーニングヘルパー
3275
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
3276
+ const coal = (col) => `COALESCE(${col}, '')`;
3277
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
3278
+ // ========================================================
3279
+ // 1-A: Theories テーブル再構築
3280
+ // ========================================================
3281
+ db.exec(`ALTER TABLE theories RENAME TO theories_old`);
3282
+ db.exec(`
3283
+ CREATE TABLE theories (
3284
+ -- 1. 識別
3285
+ id TEXT PRIMARY KEY,
3286
+ title TEXT NOT NULL,
3287
+ description TEXT,
3288
+
3289
+ -- 2. L1: 自然言語コンテンツ
3290
+ l1_content TEXT,
3291
+
3292
+ -- 3. L2: 構造化コンテンツ
3293
+ l2_core_thesis TEXT,
3294
+ l2_principles TEXT NOT NULL DEFAULT '[]',
3295
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
3296
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
3297
+ l2_applicable_context TEXT,
3298
+
3299
+ -- 4. Theory固有
3300
+ non_goals TEXT NOT NULL DEFAULT '[]',
3301
+ open_questions TEXT NOT NULL DEFAULT '[]',
3302
+
3303
+ -- 5. メタデータ
3304
+ tags TEXT NOT NULL DEFAULT '[]',
3305
+ scope TEXT NOT NULL DEFAULT 'global',
3306
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
3307
+ search_summary TEXT,
3308
+ embedding BLOB,
3309
+
3310
+ -- 6. 参照
3311
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
3312
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
3313
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
3314
+
3315
+ -- 7. 分類(昇格元)
3316
+ promoted_from_store TEXT,
3317
+ promoted_from_id TEXT,
3318
+
3319
+ -- 8. ライフサイクル
3320
+ status TEXT NOT NULL DEFAULT 'active'
3321
+ CHECK(status IN ('active','archived','promoted')),
3322
+
3323
+ -- 9. プロヴナンス
3324
+ source_tool TEXT,
3325
+ session_id TEXT,
3326
+ client_name TEXT,
3327
+ client_version TEXT,
3328
+
3329
+ -- 10. タイムスタンプ
3330
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
3331
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
3332
+ )
3333
+ `);
3334
+ // データ移行: core_thesis → l2_core_thesis, principles → l2_principles
3335
+ db.exec(`
3336
+ INSERT INTO theories (
3337
+ id, title, description,
3338
+ l1_content,
3339
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
3340
+ non_goals, open_questions,
3341
+ tags, scope, confidence, search_summary, embedding,
3342
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
3343
+ promoted_from_store, promoted_from_id,
3344
+ status,
3345
+ source_tool, session_id, client_name, client_version,
3346
+ created_at, updated_at
3347
+ )
3348
+ SELECT
3349
+ id, title, description,
3350
+ NULL,
3351
+ core_thesis, principles, '[]', '[]', NULL,
3352
+ non_goals, open_questions,
3353
+ tags, scope, confidence, search_summary, NULL,
3354
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
3355
+ promoted_from_store, promoted_from_id,
3356
+ status,
3357
+ source_tool, session_id, client_name, client_version,
3358
+ created_at, updated_at
3359
+ FROM theories_old
3360
+ `);
3361
+ db.exec(`DROP TABLE theories_old`);
3362
+ // theories インデックス
3363
+ db.exec(`CREATE INDEX idx_theories_scope ON theories(scope)`);
3364
+ db.exec(`CREATE INDEX idx_theories_status ON theories(status)`);
3365
+ db.exec(`CREATE INDEX idx_theories_confidence ON theories(confidence)`);
3366
+ db.exec(`CREATE INDEX idx_theories_created ON theories(created_at)`);
3367
+ db.exec(`CREATE INDEX idx_theories_updated ON theories(updated_at)`);
3368
+ db.exec(`CREATE UNIQUE INDEX idx_theories_promoted_from ON theories(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
3369
+ // ========================================================
3370
+ // 1-B: Models テーブル再構築(Theoriesと同構造、embeddingは既存値コピー)
3371
+ // ========================================================
3372
+ db.exec(`ALTER TABLE models RENAME TO models_old`);
3373
+ db.exec(`
3374
+ CREATE TABLE models (
3375
+ -- 1. 識別
3376
+ id TEXT PRIMARY KEY,
3377
+ title TEXT NOT NULL,
3378
+ description TEXT,
3379
+
3380
+ -- 2. L1: 自然言語コンテンツ
3381
+ l1_content TEXT,
3382
+
3383
+ -- 3. L2: 構造化コンテンツ
3384
+ l2_core_thesis TEXT,
3385
+ l2_principles TEXT NOT NULL DEFAULT '[]',
3386
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
3387
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
3388
+ l2_applicable_context TEXT,
3389
+
3390
+ -- 4. Model固有
3391
+ non_goals TEXT NOT NULL DEFAULT '[]',
3392
+ open_questions TEXT NOT NULL DEFAULT '[]',
3393
+
3394
+ -- 5. メタデータ
3395
+ tags TEXT NOT NULL DEFAULT '[]',
3396
+ scope TEXT NOT NULL DEFAULT 'global',
3397
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
3398
+ search_summary TEXT,
3399
+ embedding BLOB,
3400
+
3401
+ -- 6. 参照
3402
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
3403
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
3404
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
3405
+
3406
+ -- 7. 分類(昇格元)
3407
+ promoted_from_store TEXT,
3408
+ promoted_from_id TEXT,
3409
+
3410
+ -- 8. ライフサイクル
3411
+ status TEXT NOT NULL DEFAULT 'active'
3412
+ CHECK(status IN ('active','archived','promoted')),
3413
+
3414
+ -- 9. プロヴナンス
3415
+ source_tool TEXT,
3416
+ session_id TEXT,
3417
+ client_name TEXT,
3418
+ client_version TEXT,
3419
+
3420
+ -- 10. タイムスタンプ
3421
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
3422
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
3423
+ )
3424
+ `);
3425
+ // データ移行: embeddingは既存値をコピー
3426
+ db.exec(`
3427
+ INSERT INTO models (
3428
+ id, title, description,
3429
+ l1_content,
3430
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
3431
+ non_goals, open_questions,
3432
+ tags, scope, confidence, search_summary, embedding,
3433
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
3434
+ promoted_from_store, promoted_from_id,
3435
+ status,
3436
+ source_tool, session_id, client_name, client_version,
3437
+ created_at, updated_at
3438
+ )
3439
+ SELECT
3440
+ id, title, description,
3441
+ NULL,
3442
+ core_thesis, principles, '[]', '[]', NULL,
3443
+ non_goals, open_questions,
3444
+ tags, scope, confidence, search_summary, embedding,
3445
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
3446
+ promoted_from_store, promoted_from_id,
3447
+ status,
3448
+ source_tool, session_id, client_name, client_version,
3449
+ created_at, updated_at
3450
+ FROM models_old
3451
+ `);
3452
+ db.exec(`DROP TABLE models_old`);
3453
+ // models インデックス
3454
+ db.exec(`CREATE INDEX idx_models_scope ON models(scope)`);
3455
+ db.exec(`CREATE INDEX idx_models_status ON models(status)`);
3456
+ db.exec(`CREATE INDEX idx_models_confidence ON models(confidence)`);
3457
+ db.exec(`CREATE INDEX idx_models_created ON models(created_at)`);
3458
+ db.exec(`CREATE INDEX idx_models_updated ON models(updated_at)`);
3459
+ db.exec(`CREATE UNIQUE INDEX idx_models_promoted_from ON models(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
3460
+ // ========================================================
3461
+ // 1-C: Episodes テーブル再構築
3462
+ // ========================================================
3463
+ db.exec(`ALTER TABLE episodes RENAME TO episodes_old`);
3464
+ db.exec(`
3465
+ CREATE TABLE episodes (
3466
+ -- 1. 識別
3467
+ id TEXT PRIMARY KEY,
3468
+ title TEXT NOT NULL,
3469
+
3470
+ -- 2. L1: 自然言語コンテンツ
3471
+ l1_content TEXT,
3472
+
3473
+ -- 3. L2: 構造化コンテンツ
3474
+ l2_context TEXT,
3475
+ l2_trigger TEXT,
3476
+ l2_problems TEXT NOT NULL DEFAULT '[]',
3477
+ l2_desires TEXT NOT NULL DEFAULT '[]',
3478
+ l2_decisions TEXT NOT NULL DEFAULT '[]',
3479
+ l2_outcomes TEXT NOT NULL DEFAULT '[]',
3480
+ l2_principles TEXT NOT NULL DEFAULT '[]',
3481
+
3482
+ -- 4. メタデータ
3483
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
3484
+ tags TEXT NOT NULL DEFAULT '[]',
3485
+ scope TEXT NOT NULL DEFAULT 'global',
3486
+ search_summary TEXT,
3487
+ embedding BLOB,
3488
+
3489
+ -- 5. 分類(昇格元)
3490
+ promoted_from_store TEXT,
3491
+ promoted_from_id TEXT,
3492
+
3493
+ -- 6. ライフサイクル
3494
+ status TEXT NOT NULL DEFAULT 'active'
3495
+ CHECK(status IN ('active','archived','promoted')),
3496
+ groomed_at TEXT,
3497
+
3498
+ -- 7. プロヴナンス
3499
+ source_tool TEXT,
3500
+ session_id TEXT,
3501
+ client_name TEXT,
3502
+ client_version TEXT,
3503
+ user_input TEXT,
3504
+
3505
+ -- 8. タイムスタンプ
3506
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
3507
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
3508
+ )
3509
+ `);
3510
+ // データ移行: 7フィールドリネーム
3511
+ db.exec(`
3512
+ INSERT INTO episodes (
3513
+ id, title,
3514
+ l1_content,
3515
+ l2_context, l2_trigger, l2_problems, l2_desires, l2_decisions, l2_outcomes, l2_principles,
3516
+ evidence_refs, tags, scope, search_summary, embedding,
3517
+ promoted_from_store, promoted_from_id,
3518
+ status, groomed_at,
3519
+ source_tool, session_id, client_name, client_version, user_input,
3520
+ created_at, updated_at
3521
+ )
3522
+ SELECT
3523
+ id, title,
3524
+ NULL,
3525
+ context, trigger, problems, desires, decisions, outcomes, principles,
3526
+ evidence_refs, tags, scope, search_summary, embedding,
3527
+ promoted_from_store, promoted_from_id,
3528
+ status, groomed_at,
3529
+ source_tool, session_id, client_name, client_version, user_input,
3530
+ created_at, updated_at
3531
+ FROM episodes_old
3532
+ `);
3533
+ db.exec(`DROP TABLE episodes_old`);
3534
+ // episodes インデックス
3535
+ db.exec(`CREATE INDEX idx_episodes_scope ON episodes(scope)`);
3536
+ db.exec(`CREATE INDEX idx_episodes_status ON episodes(status)`);
3537
+ db.exec(`CREATE INDEX idx_episodes_groomed ON episodes(groomed_at)`);
3538
+ db.exec(`CREATE INDEX idx_episodes_created ON episodes(created_at)`);
3539
+ db.exec(`CREATE INDEX idx_episodes_updated ON episodes(updated_at)`);
3540
+ db.exec(`CREATE UNIQUE INDEX idx_episodes_promoted_from ON episodes(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
3541
+ // ========================================================
3542
+ // 1-D: Decisions テーブル再構築
3543
+ // ========================================================
3544
+ db.exec(`ALTER TABLE decisions RENAME TO decisions_old`);
3545
+ db.exec(`
3546
+ CREATE TABLE decisions (
3547
+ -- 1. 識別
3548
+ id TEXT PRIMARY KEY,
3549
+ title TEXT NOT NULL,
3550
+ description TEXT NOT NULL,
3551
+
3552
+ -- 2. L1: 自然言語コンテンツ
3553
+ l1_content TEXT,
3554
+
3555
+ -- 3. L2: 構造化コンテンツ
3556
+ l2_reasoning TEXT NOT NULL,
3557
+ l2_alternatives TEXT NOT NULL DEFAULT '[]',
3558
+
3559
+ -- 4. メタデータ
3560
+ related_claim_ids TEXT NOT NULL DEFAULT '[]',
3561
+ scope TEXT NOT NULL DEFAULT 'global',
3562
+ search_summary TEXT,
3563
+ embedding BLOB,
3564
+
3565
+ -- 5. 分類(昇格元)
3566
+ promoted_from_store TEXT,
3567
+ promoted_from_id TEXT,
3568
+
3569
+ -- 6. ライフサイクル
3570
+ status TEXT NOT NULL DEFAULT 'active'
3571
+ CHECK(status IN ('active','reversed','obsolete','promoted')),
3572
+
3573
+ -- 7. プロヴナンス
3574
+ source_tool TEXT,
3575
+ client_name TEXT,
3576
+ client_version TEXT,
3577
+ user_input TEXT,
3578
+
3579
+ -- 8. タイムスタンプ
3580
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
3581
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
3582
+ )
3583
+ `);
3584
+ // データ移行: reasoning → l2_reasoning, alternatives → l2_alternatives
3585
+ db.exec(`
3586
+ INSERT INTO decisions (
3587
+ id, title, description,
3588
+ l1_content,
3589
+ l2_reasoning, l2_alternatives,
3590
+ related_claim_ids, scope, search_summary, embedding,
3591
+ promoted_from_store, promoted_from_id,
3592
+ status,
3593
+ source_tool, client_name, client_version, user_input,
3594
+ created_at, updated_at
3595
+ )
3596
+ SELECT
3597
+ id, title, description,
3598
+ NULL,
3599
+ reasoning, alternatives,
3600
+ related_claim_ids, scope, search_summary, embedding,
3601
+ promoted_from_store, promoted_from_id,
3602
+ status,
3603
+ source_tool, client_name, client_version, user_input,
3604
+ created_at, updated_at
3605
+ FROM decisions_old
3606
+ `);
3607
+ db.exec(`DROP TABLE decisions_old`);
3608
+ // decisions インデックス
3609
+ db.exec(`CREATE INDEX idx_decisions_scope ON decisions(scope)`);
3610
+ db.exec(`CREATE INDEX idx_decisions_status ON decisions(status)`);
3611
+ db.exec(`CREATE INDEX idx_decisions_created ON decisions(created_at)`);
3612
+ db.exec(`CREATE INDEX idx_decisions_updated ON decisions(updated_at)`);
3613
+ db.exec(`CREATE UNIQUE INDEX idx_decisions_promoted_from ON decisions(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
3614
+ // ========================================================
3615
+ // 1-E: FTS 再構築(4テーブル)
3616
+ // ========================================================
3617
+ // --- theories_fts ---
3618
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_insert`);
3619
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_update`);
3620
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_delete`);
3621
+ db.exec(`DROP TABLE IF EXISTS theories_fts`);
3622
+ db.exec(`
3623
+ CREATE VIRTUAL TABLE theories_fts USING fts5(
3624
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
3625
+ content='theories',
3626
+ content_rowid='rowid',
3627
+ tokenize='trigram'
3628
+ )
3629
+ `);
3630
+ db.exec(`
3631
+ CREATE TRIGGER theories_fts_insert AFTER INSERT ON theories BEGIN
3632
+ INSERT INTO theories_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3633
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
3634
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")},
3635
+ ${jc("new.tags")}, new.search_summary);
3636
+ END
3637
+ `);
3638
+ db.exec(`
3639
+ CREATE TRIGGER theories_fts_update AFTER UPDATE ON theories BEGIN
3640
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3641
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
3642
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")},
3643
+ ${jc("old.tags")}, old.search_summary);
3644
+ INSERT INTO theories_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3645
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
3646
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")},
3647
+ ${jc("new.tags")}, new.search_summary);
3648
+ END
3649
+ `);
3650
+ db.exec(`
3651
+ CREATE TRIGGER theories_fts_delete AFTER DELETE ON theories BEGIN
3652
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3653
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
3654
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")},
3655
+ ${jc("old.tags")}, old.search_summary);
3656
+ END
3657
+ `);
3658
+ // --- models_fts ---
3659
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_insert`);
3660
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_update`);
3661
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_delete`);
3662
+ db.exec(`DROP TABLE IF EXISTS models_fts`);
3663
+ db.exec(`
3664
+ CREATE VIRTUAL TABLE models_fts USING fts5(
3665
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
3666
+ content='models',
3667
+ content_rowid='rowid',
3668
+ tokenize='trigram'
3669
+ )
3670
+ `);
3671
+ db.exec(`
3672
+ CREATE TRIGGER models_fts_insert AFTER INSERT ON models BEGIN
3673
+ INSERT INTO models_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3674
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
3675
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")},
3676
+ ${jc("new.tags")}, new.search_summary);
3677
+ END
3678
+ `);
3679
+ db.exec(`
3680
+ CREATE TRIGGER models_fts_update AFTER UPDATE ON models BEGIN
3681
+ INSERT INTO models_fts(models_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3682
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
3683
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")},
3684
+ ${jc("old.tags")}, old.search_summary);
3685
+ INSERT INTO models_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3686
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
3687
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")},
3688
+ ${jc("new.tags")}, new.search_summary);
3689
+ END
3690
+ `);
3691
+ db.exec(`
3692
+ CREATE TRIGGER models_fts_delete AFTER DELETE ON models BEGIN
3693
+ INSERT INTO models_fts(models_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
3694
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
3695
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")},
3696
+ ${jc("old.tags")}, old.search_summary);
3697
+ END
3698
+ `);
3699
+ // --- episodes_fts ---
3700
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_insert`);
3701
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_update`);
3702
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_delete`);
3703
+ db.exec(`DROP TABLE IF EXISTS episodes_fts`);
3704
+ db.exec(`
3705
+ CREATE VIRTUAL TABLE episodes_fts USING fts5(
3706
+ title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input,
3707
+ content='episodes',
3708
+ content_rowid='rowid',
3709
+ tokenize='trigram'
3710
+ )
3711
+ `);
3712
+ db.exec(`
3713
+ CREATE TRIGGER episodes_fts_insert AFTER INSERT ON episodes BEGIN
3714
+ 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)
3715
+ VALUES (new.rowid, new.title, new.l1_content, new.l2_context, new.l2_trigger,
3716
+ ${jc("new.l2_principles")}, ${jc("new.l2_problems")}, ${jc("new.l2_desires")},
3717
+ ${jc("new.l2_decisions")}, ${jc("new.l2_outcomes")},
3718
+ new.search_summary, new.user_input);
3719
+ END
3720
+ `);
3721
+ db.exec(`
3722
+ CREATE TRIGGER episodes_fts_update AFTER UPDATE ON episodes BEGIN
3723
+ 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)
3724
+ VALUES ('delete', old.rowid, old.title, old.l1_content, old.l2_context, old.l2_trigger,
3725
+ ${jc("old.l2_principles")}, ${jc("old.l2_problems")}, ${jc("old.l2_desires")},
3726
+ ${jc("old.l2_decisions")}, ${jc("old.l2_outcomes")},
3727
+ old.search_summary, old.user_input);
3728
+ 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)
3729
+ VALUES (new.rowid, new.title, new.l1_content, new.l2_context, new.l2_trigger,
3730
+ ${jc("new.l2_principles")}, ${jc("new.l2_problems")}, ${jc("new.l2_desires")},
3731
+ ${jc("new.l2_decisions")}, ${jc("new.l2_outcomes")},
3732
+ new.search_summary, new.user_input);
3733
+ END
3734
+ `);
3735
+ db.exec(`
3736
+ CREATE TRIGGER episodes_fts_delete AFTER DELETE ON episodes BEGIN
3737
+ 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)
3738
+ VALUES ('delete', old.rowid, old.title, old.l1_content, old.l2_context, old.l2_trigger,
3739
+ ${jc("old.l2_principles")}, ${jc("old.l2_problems")}, ${jc("old.l2_desires")},
3740
+ ${jc("old.l2_decisions")}, ${jc("old.l2_outcomes")},
3741
+ old.search_summary, old.user_input);
3742
+ END
3743
+ `);
3744
+ // --- decisions_fts ---
3745
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_insert`);
3746
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_update`);
3747
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_delete`);
3748
+ db.exec(`DROP TABLE IF EXISTS decisions_fts`);
3749
+ db.exec(`
3750
+ CREATE VIRTUAL TABLE decisions_fts USING fts5(
3751
+ title, description, l1_content, l2_reasoning, search_summary, user_input,
3752
+ content='decisions',
3753
+ content_rowid='rowid',
3754
+ tokenize='trigram'
3755
+ )
3756
+ `);
3757
+ db.exec(`
3758
+ CREATE TRIGGER decisions_fts_insert AFTER INSERT ON decisions BEGIN
3759
+ INSERT INTO decisions_fts(rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
3760
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_reasoning, new.search_summary, new.user_input);
3761
+ END
3762
+ `);
3763
+ db.exec(`
3764
+ CREATE TRIGGER decisions_fts_update AFTER UPDATE ON decisions BEGIN
3765
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
3766
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_reasoning, old.search_summary, old.user_input);
3767
+ INSERT INTO decisions_fts(rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
3768
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_reasoning, new.search_summary, new.user_input);
3769
+ END
3770
+ `);
3771
+ db.exec(`
3772
+ CREATE TRIGGER decisions_fts_delete AFTER DELETE ON decisions BEGIN
3773
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
3774
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_reasoning, old.search_summary, old.user_input);
3775
+ END
3776
+ `);
3777
+ // ========================================================
3778
+ // 1-F: unified_search_items トリガー再構築(4テーブル)
3779
+ // ========================================================
3780
+ // --- theories ---
3781
+ db.exec(`DROP TRIGGER IF EXISTS theories_unified_insert`);
3782
+ db.exec(`DROP TRIGGER IF EXISTS theories_unified_update`);
3783
+ db.exec(`DROP TRIGGER IF EXISTS theories_unified_delete`);
3784
+ const theorySearch = (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}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`;
3785
+ db.exec(`
3786
+ CREATE TRIGGER theories_unified_insert AFTER INSERT ON theories
3787
+ WHEN new.status = 'active' BEGIN
3788
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3789
+ VALUES ('theory', new.id, new.scope, NULL, new.title, ${theorySearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
3790
+ END
3791
+ `);
3792
+ db.exec(`
3793
+ CREATE TRIGGER theories_unified_update AFTER UPDATE ON theories BEGIN
3794
+ DELETE FROM unified_search_items WHERE entity_type = 'theory' AND entity_id = old.id;
3795
+ INSERT INTO unified_search_items(${columns})
3796
+ SELECT 'theory', new.id, new.scope, NULL, new.title, ${theorySearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at
3797
+ WHERE new.status = 'active';
3798
+ END
3799
+ `);
3800
+ db.exec(`
3801
+ CREATE TRIGGER theories_unified_delete AFTER DELETE ON theories BEGIN
3802
+ DELETE FROM unified_search_items WHERE entity_type = 'theory' AND entity_id = old.id;
3803
+ END
3804
+ `);
3805
+ // --- models ---
3806
+ db.exec(`DROP TRIGGER IF EXISTS models_unified_insert`);
3807
+ db.exec(`DROP TRIGGER IF EXISTS models_unified_update`);
3808
+ db.exec(`DROP TRIGGER IF EXISTS models_unified_delete`);
3809
+ const modelSearch = (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}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`;
3810
+ db.exec(`
3811
+ CREATE TRIGGER models_unified_insert AFTER INSERT ON models
3812
+ WHEN new.status = 'active' BEGIN
3813
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3814
+ VALUES ('model', new.id, new.scope, NULL, new.title, ${modelSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
3815
+ END
3816
+ `);
3817
+ db.exec(`
3818
+ CREATE TRIGGER models_unified_update AFTER UPDATE ON models BEGIN
3819
+ DELETE FROM unified_search_items WHERE entity_type = 'model' AND entity_id = old.id;
3820
+ INSERT INTO unified_search_items(${columns})
3821
+ SELECT 'model', new.id, new.scope, NULL, new.title, ${modelSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at
3822
+ WHERE new.status = 'active';
3823
+ END
3824
+ `);
3825
+ db.exec(`
3826
+ CREATE TRIGGER models_unified_delete AFTER DELETE ON models BEGIN
3827
+ DELETE FROM unified_search_items WHERE entity_type = 'model' AND entity_id = old.id;
3828
+ END
3829
+ `);
3830
+ // --- episodes ---
3831
+ db.exec(`DROP TRIGGER IF EXISTS episodes_unified_insert`);
3832
+ db.exec(`DROP TRIGGER IF EXISTS episodes_unified_update`);
3833
+ db.exec(`DROP TRIGGER IF EXISTS episodes_unified_delete`);
3834
+ const epSearch = (p) => `${p}title || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}l2_context`)} || ' ' || ${coal(`${p}l2_trigger`)} || ' ' || ${jc(`${p}l2_problems`)} || ' ' || ${jc(`${p}l2_desires`)} || ' ' || ${jc(`${p}l2_decisions`)} || ' ' || ${jc(`${p}l2_outcomes`)} || ' ' || ${jc(`${p}l2_principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`;
3835
+ db.exec(`
3836
+ CREATE TRIGGER episodes_unified_insert AFTER INSERT ON episodes
3837
+ WHEN new.status = 'active' BEGIN
3838
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3839
+ VALUES ('episode', new.id, new.scope, NULL, new.title, ${epSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at);
3840
+ END
3841
+ `);
3842
+ db.exec(`
3843
+ CREATE TRIGGER episodes_unified_update AFTER UPDATE ON episodes BEGIN
3844
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
3845
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3846
+ SELECT 'episode', new.id, new.scope, NULL, new.title, ${epSearch("new.")}, new.search_summary, new.tags, new.created_at, new.updated_at
3847
+ WHERE new.status = 'active';
3848
+ END
3849
+ `);
3850
+ db.exec(`
3851
+ CREATE TRIGGER episodes_unified_delete AFTER DELETE ON episodes BEGIN
3852
+ DELETE FROM unified_search_items WHERE entity_type = 'episode' AND entity_id = old.id;
3853
+ END
3854
+ `);
3855
+ // --- decisions ---
3856
+ db.exec(`DROP TRIGGER IF EXISTS decisions_unified_insert`);
3857
+ db.exec(`DROP TRIGGER IF EXISTS decisions_unified_update`);
3858
+ db.exec(`DROP TRIGGER IF EXISTS decisions_unified_delete`);
3859
+ const decSearch = (p) => `${p}title || ' ' || ${p}description || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${p}l2_reasoning || ' ' || ${jc(`${p}l2_alternatives`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`;
3860
+ db.exec(`
3861
+ CREATE TRIGGER decisions_unified_insert AFTER INSERT ON decisions
3862
+ WHEN new.status = 'active' BEGIN
3863
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3864
+ VALUES ('decision', new.id, new.scope, NULL, new.title, ${decSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at);
3865
+ END
3866
+ `);
3867
+ db.exec(`
3868
+ CREATE TRIGGER decisions_unified_update AFTER UPDATE ON decisions BEGIN
3869
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
3870
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3871
+ SELECT 'decision', new.id, new.scope, NULL, new.title, ${decSearch("new.")}, new.search_summary, '[]', new.created_at, new.updated_at
3872
+ WHERE new.status = 'active';
3873
+ END
3874
+ `);
3875
+ db.exec(`
3876
+ CREATE TRIGGER decisions_unified_delete AFTER DELETE ON decisions BEGIN
3877
+ DELETE FROM unified_search_items WHERE entity_type = 'decision' AND entity_id = old.id;
3878
+ END
3879
+ `);
3880
+ // ========================================================
3881
+ // 1-G: FTS + unified_search_items リビルド
3882
+ // ========================================================
3883
+ db.exec(`INSERT INTO theories_fts(theories_fts) VALUES('rebuild')`);
3884
+ db.exec(`INSERT INTO models_fts(models_fts) VALUES('rebuild')`);
3885
+ db.exec(`INSERT INTO episodes_fts(episodes_fts) VALUES('rebuild')`);
3886
+ db.exec(`INSERT INTO decisions_fts(decisions_fts) VALUES('rebuild')`);
3887
+ // unified_search_items の4エンティティを再構築
3888
+ for (const e of [
3889
+ { type: "theory", table: "theories", search: theorySearch },
3890
+ { type: "model", table: "models", search: modelSearch },
3891
+ { type: "episode", table: "episodes", search: epSearch },
3892
+ { type: "decision", table: "decisions", search: decSearch },
3893
+ ]) {
3894
+ db.exec(`DELETE FROM unified_search_items WHERE entity_type = '${e.type}'`);
3895
+ db.exec(`
3896
+ INSERT OR REPLACE INTO unified_search_items(${columns})
3897
+ SELECT '${e.type}', id, scope, NULL, title, ${e.search("")}, search_summary, ${e.type === "decision" ? "'[]'" : "tags"}, created_at, updated_at
3898
+ FROM ${e.table}
3899
+ WHERE status = 'active'
3900
+ `);
3901
+ }
3902
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
3903
+ // --- 外部キー整合性チェック ---
3904
+ const fkViolations = db.pragma("foreign_key_check");
3905
+ if (fkViolations.length > 0) {
3906
+ throw new Error(`V20マイグレーション後に外部キー違反が検出されました: ${JSON.stringify(fkViolations)}`);
3907
+ }
3908
+ // --- スキーマバージョン記録 ---
3909
+ db.exec(`INSERT INTO schema_version (version) VALUES (20)`);
3910
+ });
3911
+ transaction();
3912
+ }
3913
+ finally {
3914
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
3915
+ db.pragma("foreign_keys = ON");
3916
+ }
3917
+ }
3918
+ }
3919
+ /**
3920
+ * V21: audit_log に client_id カラム追加 + 既存レコードの backfill
3921
+ * どのデバイスで作られたレコードか追跡するため。
3922
+ * sync で複数デバイスのデータが混在する前に backfill することで、
3923
+ * 既存データにもデバイス情報を付与する。
3924
+ */
3925
+ function applyV21(db) {
3926
+ // audit_log に client_id カラムを追加
3927
+ if (!hasColumn(db, "audit_log", "client_id")) {
3928
+ db.exec(`ALTER TABLE audit_log ADD COLUMN client_id TEXT`);
3929
+ }
3930
+ // 既存レコードに現在のデバイスの client_id を backfill
3931
+ // (まだマージされていない = すべて自分のデバイスで作ったレコード)
3932
+ const clientIdRow = db.prepare("SELECT value FROM store_meta WHERE key = 'client_id'").get();
3933
+ if (clientIdRow) {
3934
+ db.prepare("UPDATE audit_log SET client_id = ? WHERE client_id IS NULL").run(clientIdRow.value);
3935
+ }
3936
+ db.exec(`INSERT INTO schema_version (version) VALUES (21)`);
3937
+ }
3938
+ /**
3939
+ * V22スキーマ: unified_search_items に entity_id インデックス追加(Issue #60)
3940
+ * entity_id 単独での検索(entity_type 省略時の自動解決)を高速化する。
3941
+ */
3942
+ function applyV22(db) {
3943
+ db.exec(`
3944
+ CREATE INDEX IF NOT EXISTS idx_unified_search_entity_id ON unified_search_items(entity_id);
3945
+ INSERT INTO schema_version (version) VALUES (22);
3946
+ `);
3947
+ }
3948
+ /**
3949
+ * V23マイグレーション: Claim に l1_content 追加 + embedding → l1_embedding リネーム (Issue #64)
3950
+ * - claims テーブル再構築: l1_content TEXT 追加 + embedding → l1_embedding
3951
+ * - 他8テーブル: embedding → l1_embedding リネーム(episodes, decisions, theories, insights, models, user_memos, user_plans, user_issues)
3952
+ * - unified_search_items: embedding → l1_embedding
3953
+ * - claims_fts: l1_content カラム追加
3954
+ * - 全 unified_search_items トリガー再構築(claims の search_text に l1_content 追加)
3955
+ */
3956
+ function applyV23(db) {
3957
+ const fkState = db.pragma("foreign_keys");
3958
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
3959
+ db.pragma("foreign_keys = OFF");
3960
+ }
3961
+ // 2026-03-03 修正: ALTER TABLE RENAME 時に FK 参照が自動更新されるのを防止
3962
+ // SQLite 3.26+ では legacy_alter_table = OFF (デフォルト) だと
3963
+ // RENAME TABLE 時に他テーブルの FK 参照が新テーブル名に書き換わってしまう。
3964
+ // 例: claim_checks REFERENCES claims(id) → claims_old(id) に変わり、DROP 後にエラー。
3965
+ db.pragma("legacy_alter_table = ON");
3966
+ try {
3967
+ const transaction = db.transaction(() => {
3968
+ // JSON配列クリーニングヘルパー
3969
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
3970
+ const coal = (col) => `COALESCE(${col}, '')`;
3971
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
3972
+ // ========================================================
3973
+ // 1-A: claims テーブル再構築(l1_content 追加 + embedding → l1_embedding)
3974
+ // ========================================================
3975
+ db.exec(`ALTER TABLE claims RENAME TO claims_old`);
3976
+ db.exec(`
3977
+ CREATE TABLE claims (
3978
+ -- 1. 識別
3979
+ id TEXT PRIMARY KEY,
3980
+ subject TEXT NOT NULL,
3981
+ predicate TEXT NOT NULL,
3982
+ object TEXT NOT NULL,
3983
+
3984
+ -- 2. 分類
3985
+ category TEXT NOT NULL CHECK(category IN ('preference','identity','skill','value','workflow','knowledge','custom')),
3986
+ scope TEXT NOT NULL DEFAULT 'global',
3987
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
3988
+
3989
+ -- 3. 知識・検証
3990
+ evidence TEXT,
3991
+ falsifier TEXT,
3992
+
3993
+ -- 4. L1: 自然言語コンテンツ(Issue #64 追加)
3994
+ l1_content TEXT,
3995
+
3996
+ -- 5. メタデータ
3997
+ search_summary TEXT,
3998
+ -- 2026-03-03 リネーム (Issue #64): embedding → l1_embedding
3999
+ l1_embedding BLOB,
4000
+
4001
+ -- 6. エンゲージメント
4002
+ hit_count INTEGER NOT NULL DEFAULT 0,
4003
+ last_hit_at TEXT,
4004
+
4005
+ -- 7. 分類(昇格元)
4006
+ promoted_from_store TEXT,
4007
+ promoted_from_id TEXT,
4008
+
4009
+ -- 8. ライフサイクル
4010
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','retracted','superseded','promoted')),
4011
+
4012
+ -- 9. プロヴナンス
4013
+ source_tool TEXT,
4014
+ source_session TEXT,
4015
+ client_name TEXT,
4016
+ client_version TEXT,
4017
+
4018
+ -- 10. タイムスタンプ
4019
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4020
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4021
+ )
4022
+ `);
4023
+ db.exec(`
4024
+ INSERT INTO claims (
4025
+ id, subject, predicate, object,
4026
+ category, scope, confidence,
4027
+ evidence, falsifier,
4028
+ l1_content,
4029
+ search_summary, l1_embedding,
4030
+ hit_count, last_hit_at,
4031
+ promoted_from_store, promoted_from_id,
4032
+ status,
4033
+ source_tool, source_session, client_name, client_version,
4034
+ created_at, updated_at
4035
+ )
4036
+ SELECT
4037
+ id, subject, predicate, object,
4038
+ category, scope, confidence,
4039
+ evidence, falsifier,
4040
+ NULL,
4041
+ search_summary, embedding,
4042
+ hit_count, last_hit_at,
4043
+ promoted_from_store, promoted_from_id,
4044
+ status,
4045
+ source_tool, source_session, client_name, client_version,
4046
+ created_at, updated_at
4047
+ FROM claims_old
4048
+ `);
4049
+ db.exec(`DROP TABLE claims_old`);
4050
+ // claims インデックス
4051
+ db.exec(`CREATE INDEX idx_claims_category ON claims(category)`);
4052
+ db.exec(`CREATE INDEX idx_claims_scope ON claims(scope)`);
4053
+ db.exec(`CREATE INDEX idx_claims_status ON claims(status)`);
4054
+ db.exec(`CREATE INDEX idx_claims_subject ON claims(subject)`);
4055
+ db.exec(`CREATE INDEX idx_claims_updated ON claims(updated_at)`);
4056
+ db.exec(`CREATE UNIQUE INDEX idx_claims_promoted_from ON claims(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
4057
+ // ========================================================
4058
+ // 1-B: episodes テーブル再構築(embedding → l1_embedding)
4059
+ // ========================================================
4060
+ db.exec(`ALTER TABLE episodes RENAME TO episodes_old`);
4061
+ db.exec(`
4062
+ CREATE TABLE episodes (
4063
+ id TEXT PRIMARY KEY,
4064
+ title TEXT NOT NULL,
4065
+ l1_content TEXT,
4066
+ l2_context TEXT,
4067
+ l2_trigger TEXT,
4068
+ l2_problems TEXT NOT NULL DEFAULT '[]',
4069
+ l2_desires TEXT NOT NULL DEFAULT '[]',
4070
+ l2_decisions TEXT NOT NULL DEFAULT '[]',
4071
+ l2_outcomes TEXT NOT NULL DEFAULT '[]',
4072
+ l2_principles TEXT NOT NULL DEFAULT '[]',
4073
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
4074
+ tags TEXT NOT NULL DEFAULT '[]',
4075
+ scope TEXT NOT NULL DEFAULT 'global',
4076
+ search_summary TEXT,
4077
+ l1_embedding BLOB,
4078
+ promoted_from_store TEXT,
4079
+ promoted_from_id TEXT,
4080
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
4081
+ groomed_at TEXT,
4082
+ source_tool TEXT,
4083
+ session_id TEXT,
4084
+ client_name TEXT,
4085
+ client_version TEXT,
4086
+ user_input TEXT,
4087
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
4088
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
4089
+ )
4090
+ `);
4091
+ db.exec(`
4092
+ INSERT INTO episodes (
4093
+ id, title, l1_content,
4094
+ l2_context, l2_trigger, l2_problems, l2_desires, l2_decisions, l2_outcomes, l2_principles,
4095
+ evidence_refs, tags, scope, search_summary, l1_embedding,
4096
+ promoted_from_store, promoted_from_id,
4097
+ status, groomed_at,
4098
+ source_tool, session_id, client_name, client_version, user_input,
4099
+ created_at, updated_at
4100
+ )
4101
+ SELECT
4102
+ id, title, l1_content,
4103
+ l2_context, l2_trigger, l2_problems, l2_desires, l2_decisions, l2_outcomes, l2_principles,
4104
+ evidence_refs, tags, scope, search_summary, embedding,
4105
+ promoted_from_store, promoted_from_id,
4106
+ status, groomed_at,
4107
+ source_tool, session_id, client_name, client_version, user_input,
4108
+ created_at, updated_at
4109
+ FROM episodes_old
4110
+ `);
4111
+ db.exec(`DROP TABLE episodes_old`);
4112
+ db.exec(`CREATE INDEX idx_episodes_scope ON episodes(scope)`);
4113
+ db.exec(`CREATE INDEX idx_episodes_status ON episodes(status)`);
4114
+ db.exec(`CREATE INDEX idx_episodes_created ON episodes(created_at)`);
4115
+ db.exec(`CREATE INDEX idx_episodes_updated ON episodes(updated_at)`);
4116
+ db.exec(`CREATE UNIQUE INDEX idx_episodes_promoted_from ON episodes(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
4117
+ // ========================================================
4118
+ // 1-C: decisions テーブル再構築(embedding → l1_embedding)
4119
+ // ========================================================
4120
+ db.exec(`ALTER TABLE decisions RENAME TO decisions_old`);
4121
+ db.exec(`
4122
+ CREATE TABLE decisions (
4123
+ id TEXT PRIMARY KEY,
4124
+ title TEXT NOT NULL,
4125
+ description TEXT NOT NULL,
4126
+ l1_content TEXT,
4127
+ l2_reasoning TEXT NOT NULL,
4128
+ l2_alternatives TEXT NOT NULL DEFAULT '[]',
4129
+ related_claim_ids TEXT NOT NULL DEFAULT '[]',
4130
+ scope TEXT NOT NULL DEFAULT 'global',
4131
+ search_summary TEXT,
4132
+ l1_embedding BLOB,
4133
+ promoted_from_store TEXT,
4134
+ promoted_from_id TEXT,
4135
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','reversed','obsolete','promoted')),
4136
+ source_tool TEXT,
4137
+ client_name TEXT,
4138
+ client_version TEXT,
4139
+ user_input TEXT,
4140
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
4141
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
4142
+ )
4143
+ `);
4144
+ db.exec(`
4145
+ INSERT INTO decisions (
4146
+ id, title, description, l1_content, l2_reasoning, l2_alternatives,
4147
+ related_claim_ids, scope, search_summary, l1_embedding,
4148
+ promoted_from_store, promoted_from_id,
4149
+ status, source_tool, client_name, client_version, user_input,
4150
+ created_at, updated_at
4151
+ )
4152
+ SELECT
4153
+ id, title, description, l1_content, l2_reasoning, l2_alternatives,
4154
+ related_claim_ids, scope, search_summary, embedding,
4155
+ promoted_from_store, promoted_from_id,
4156
+ status, source_tool, client_name, client_version, user_input,
4157
+ created_at, updated_at
4158
+ FROM decisions_old
4159
+ `);
4160
+ db.exec(`DROP TABLE decisions_old`);
4161
+ db.exec(`CREATE INDEX idx_decisions_scope ON decisions(scope)`);
4162
+ db.exec(`CREATE INDEX idx_decisions_status ON decisions(status)`);
4163
+ db.exec(`CREATE INDEX idx_decisions_created ON decisions(created_at)`);
4164
+ db.exec(`CREATE INDEX idx_decisions_updated ON decisions(updated_at)`);
4165
+ db.exec(`CREATE UNIQUE INDEX idx_decisions_promoted_from ON decisions(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
4166
+ // ========================================================
4167
+ // 1-D: theories テーブル再構築(embedding → l1_embedding)
4168
+ // ========================================================
4169
+ db.exec(`ALTER TABLE theories RENAME TO theories_old`);
4170
+ db.exec(`
4171
+ CREATE TABLE theories (
4172
+ id TEXT PRIMARY KEY,
4173
+ title TEXT NOT NULL,
4174
+ description TEXT,
4175
+ l1_content TEXT,
4176
+ l2_core_thesis TEXT,
4177
+ l2_principles TEXT NOT NULL DEFAULT '[]',
4178
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
4179
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
4180
+ l2_applicable_context TEXT,
4181
+ non_goals TEXT NOT NULL DEFAULT '[]',
4182
+ open_questions TEXT NOT NULL DEFAULT '[]',
4183
+ tags TEXT NOT NULL DEFAULT '[]',
4184
+ scope TEXT NOT NULL DEFAULT 'global',
4185
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
4186
+ search_summary TEXT,
4187
+ l1_embedding BLOB,
4188
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
4189
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
4190
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
4191
+ promoted_from_store TEXT,
4192
+ promoted_from_id TEXT,
4193
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
4194
+ source_tool TEXT,
4195
+ session_id TEXT,
4196
+ client_name TEXT,
4197
+ client_version TEXT,
4198
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
4199
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
4200
+ )
4201
+ `);
4202
+ db.exec(`
4203
+ INSERT INTO theories (
4204
+ id, title, description, l1_content,
4205
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
4206
+ non_goals, open_questions,
4207
+ tags, scope, confidence, search_summary, l1_embedding,
4208
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
4209
+ promoted_from_store, promoted_from_id,
4210
+ status, source_tool, session_id, client_name, client_version,
4211
+ created_at, updated_at
4212
+ )
4213
+ SELECT
4214
+ id, title, description, l1_content,
4215
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
4216
+ non_goals, open_questions,
4217
+ tags, scope, confidence, search_summary, embedding,
4218
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
4219
+ promoted_from_store, promoted_from_id,
4220
+ status, source_tool, session_id, client_name, client_version,
4221
+ created_at, updated_at
4222
+ FROM theories_old
4223
+ `);
4224
+ db.exec(`DROP TABLE theories_old`);
4225
+ db.exec(`CREATE INDEX idx_theories_scope ON theories(scope)`);
4226
+ db.exec(`CREATE INDEX idx_theories_status ON theories(status)`);
4227
+ db.exec(`CREATE INDEX idx_theories_created ON theories(created_at)`);
4228
+ db.exec(`CREATE INDEX idx_theories_updated ON theories(updated_at)`);
4229
+ db.exec(`CREATE UNIQUE INDEX idx_theories_promoted_from ON theories(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
4230
+ // ========================================================
4231
+ // 1-E: insights テーブル再構築(embedding → l1_embedding)
4232
+ // ========================================================
4233
+ db.exec(`ALTER TABLE insights RENAME TO insights_old`);
4234
+ db.exec(`
4235
+ CREATE TABLE insights (
4236
+ id TEXT PRIMARY KEY,
4237
+ title TEXT NOT NULL,
4238
+ description TEXT,
4239
+ l1_content TEXT,
4240
+ l2_core_thesis TEXT,
4241
+ l2_principles TEXT NOT NULL DEFAULT '[]',
4242
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
4243
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
4244
+ l2_applicable_context TEXT,
4245
+ tags TEXT NOT NULL DEFAULT '[]',
4246
+ scope TEXT NOT NULL DEFAULT 'global',
4247
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
4248
+ search_summary TEXT,
4249
+ l1_embedding BLOB,
4250
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
4251
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
4252
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
4253
+ promoted_from_store TEXT,
4254
+ promoted_from_id TEXT,
4255
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
4256
+ source_tool TEXT,
4257
+ session_id TEXT,
4258
+ client_name TEXT,
4259
+ client_version TEXT,
4260
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
4261
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
4262
+ )
4263
+ `);
4264
+ db.exec(`
4265
+ INSERT INTO insights (
4266
+ id, title, description, l1_content,
4267
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
4268
+ tags, scope, confidence, search_summary, l1_embedding,
4269
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
4270
+ promoted_from_store, promoted_from_id,
4271
+ status, source_tool, session_id, client_name, client_version,
4272
+ created_at, updated_at
4273
+ )
4274
+ SELECT
4275
+ id, title, description, l1_content,
4276
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
4277
+ tags, scope, confidence, search_summary, embedding,
4278
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
4279
+ promoted_from_store, promoted_from_id,
4280
+ status, source_tool, session_id, client_name, client_version,
4281
+ created_at, updated_at
4282
+ FROM insights_old
4283
+ `);
4284
+ db.exec(`DROP TABLE insights_old`);
4285
+ db.exec(`CREATE INDEX idx_insights_scope ON insights(scope)`);
4286
+ db.exec(`CREATE INDEX idx_insights_status ON insights(status)`);
4287
+ db.exec(`CREATE INDEX idx_insights_created ON insights(created_at)`);
4288
+ db.exec(`CREATE INDEX idx_insights_updated ON insights(updated_at)`);
4289
+ db.exec(`CREATE UNIQUE INDEX idx_insights_promoted_from ON insights(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
4290
+ // ========================================================
4291
+ // 1-F: models テーブル再構築(embedding → l1_embedding)
4292
+ // ========================================================
4293
+ db.exec(`ALTER TABLE models RENAME TO models_old`);
4294
+ db.exec(`
4295
+ CREATE TABLE models (
4296
+ id TEXT PRIMARY KEY,
4297
+ title TEXT NOT NULL,
4298
+ description TEXT,
4299
+ l1_content TEXT,
4300
+ l2_core_thesis TEXT,
4301
+ l2_principles TEXT NOT NULL DEFAULT '[]',
4302
+ l2_trigger_conditions TEXT NOT NULL DEFAULT '[]',
4303
+ l2_resolution_steps TEXT NOT NULL DEFAULT '[]',
4304
+ l2_applicable_context TEXT,
4305
+ non_goals TEXT NOT NULL DEFAULT '[]',
4306
+ open_questions TEXT NOT NULL DEFAULT '[]',
4307
+ tags TEXT NOT NULL DEFAULT '[]',
4308
+ scope TEXT NOT NULL DEFAULT 'global',
4309
+ confidence REAL NOT NULL DEFAULT 0.8 CHECK(confidence >= 0.0 AND confidence <= 1.0),
4310
+ search_summary TEXT,
4311
+ l1_embedding BLOB,
4312
+ supporting_episode_ids TEXT NOT NULL DEFAULT '[]',
4313
+ supporting_claim_ids TEXT NOT NULL DEFAULT '[]',
4314
+ evidence_refs TEXT NOT NULL DEFAULT '[]',
4315
+ promoted_from_store TEXT,
4316
+ promoted_from_id TEXT,
4317
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','promoted')),
4318
+ source_tool TEXT,
4319
+ session_id TEXT,
4320
+ client_name TEXT,
4321
+ client_version TEXT,
4322
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
4323
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
4324
+ )
4325
+ `);
4326
+ db.exec(`
4327
+ INSERT INTO models (
4328
+ id, title, description, l1_content,
4329
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
4330
+ non_goals, open_questions,
4331
+ tags, scope, confidence, search_summary, l1_embedding,
4332
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
4333
+ promoted_from_store, promoted_from_id,
4334
+ status, source_tool, session_id, client_name, client_version,
4335
+ created_at, updated_at
4336
+ )
4337
+ SELECT
4338
+ id, title, description, l1_content,
4339
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
4340
+ non_goals, open_questions,
4341
+ tags, scope, confidence, search_summary, embedding,
4342
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
4343
+ promoted_from_store, promoted_from_id,
4344
+ status, source_tool, session_id, client_name, client_version,
4345
+ created_at, updated_at
4346
+ FROM models_old
4347
+ `);
4348
+ db.exec(`DROP TABLE models_old`);
4349
+ db.exec(`CREATE INDEX idx_models_scope ON models(scope)`);
4350
+ db.exec(`CREATE INDEX idx_models_status ON models(status)`);
4351
+ db.exec(`CREATE INDEX idx_models_created ON models(created_at)`);
4352
+ db.exec(`CREATE INDEX idx_models_updated ON models(updated_at)`);
4353
+ db.exec(`CREATE UNIQUE INDEX idx_models_promoted_from ON models(promoted_from_store, promoted_from_id) WHERE promoted_from_store IS NOT NULL`);
4354
+ // ========================================================
4355
+ // 1-G: user_memos テーブル再構築(embedding → l1_embedding)
4356
+ // ========================================================
4357
+ db.exec(`ALTER TABLE user_memos RENAME TO user_memos_old`);
4358
+ db.exec(`
4359
+ CREATE TABLE user_memos (
4360
+ id TEXT PRIMARY KEY,
4361
+ title TEXT NOT NULL,
4362
+ content TEXT NOT NULL,
4363
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
4364
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
4365
+ tags TEXT NOT NULL DEFAULT '[]',
4366
+ scope TEXT NOT NULL DEFAULT 'global',
4367
+ search_summary TEXT,
4368
+ status TEXT NOT NULL DEFAULT 'active'
4369
+ CHECK(status IN ('active','archived')),
4370
+ l1_embedding BLOB,
4371
+ client_name TEXT,
4372
+ client_version TEXT,
4373
+ source_tool TEXT,
4374
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4375
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4376
+ )
4377
+ `);
4378
+ db.exec(`
4379
+ INSERT INTO user_memos (
4380
+ id, title, content, usage_policy, tags, scope, search_summary,
4381
+ status, l1_embedding, client_name, client_version, source_tool,
4382
+ created_at, updated_at
4383
+ )
4384
+ SELECT
4385
+ id, title, content, usage_policy, tags, scope, search_summary,
4386
+ status, embedding, client_name, client_version, source_tool,
4387
+ created_at, updated_at
4388
+ FROM user_memos_old
4389
+ `);
4390
+ db.exec(`DROP TABLE user_memos_old`);
4391
+ db.exec(`CREATE INDEX idx_user_memos_status ON user_memos(status)`);
4392
+ db.exec(`CREATE INDEX idx_user_memos_scope ON user_memos(scope)`);
4393
+ db.exec(`CREATE INDEX idx_user_memos_usage_policy ON user_memos(usage_policy)`);
4394
+ db.exec(`CREATE INDEX idx_user_memos_updated ON user_memos(updated_at)`);
4395
+ // ========================================================
4396
+ // 1-H: user_plans テーブル再構築(embedding → l1_embedding)
4397
+ // ========================================================
4398
+ db.exec(`ALTER TABLE user_plans RENAME TO user_plans_old`);
4399
+ db.exec(`
4400
+ CREATE TABLE user_plans (
4401
+ id TEXT PRIMARY KEY,
4402
+ title TEXT NOT NULL,
4403
+ content TEXT NOT NULL,
4404
+ usage_policy TEXT NOT NULL DEFAULT 'auto'
4405
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
4406
+ tags TEXT NOT NULL DEFAULT '[]',
4407
+ scope TEXT NOT NULL DEFAULT 'global',
4408
+ search_summary TEXT,
4409
+ user_input TEXT,
4410
+ status TEXT NOT NULL DEFAULT 'active'
4411
+ CHECK(status IN ('active','completed','archived')),
4412
+ l1_embedding BLOB,
4413
+ client_name TEXT,
4414
+ client_version TEXT,
4415
+ source_tool TEXT,
4416
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4417
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4418
+ )
4419
+ `);
4420
+ db.exec(`
4421
+ INSERT INTO user_plans (
4422
+ id, title, content, usage_policy, tags, scope, search_summary,
4423
+ user_input, status, l1_embedding, client_name, client_version, source_tool,
4424
+ created_at, updated_at
4425
+ )
4426
+ SELECT
4427
+ id, title, content, usage_policy, tags, scope, search_summary,
4428
+ user_input, status, embedding, client_name, client_version, source_tool,
4429
+ created_at, updated_at
4430
+ FROM user_plans_old
4431
+ `);
4432
+ db.exec(`DROP TABLE user_plans_old`);
4433
+ db.exec(`CREATE INDEX idx_user_plans_status ON user_plans(status)`);
4434
+ db.exec(`CREATE INDEX idx_user_plans_scope ON user_plans(scope)`);
4435
+ db.exec(`CREATE INDEX idx_user_plans_usage_policy ON user_plans(usage_policy)`);
4436
+ db.exec(`CREATE INDEX idx_user_plans_updated ON user_plans(updated_at)`);
4437
+ // ========================================================
4438
+ // 1-I: user_issues テーブル再構築(embedding → l1_embedding)
4439
+ // ========================================================
4440
+ db.exec(`ALTER TABLE user_issues RENAME TO user_issues_old`);
4441
+ db.exec(`
4442
+ CREATE TABLE user_issues (
4443
+ id TEXT PRIMARY KEY,
4444
+ title TEXT NOT NULL,
4445
+ content TEXT NOT NULL,
4446
+ entries TEXT NOT NULL DEFAULT '[]',
4447
+ priority TEXT NOT NULL DEFAULT 'medium'
4448
+ CHECK(priority IN ('low','medium','high','critical')),
4449
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
4450
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
4451
+ tags TEXT NOT NULL DEFAULT '[]',
4452
+ scope TEXT NOT NULL DEFAULT 'global',
4453
+ search_summary TEXT,
4454
+ user_input TEXT,
4455
+ status TEXT NOT NULL DEFAULT 'open'
4456
+ CHECK(status IN ('open','closed','archived')),
4457
+ l1_embedding BLOB,
4458
+ client_name TEXT,
4459
+ client_version TEXT,
4460
+ source_tool TEXT,
4461
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4462
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4463
+ )
4464
+ `);
4465
+ db.exec(`
4466
+ INSERT INTO user_issues (
4467
+ id, title, content, entries, priority, usage_policy, tags, scope,
4468
+ search_summary, user_input, status, l1_embedding,
4469
+ client_name, client_version, source_tool,
4470
+ created_at, updated_at
4471
+ )
4472
+ SELECT
4473
+ id, title, content, entries, priority, usage_policy, tags, scope,
4474
+ search_summary, user_input, status, embedding,
4475
+ client_name, client_version, source_tool,
4476
+ created_at, updated_at
4477
+ FROM user_issues_old
4478
+ `);
4479
+ db.exec(`DROP TABLE user_issues_old`);
4480
+ db.exec(`CREATE INDEX idx_user_issues_status ON user_issues(status)`);
4481
+ db.exec(`CREATE INDEX idx_user_issues_scope ON user_issues(scope)`);
4482
+ db.exec(`CREATE INDEX idx_user_issues_priority ON user_issues(priority)`);
4483
+ db.exec(`CREATE INDEX idx_user_issues_updated ON user_issues(updated_at)`);
4484
+ // ========================================================
4485
+ // 1-J: unified_search_items テーブル再構築(embedding → l1_embedding)
4486
+ // ========================================================
4487
+ // 既存トリガーをすべて削除(テーブル再構築のため)
4488
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_insert`);
4489
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_update`);
4490
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
4491
+ for (const table of ["claims", "episodes", "decisions", "theories", "insights", "models", "user_memos", "user_plans", "user_issues"]) {
4492
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_insert`);
4493
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_update`);
4494
+ db.exec(`DROP TRIGGER IF EXISTS ${table}_unified_delete`);
4495
+ }
4496
+ db.exec(`
4497
+ CREATE TABLE unified_search_items_new (
4498
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('claim','episode','decision','theory','insight','model','user_memo','user_plan','user_issue')),
4499
+ entity_id TEXT NOT NULL,
4500
+ scope TEXT NOT NULL DEFAULT 'global',
4501
+ category TEXT,
4502
+ title_summary TEXT NOT NULL,
4503
+ search_text TEXT NOT NULL,
4504
+ search_summary TEXT,
4505
+ tags TEXT NOT NULL DEFAULT '[]',
4506
+ l1_embedding BLOB,
4507
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4508
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
4509
+ PRIMARY KEY (entity_type, entity_id)
4510
+ )
4511
+ `);
4512
+ db.exec(`
4513
+ 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)
4514
+ SELECT entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, embedding, created_at, updated_at
4515
+ FROM unified_search_items
4516
+ `);
4517
+ db.exec(`DROP TABLE unified_search_items`);
4518
+ db.exec(`ALTER TABLE unified_search_items_new RENAME TO unified_search_items`);
4519
+ db.exec(`CREATE INDEX idx_unified_search_scope ON unified_search_items(scope)`);
4520
+ db.exec(`CREATE INDEX idx_unified_search_entity_type ON unified_search_items(entity_type)`);
4521
+ db.exec(`CREATE INDEX idx_unified_search_updated ON unified_search_items(updated_at)`);
4522
+ db.exec(`CREATE INDEX idx_unified_search_entity_id ON unified_search_items(entity_id)`);
4523
+ // unified_search_fts 再作成
4524
+ db.exec(`DROP TABLE IF EXISTS unified_search_fts`);
4525
+ db.exec(`
4526
+ CREATE VIRTUAL TABLE unified_search_fts USING fts5(
4527
+ title_summary, search_text, search_summary,
4528
+ content='unified_search_items',
4529
+ content_rowid='rowid',
4530
+ tokenize='trigram'
4531
+ )
4532
+ `);
4533
+ // FTS同期トリガー再作成
4534
+ db.exec(`
4535
+ CREATE TRIGGER unified_search_fts_insert AFTER INSERT ON unified_search_items BEGIN
4536
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
4537
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
4538
+ END
4539
+ `);
4540
+ db.exec(`
4541
+ CREATE TRIGGER unified_search_fts_update AFTER UPDATE ON unified_search_items BEGIN
4542
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
4543
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
4544
+ INSERT INTO unified_search_fts(rowid, title_summary, search_text, search_summary)
4545
+ VALUES (new.rowid, new.title_summary, new.search_text, new.search_summary);
4546
+ END
4547
+ `);
4548
+ db.exec(`
4549
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
4550
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
4551
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
4552
+ END
4553
+ `);
4554
+ // ========================================================
4555
+ // 1-K: claims_fts 再構築(l1_content カラム追加)
4556
+ // ========================================================
4557
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_insert`);
4558
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_update`);
4559
+ db.exec(`DROP TRIGGER IF EXISTS claims_fts_delete`);
4560
+ db.exec(`DROP TABLE IF EXISTS claims_fts`);
4561
+ db.exec(`
4562
+ CREATE VIRTUAL TABLE claims_fts USING fts5(
4563
+ subject, predicate, object, evidence, l1_content, search_summary,
4564
+ content='claims',
4565
+ content_rowid='rowid',
4566
+ tokenize='trigram'
4567
+ )
4568
+ `);
4569
+ db.exec(`
4570
+ CREATE TRIGGER claims_fts_insert AFTER INSERT ON claims BEGIN
4571
+ INSERT INTO claims_fts(rowid, subject, predicate, object, evidence, l1_content, search_summary)
4572
+ VALUES (new.rowid, new.subject, new.predicate, new.object, new.evidence, new.l1_content, new.search_summary);
4573
+ END
4574
+ `);
4575
+ db.exec(`
4576
+ CREATE TRIGGER claims_fts_update AFTER UPDATE ON claims BEGIN
4577
+ INSERT INTO claims_fts(claims_fts, rowid, subject, predicate, object, evidence, l1_content, search_summary)
4578
+ VALUES ('delete', old.rowid, old.subject, old.predicate, old.object, old.evidence, old.l1_content, old.search_summary);
4579
+ INSERT INTO claims_fts(rowid, subject, predicate, object, evidence, l1_content, search_summary)
4580
+ VALUES (new.rowid, new.subject, new.predicate, new.object, new.evidence, new.l1_content, new.search_summary);
4581
+ END
4582
+ `);
4583
+ db.exec(`
4584
+ CREATE TRIGGER claims_fts_delete AFTER DELETE ON claims BEGIN
4585
+ INSERT INTO claims_fts(claims_fts, rowid, subject, predicate, object, evidence, l1_content, search_summary)
4586
+ VALUES ('delete', old.rowid, old.subject, old.predicate, old.object, old.evidence, old.l1_content, old.search_summary);
4587
+ END
4588
+ `);
4589
+ // ========================================================
4590
+ // 1-L: 他エンティティの FTS 再構築(テーブル再構築でトリガーが消えたため)
4591
+ // ========================================================
4592
+ // --- theories_fts ---
4593
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_insert`);
4594
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_update`);
4595
+ db.exec(`DROP TRIGGER IF EXISTS theories_fts_delete`);
4596
+ db.exec(`DROP TABLE IF EXISTS theories_fts`);
4597
+ db.exec(`
4598
+ CREATE VIRTUAL TABLE theories_fts USING fts5(
4599
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
4600
+ content='theories', content_rowid='rowid', tokenize='trigram'
4601
+ )
4602
+ `);
4603
+ db.exec(`
4604
+ CREATE TRIGGER theories_fts_insert AFTER INSERT ON theories BEGIN
4605
+ INSERT INTO theories_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4606
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
4607
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
4608
+ END
4609
+ `);
4610
+ db.exec(`
4611
+ CREATE TRIGGER theories_fts_update AFTER UPDATE ON theories BEGIN
4612
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4613
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
4614
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
4615
+ INSERT INTO theories_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4616
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
4617
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
4618
+ END
4619
+ `);
4620
+ db.exec(`
4621
+ CREATE TRIGGER theories_fts_delete AFTER DELETE ON theories BEGIN
4622
+ INSERT INTO theories_fts(theories_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4623
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
4624
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
4625
+ END
4626
+ `);
4627
+ // --- models_fts ---
4628
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_insert`);
4629
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_update`);
4630
+ db.exec(`DROP TRIGGER IF EXISTS models_fts_delete`);
4631
+ db.exec(`DROP TABLE IF EXISTS models_fts`);
4632
+ db.exec(`
4633
+ CREATE VIRTUAL TABLE models_fts USING fts5(
4634
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
4635
+ content='models', content_rowid='rowid', tokenize='trigram'
4636
+ )
4637
+ `);
4638
+ db.exec(`
4639
+ CREATE TRIGGER models_fts_insert AFTER INSERT ON models BEGIN
4640
+ INSERT INTO models_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4641
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
4642
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
4643
+ END
4644
+ `);
4645
+ db.exec(`
4646
+ CREATE TRIGGER models_fts_update AFTER UPDATE ON models BEGIN
4647
+ INSERT INTO models_fts(models_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4648
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
4649
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
4650
+ INSERT INTO models_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4651
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
4652
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
4653
+ END
4654
+ `);
4655
+ db.exec(`
4656
+ CREATE TRIGGER models_fts_delete AFTER DELETE ON models BEGIN
4657
+ INSERT INTO models_fts(models_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4658
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
4659
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
4660
+ END
4661
+ `);
4662
+ // --- insights_fts ---
4663
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_insert`);
4664
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_update`);
4665
+ db.exec(`DROP TRIGGER IF EXISTS insights_fts_delete`);
4666
+ db.exec(`DROP TABLE IF EXISTS insights_fts`);
4667
+ db.exec(`
4668
+ CREATE VIRTUAL TABLE insights_fts USING fts5(
4669
+ title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary,
4670
+ content='insights', content_rowid='rowid', tokenize='trigram'
4671
+ )
4672
+ `);
4673
+ db.exec(`
4674
+ CREATE TRIGGER insights_fts_insert AFTER INSERT ON insights BEGIN
4675
+ INSERT INTO insights_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4676
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
4677
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
4678
+ END
4679
+ `);
4680
+ db.exec(`
4681
+ CREATE TRIGGER insights_fts_update AFTER UPDATE ON insights BEGIN
4682
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4683
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
4684
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
4685
+ INSERT INTO insights_fts(rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4686
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_core_thesis,
4687
+ ${jc("new.l2_principles")}, ${jc("new.evidence_refs")}, ${jc("new.tags")}, new.search_summary);
4688
+ END
4689
+ `);
4690
+ db.exec(`
4691
+ CREATE TRIGGER insights_fts_delete AFTER DELETE ON insights BEGIN
4692
+ INSERT INTO insights_fts(insights_fts, rowid, title, description, l1_content, l2_core_thesis, l2_principles, evidence_refs, tags, search_summary)
4693
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_core_thesis,
4694
+ ${jc("old.l2_principles")}, ${jc("old.evidence_refs")}, ${jc("old.tags")}, old.search_summary);
4695
+ END
4696
+ `);
4697
+ // --- episodes_fts ---
4698
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_insert`);
4699
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_update`);
4700
+ db.exec(`DROP TRIGGER IF EXISTS episodes_fts_delete`);
4701
+ db.exec(`DROP TABLE IF EXISTS episodes_fts`);
4702
+ db.exec(`
4703
+ CREATE VIRTUAL TABLE episodes_fts USING fts5(
4704
+ title, l1_content, l2_context, l2_trigger, l2_principles, l2_problems, l2_desires, l2_decisions, l2_outcomes, search_summary, user_input,
4705
+ content='episodes', content_rowid='rowid', tokenize='trigram'
4706
+ )
4707
+ `);
4708
+ db.exec(`
4709
+ CREATE TRIGGER episodes_fts_insert AFTER INSERT ON episodes BEGIN
4710
+ 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)
4711
+ VALUES (new.rowid, new.title, new.l1_content, new.l2_context, new.l2_trigger,
4712
+ ${jc("new.l2_principles")}, ${jc("new.l2_problems")}, ${jc("new.l2_desires")},
4713
+ ${jc("new.l2_decisions")}, ${jc("new.l2_outcomes")},
4714
+ new.search_summary, new.user_input);
4715
+ END
4716
+ `);
4717
+ db.exec(`
4718
+ CREATE TRIGGER episodes_fts_update AFTER UPDATE ON episodes BEGIN
4719
+ 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)
4720
+ VALUES ('delete', old.rowid, old.title, old.l1_content, old.l2_context, old.l2_trigger,
4721
+ ${jc("old.l2_principles")}, ${jc("old.l2_problems")}, ${jc("old.l2_desires")},
4722
+ ${jc("old.l2_decisions")}, ${jc("old.l2_outcomes")},
4723
+ old.search_summary, old.user_input);
4724
+ 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)
4725
+ VALUES (new.rowid, new.title, new.l1_content, new.l2_context, new.l2_trigger,
4726
+ ${jc("new.l2_principles")}, ${jc("new.l2_problems")}, ${jc("new.l2_desires")},
4727
+ ${jc("new.l2_decisions")}, ${jc("new.l2_outcomes")},
4728
+ new.search_summary, new.user_input);
4729
+ END
4730
+ `);
4731
+ db.exec(`
4732
+ CREATE TRIGGER episodes_fts_delete AFTER DELETE ON episodes BEGIN
4733
+ 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)
4734
+ VALUES ('delete', old.rowid, old.title, old.l1_content, old.l2_context, old.l2_trigger,
4735
+ ${jc("old.l2_principles")}, ${jc("old.l2_problems")}, ${jc("old.l2_desires")},
4736
+ ${jc("old.l2_decisions")}, ${jc("old.l2_outcomes")},
4737
+ old.search_summary, old.user_input);
4738
+ END
4739
+ `);
4740
+ // --- decisions_fts ---
4741
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_insert`);
4742
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_update`);
4743
+ db.exec(`DROP TRIGGER IF EXISTS decisions_fts_delete`);
4744
+ db.exec(`DROP TABLE IF EXISTS decisions_fts`);
4745
+ db.exec(`
4746
+ CREATE VIRTUAL TABLE decisions_fts USING fts5(
4747
+ title, description, l1_content, l2_reasoning, search_summary, user_input,
4748
+ content='decisions', content_rowid='rowid', tokenize='trigram'
4749
+ )
4750
+ `);
4751
+ db.exec(`
4752
+ CREATE TRIGGER decisions_fts_insert AFTER INSERT ON decisions BEGIN
4753
+ INSERT INTO decisions_fts(rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
4754
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_reasoning, new.search_summary, new.user_input);
4755
+ END
4756
+ `);
4757
+ db.exec(`
4758
+ CREATE TRIGGER decisions_fts_update AFTER UPDATE ON decisions BEGIN
4759
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
4760
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_reasoning, old.search_summary, old.user_input);
4761
+ INSERT INTO decisions_fts(rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
4762
+ VALUES (new.rowid, new.title, new.description, new.l1_content, new.l2_reasoning, new.search_summary, new.user_input);
4763
+ END
4764
+ `);
4765
+ db.exec(`
4766
+ CREATE TRIGGER decisions_fts_delete AFTER DELETE ON decisions BEGIN
4767
+ INSERT INTO decisions_fts(decisions_fts, rowid, title, description, l1_content, l2_reasoning, search_summary, user_input)
4768
+ VALUES ('delete', old.rowid, old.title, old.description, old.l1_content, old.l2_reasoning, old.search_summary, old.user_input);
4769
+ END
4770
+ `);
4771
+ // --- user_memos_fts ---
4772
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_fts_insert`);
4773
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_fts_update`);
4774
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_fts_delete`);
4775
+ db.exec(`DROP TABLE IF EXISTS user_memos_fts`);
4776
+ db.exec(`
4777
+ CREATE VIRTUAL TABLE user_memos_fts USING fts5(
4778
+ title, content, tags, search_summary,
4779
+ content='user_memos', content_rowid='rowid', tokenize='trigram'
4780
+ )
4781
+ `);
4782
+ db.exec(`
4783
+ CREATE TRIGGER user_memos_fts_insert AFTER INSERT ON user_memos BEGIN
4784
+ INSERT INTO user_memos_fts(rowid, title, content, tags, search_summary)
4785
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
4786
+ END
4787
+ `);
4788
+ db.exec(`
4789
+ CREATE TRIGGER user_memos_fts_update AFTER UPDATE ON user_memos BEGIN
4790
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, content, tags, search_summary)
4791
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
4792
+ INSERT INTO user_memos_fts(rowid, title, content, tags, search_summary)
4793
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
4794
+ END
4795
+ `);
4796
+ db.exec(`
4797
+ CREATE TRIGGER user_memos_fts_delete AFTER DELETE ON user_memos BEGIN
4798
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, content, tags, search_summary)
4799
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
4800
+ END
4801
+ `);
4802
+ // --- user_plans_fts ---
4803
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_fts_insert`);
4804
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_fts_update`);
4805
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_fts_delete`);
4806
+ db.exec(`DROP TABLE IF EXISTS user_plans_fts`);
4807
+ db.exec(`
4808
+ CREATE VIRTUAL TABLE user_plans_fts USING fts5(
4809
+ title, content, tags, search_summary,
4810
+ content='user_plans', content_rowid='rowid', tokenize='trigram'
4811
+ )
4812
+ `);
4813
+ db.exec(`
4814
+ CREATE TRIGGER user_plans_fts_insert AFTER INSERT ON user_plans BEGIN
4815
+ INSERT INTO user_plans_fts(rowid, title, content, tags, search_summary)
4816
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
4817
+ END
4818
+ `);
4819
+ db.exec(`
4820
+ CREATE TRIGGER user_plans_fts_update AFTER UPDATE ON user_plans BEGIN
4821
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, content, tags, search_summary)
4822
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
4823
+ INSERT INTO user_plans_fts(rowid, title, content, tags, search_summary)
4824
+ VALUES (new.rowid, new.title, new.content, ${jc("new.tags")}, new.search_summary);
4825
+ END
4826
+ `);
4827
+ db.exec(`
4828
+ CREATE TRIGGER user_plans_fts_delete AFTER DELETE ON user_plans BEGIN
4829
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, content, tags, search_summary)
4830
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.tags")}, old.search_summary);
4831
+ END
4832
+ `);
4833
+ // --- user_issues_fts ---
4834
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_fts_insert`);
4835
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_fts_update`);
4836
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_fts_delete`);
4837
+ db.exec(`DROP TABLE IF EXISTS user_issues_fts`);
4838
+ db.exec(`
4839
+ CREATE VIRTUAL TABLE user_issues_fts USING fts5(
4840
+ title, content, entries, tags, search_summary,
4841
+ content='user_issues', content_rowid='rowid', tokenize='trigram'
4842
+ )
4843
+ `);
4844
+ db.exec(`
4845
+ CREATE TRIGGER user_issues_fts_insert AFTER INSERT ON user_issues BEGIN
4846
+ INSERT INTO user_issues_fts(rowid, title, content, entries, tags, search_summary)
4847
+ VALUES (new.rowid, new.title, new.content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
4848
+ END
4849
+ `);
4850
+ db.exec(`
4851
+ CREATE TRIGGER user_issues_fts_update AFTER UPDATE ON user_issues BEGIN
4852
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, content, entries, tags, search_summary)
4853
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
4854
+ INSERT INTO user_issues_fts(rowid, title, content, entries, tags, search_summary)
4855
+ VALUES (new.rowid, new.title, new.content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
4856
+ END
4857
+ `);
4858
+ db.exec(`
4859
+ CREATE TRIGGER user_issues_fts_delete AFTER DELETE ON user_issues BEGIN
4860
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, content, entries, tags, search_summary)
4861
+ VALUES ('delete', old.rowid, old.title, old.content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
4862
+ END
4863
+ `);
4864
+ // 2026-03-03 修正 (Issue #64): claims の search に l1_content を追加
4865
+ const v23Entities = [
4866
+ {
4867
+ type: "claim", table: "claims",
4868
+ category: (p) => `${p}category`,
4869
+ title: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object`,
4870
+ search: (p) => `${p}subject || ' ' || ${p}predicate || ' ' || ${p}object || ' ' || ${coal(`${p}evidence`)} || ' ' || ${coal(`${p}falsifier`)} || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}search_summary`)}`,
4871
+ tags: () => `'[]'`,
4872
+ },
4873
+ {
4874
+ type: "episode", table: "episodes",
4875
+ category: () => `NULL`,
4876
+ title: (p) => `${p}title`,
4877
+ search: (p) => `${p}title || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${coal(`${p}l2_context`)} || ' ' || ${coal(`${p}l2_trigger`)} || ' ' || ${jc(`${p}l2_problems`)} || ' ' || ${jc(`${p}l2_desires`)} || ' ' || ${jc(`${p}l2_decisions`)} || ' ' || ${jc(`${p}l2_outcomes`)} || ' ' || ${jc(`${p}l2_principles`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`,
4878
+ tags: (p) => `${p}tags`,
4879
+ },
4880
+ {
4881
+ type: "decision", table: "decisions",
4882
+ category: () => `NULL`,
4883
+ title: (p) => `${p}title`,
4884
+ search: (p) => `${p}title || ' ' || ${p}description || ' ' || ${coal(`${p}l1_content`)} || ' ' || ${p}l2_reasoning || ' ' || ${jc(`${p}l2_alternatives`)} || ' ' || ${coal(`${p}search_summary`)} || ' ' || ${coal(`${p}user_input`)}`,
4885
+ tags: () => `'[]'`,
4886
+ },
4887
+ {
4888
+ type: "theory", table: "theories",
4889
+ category: () => `NULL`,
4890
+ title: (p) => `${p}title`,
4891
+ search: (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}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
4892
+ tags: (p) => `${p}tags`,
4893
+ },
4894
+ {
4895
+ type: "insight", table: "insights",
4896
+ category: () => `NULL`,
4897
+ title: (p) => `${p}title`,
4898
+ search: (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`)}`,
4899
+ tags: (p) => `${p}tags`,
4900
+ },
4901
+ {
4902
+ type: "model", table: "models",
4903
+ category: () => `NULL`,
4904
+ title: (p) => `${p}title`,
4905
+ search: (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}non_goals`)} || ' ' || ${jc(`${p}open_questions`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
4906
+ tags: (p) => `${p}tags`,
4907
+ },
4908
+ {
4909
+ type: "user_memo", table: "user_memos",
4910
+ category: () => `NULL`,
4911
+ title: (p) => `${p}title`,
4912
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
4913
+ tags: (p) => `${p}tags`,
4914
+ statusCondition: "AND new.usage_policy != 'human_directed'",
4915
+ },
4916
+ {
4917
+ type: "user_plan", table: "user_plans",
4918
+ category: () => `NULL`,
4919
+ title: (p) => `${p}title`,
4920
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
4921
+ tags: (p) => `${p}tags`,
4922
+ statusCondition: "AND new.usage_policy != 'human_directed'",
4923
+ },
4924
+ {
4925
+ type: "user_issue", table: "user_issues",
4926
+ category: () => `NULL`,
4927
+ title: (p) => `${p}title`,
4928
+ search: (p) => `${p}title || ' ' || ${p}content || ' ' || ${jc(`${p}entries`)} || ' ' || ${jc(`${p}tags`)} || ' ' || ${coal(`${p}search_summary`)}`,
4929
+ tags: (p) => `${p}tags`,
4930
+ statusField: "open",
4931
+ },
4932
+ ];
4933
+ for (const e of v23Entities) {
4934
+ const activeStatus = e.statusField ?? "active";
4935
+ const insertWhen = e.statusCondition
4936
+ ? `WHEN new.status = '${activeStatus}' ${e.statusCondition}`
4937
+ : `WHEN new.status = '${activeStatus}'`;
4938
+ const updateWhere = e.statusCondition
4939
+ ? `WHERE new.status = '${activeStatus}' ${e.statusCondition}`
4940
+ : `WHERE new.status = '${activeStatus}'`;
4941
+ db.exec(`
4942
+ CREATE TRIGGER ${e.table}_unified_insert AFTER INSERT ON ${e.table}
4943
+ ${insertWhen} BEGIN
4944
+ INSERT OR REPLACE INTO unified_search_items(${columns})
4945
+ VALUES ('${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at);
4946
+ END
4947
+ `);
4948
+ db.exec(`
4949
+ CREATE TRIGGER ${e.table}_unified_update AFTER UPDATE ON ${e.table} BEGIN
4950
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
4951
+ INSERT INTO unified_search_items(${columns})
4952
+ SELECT '${e.type}', new.id, new.scope, ${e.category("new.")}, ${e.title("new.")}, ${e.search("new.")}, new.search_summary, ${e.tags("new.")}, new.created_at, new.updated_at
4953
+ ${updateWhere};
4954
+ END
4955
+ `);
4956
+ db.exec(`
4957
+ CREATE TRIGGER ${e.table}_unified_delete AFTER DELETE ON ${e.table} BEGIN
4958
+ DELETE FROM unified_search_items WHERE entity_type = '${e.type}' AND entity_id = old.id;
4959
+ END
4960
+ `);
4961
+ }
4962
+ // ========================================================
4963
+ // 1-N: FTS リビルド + unified_search_items 再挿入 + バージョン記録
4964
+ // ========================================================
4965
+ db.exec(`INSERT INTO claims_fts(claims_fts) VALUES('rebuild')`);
4966
+ db.exec(`INSERT INTO theories_fts(theories_fts) VALUES('rebuild')`);
4967
+ db.exec(`INSERT INTO models_fts(models_fts) VALUES('rebuild')`);
4968
+ db.exec(`INSERT INTO insights_fts(insights_fts) VALUES('rebuild')`);
4969
+ db.exec(`INSERT INTO episodes_fts(episodes_fts) VALUES('rebuild')`);
4970
+ db.exec(`INSERT INTO decisions_fts(decisions_fts) VALUES('rebuild')`);
4971
+ db.exec(`INSERT INTO user_memos_fts(user_memos_fts) VALUES('rebuild')`);
4972
+ db.exec(`INSERT INTO user_plans_fts(user_plans_fts) VALUES('rebuild')`);
4973
+ db.exec(`INSERT INTO user_issues_fts(user_issues_fts) VALUES('rebuild')`);
4974
+ // unified_search_items の全エンティティを再構築(claims の search_text に l1_content が追加されたため)
4975
+ // DELETE 時に空の FTS への 'delete' 発行で malformed エラーになるため、トリガーを一時的に外す
4976
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
4977
+ db.exec(`DELETE FROM unified_search_items`);
4978
+ for (const e of v23Entities) {
4979
+ const activeStatus = e.statusField ?? "active";
4980
+ const usagePolicyFilter = e.statusCondition ? " AND usage_policy != 'human_directed'" : "";
4981
+ db.exec(`
4982
+ INSERT OR REPLACE INTO unified_search_items(${columns})
4983
+ SELECT '${e.type}', id, scope, ${e.category("")}, ${e.title("")}, ${e.search("")}, search_summary, ${e.tags("")}, created_at, updated_at
4984
+ FROM ${e.table}
4985
+ WHERE status = '${activeStatus}'${usagePolicyFilter}
4986
+ `);
4987
+ }
4988
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
4989
+ // DELETE 前に外した FTS delete トリガーを再作成
4990
+ db.exec(`
4991
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
4992
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
4993
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
4994
+ END
4995
+ `);
4996
+ // --- 外部キー整合性チェック ---
4997
+ const fkViolations = db.pragma("foreign_key_check");
4998
+ if (fkViolations.length > 0) {
4999
+ throw new Error(`V23マイグレーション後に外部キー違反が検出されました: ${JSON.stringify(fkViolations)}`);
5000
+ }
5001
+ // --- スキーマバージョン記録 ---
5002
+ db.exec(`INSERT INTO schema_version (version) VALUES (23)`);
5003
+ });
5004
+ transaction();
5005
+ }
5006
+ finally {
5007
+ // legacy_alter_table を元に戻す(V23 で ON にしたため)
5008
+ db.pragma("legacy_alter_table = OFF");
5009
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
5010
+ db.pragma("foreign_keys = ON");
5011
+ }
5012
+ }
5013
+ }
5014
+ /**
5015
+ * V24: user_memo/plan/issue の content → l1_content リネーム + user_memos に user_input 追加 (Issue #54)
5016
+ */
5017
+ function applyV24(db) {
5018
+ const fkState = db.pragma("foreign_keys");
5019
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
5020
+ db.pragma("foreign_keys = OFF");
5021
+ }
5022
+ db.pragma("legacy_alter_table = ON");
5023
+ try {
5024
+ const transaction = db.transaction(() => {
5025
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
5026
+ const coal = (col) => `COALESCE(${col}, '')`;
5027
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
5028
+ // ========================================================
5029
+ // 1. user_memos: content → l1_content, user_input 追加
5030
+ // ========================================================
5031
+ db.exec(`ALTER TABLE user_memos RENAME TO user_memos_old`);
5032
+ db.exec(`
5033
+ CREATE TABLE user_memos (
5034
+ id TEXT PRIMARY KEY,
5035
+ title TEXT NOT NULL,
5036
+ l1_content TEXT NOT NULL,
5037
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
5038
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
5039
+ tags TEXT NOT NULL DEFAULT '[]',
5040
+ scope TEXT NOT NULL DEFAULT 'global',
5041
+ search_summary TEXT,
5042
+ user_input TEXT,
5043
+ status TEXT NOT NULL DEFAULT 'active'
5044
+ CHECK(status IN ('active','archived')),
5045
+ l1_embedding BLOB,
5046
+ client_name TEXT,
5047
+ client_version TEXT,
5048
+ source_tool TEXT,
5049
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5050
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5051
+ )
5052
+ `);
5053
+ db.exec(`
5054
+ INSERT INTO user_memos (
5055
+ id, title, l1_content, usage_policy, tags, scope, search_summary, user_input,
5056
+ status, l1_embedding, client_name, client_version, source_tool,
5057
+ created_at, updated_at
5058
+ )
5059
+ SELECT
5060
+ id, title, content, usage_policy, tags, scope, search_summary, NULL,
5061
+ status, l1_embedding, client_name, client_version, source_tool,
5062
+ created_at, updated_at
5063
+ FROM user_memos_old
5064
+ `);
5065
+ db.exec(`DROP TABLE user_memos_old`);
5066
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_memos_status ON user_memos(status)`);
5067
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_memos_scope ON user_memos(scope)`);
5068
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_memos_usage_policy ON user_memos(usage_policy)`);
5069
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_memos_updated ON user_memos(updated_at)`);
5070
+ // ========================================================
5071
+ // 2. user_plans: content → l1_content
5072
+ // ========================================================
5073
+ db.exec(`ALTER TABLE user_plans RENAME TO user_plans_old`);
5074
+ db.exec(`
5075
+ CREATE TABLE user_plans (
5076
+ id TEXT PRIMARY KEY,
5077
+ title TEXT NOT NULL,
5078
+ l1_content TEXT NOT NULL,
5079
+ usage_policy TEXT NOT NULL DEFAULT 'auto'
5080
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
5081
+ tags TEXT NOT NULL DEFAULT '[]',
5082
+ scope TEXT NOT NULL DEFAULT 'global',
5083
+ search_summary TEXT,
5084
+ user_input TEXT,
5085
+ status TEXT NOT NULL DEFAULT 'active'
5086
+ CHECK(status IN ('active','completed','archived')),
5087
+ l1_embedding BLOB,
5088
+ client_name TEXT,
5089
+ client_version TEXT,
5090
+ source_tool TEXT,
5091
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5092
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5093
+ )
5094
+ `);
5095
+ db.exec(`
5096
+ INSERT INTO user_plans (
5097
+ id, title, l1_content, usage_policy, tags, scope, search_summary, user_input,
5098
+ status, l1_embedding, client_name, client_version, source_tool,
5099
+ created_at, updated_at
5100
+ )
5101
+ SELECT
5102
+ id, title, content, usage_policy, tags, scope, search_summary, user_input,
5103
+ status, l1_embedding, client_name, client_version, source_tool,
5104
+ created_at, updated_at
5105
+ FROM user_plans_old
5106
+ `);
5107
+ db.exec(`DROP TABLE user_plans_old`);
5108
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_status ON user_plans(status)`);
5109
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_scope ON user_plans(scope)`);
5110
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_usage_policy ON user_plans(usage_policy)`);
5111
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_plans_updated ON user_plans(updated_at)`);
5112
+ // ========================================================
5113
+ // 3. user_issues: content → l1_content
5114
+ // ========================================================
5115
+ db.exec(`ALTER TABLE user_issues RENAME TO user_issues_old`);
5116
+ db.exec(`
5117
+ CREATE TABLE user_issues (
5118
+ id TEXT PRIMARY KEY,
5119
+ title TEXT NOT NULL,
5120
+ l1_content TEXT NOT NULL,
5121
+ entries TEXT NOT NULL DEFAULT '[]',
5122
+ priority TEXT NOT NULL DEFAULT 'medium'
5123
+ CHECK(priority IN ('low','medium','high','critical')),
5124
+ usage_policy TEXT NOT NULL DEFAULT 'on_request'
5125
+ CHECK(usage_policy IN ('auto','on_request','human_directed')),
5126
+ tags TEXT NOT NULL DEFAULT '[]',
5127
+ scope TEXT NOT NULL DEFAULT 'global',
5128
+ search_summary TEXT,
5129
+ user_input TEXT,
5130
+ status TEXT NOT NULL DEFAULT 'open'
5131
+ CHECK(status IN ('open','closed','archived')),
5132
+ l1_embedding BLOB,
5133
+ client_name TEXT,
5134
+ client_version TEXT,
5135
+ source_tool TEXT,
5136
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5137
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5138
+ )
5139
+ `);
5140
+ db.exec(`
5141
+ INSERT INTO user_issues (
5142
+ id, title, l1_content, entries, priority, usage_policy, tags, scope, search_summary, user_input,
5143
+ status, l1_embedding, client_name, client_version, source_tool,
5144
+ created_at, updated_at
5145
+ )
5146
+ SELECT
5147
+ id, title, content, entries, priority, usage_policy, tags, scope, search_summary, user_input,
5148
+ status, l1_embedding, client_name, client_version, source_tool,
5149
+ created_at, updated_at
5150
+ FROM user_issues_old
5151
+ `);
5152
+ db.exec(`DROP TABLE user_issues_old`);
5153
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_status ON user_issues(status)`);
5154
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_scope ON user_issues(scope)`);
5155
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_priority ON user_issues(priority)`);
5156
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_user_issues_updated ON user_issues(updated_at)`);
5157
+ // ========================================================
5158
+ // 4. FTS 再構築(content → l1_content, user_memos に user_input 追加)
5159
+ // ========================================================
5160
+ // --- user_memos_fts ---
5161
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_fts_insert`);
5162
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_fts_update`);
5163
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_fts_delete`);
5164
+ db.exec(`DROP TABLE IF EXISTS user_memos_fts`);
5165
+ db.exec(`
5166
+ CREATE VIRTUAL TABLE user_memos_fts USING fts5(
5167
+ title, l1_content, tags, search_summary, user_input,
5168
+ content='user_memos', content_rowid='rowid', tokenize='trigram'
5169
+ )
5170
+ `);
5171
+ db.exec(`
5172
+ CREATE TRIGGER user_memos_fts_insert AFTER INSERT ON user_memos BEGIN
5173
+ INSERT INTO user_memos_fts(rowid, title, l1_content, tags, search_summary, user_input)
5174
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary, new.user_input);
5175
+ END
5176
+ `);
5177
+ db.exec(`
5178
+ CREATE TRIGGER user_memos_fts_update AFTER UPDATE ON user_memos BEGIN
5179
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, l1_content, tags, search_summary, user_input)
5180
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary, old.user_input);
5181
+ INSERT INTO user_memos_fts(rowid, title, l1_content, tags, search_summary, user_input)
5182
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary, new.user_input);
5183
+ END
5184
+ `);
5185
+ db.exec(`
5186
+ CREATE TRIGGER user_memos_fts_delete AFTER DELETE ON user_memos BEGIN
5187
+ INSERT INTO user_memos_fts(user_memos_fts, rowid, title, l1_content, tags, search_summary, user_input)
5188
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary, old.user_input);
5189
+ END
5190
+ `);
5191
+ // --- user_plans_fts ---
5192
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_fts_insert`);
5193
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_fts_update`);
5194
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_fts_delete`);
5195
+ db.exec(`DROP TABLE IF EXISTS user_plans_fts`);
5196
+ db.exec(`
5197
+ CREATE VIRTUAL TABLE user_plans_fts USING fts5(
5198
+ title, l1_content, tags, search_summary,
5199
+ content='user_plans', content_rowid='rowid', tokenize='trigram'
5200
+ )
5201
+ `);
5202
+ db.exec(`
5203
+ CREATE TRIGGER user_plans_fts_insert AFTER INSERT ON user_plans BEGIN
5204
+ INSERT INTO user_plans_fts(rowid, title, l1_content, tags, search_summary)
5205
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary);
5206
+ END
5207
+ `);
5208
+ db.exec(`
5209
+ CREATE TRIGGER user_plans_fts_update AFTER UPDATE ON user_plans BEGIN
5210
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, l1_content, tags, search_summary)
5211
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary);
5212
+ INSERT INTO user_plans_fts(rowid, title, l1_content, tags, search_summary)
5213
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.tags")}, new.search_summary);
5214
+ END
5215
+ `);
5216
+ db.exec(`
5217
+ CREATE TRIGGER user_plans_fts_delete AFTER DELETE ON user_plans BEGIN
5218
+ INSERT INTO user_plans_fts(user_plans_fts, rowid, title, l1_content, tags, search_summary)
5219
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.tags")}, old.search_summary);
5220
+ END
5221
+ `);
5222
+ // --- user_issues_fts ---
5223
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_fts_insert`);
5224
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_fts_update`);
5225
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_fts_delete`);
5226
+ db.exec(`DROP TABLE IF EXISTS user_issues_fts`);
5227
+ db.exec(`
5228
+ CREATE VIRTUAL TABLE user_issues_fts USING fts5(
5229
+ title, l1_content, entries, tags, search_summary,
5230
+ content='user_issues', content_rowid='rowid', tokenize='trigram'
5231
+ )
5232
+ `);
5233
+ db.exec(`
5234
+ CREATE TRIGGER user_issues_fts_insert AFTER INSERT ON user_issues BEGIN
5235
+ INSERT INTO user_issues_fts(rowid, title, l1_content, entries, tags, search_summary)
5236
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
5237
+ END
5238
+ `);
5239
+ db.exec(`
5240
+ CREATE TRIGGER user_issues_fts_update AFTER UPDATE ON user_issues BEGIN
5241
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, l1_content, entries, tags, search_summary)
5242
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
5243
+ INSERT INTO user_issues_fts(rowid, title, l1_content, entries, tags, search_summary)
5244
+ VALUES (new.rowid, new.title, new.l1_content, ${jc("new.entries")}, ${jc("new.tags")}, new.search_summary);
5245
+ END
5246
+ `);
5247
+ db.exec(`
5248
+ CREATE TRIGGER user_issues_fts_delete AFTER DELETE ON user_issues BEGIN
5249
+ INSERT INTO user_issues_fts(user_issues_fts, rowid, title, l1_content, entries, tags, search_summary)
5250
+ VALUES ('delete', old.rowid, old.title, old.l1_content, ${jc("old.entries")}, ${jc("old.tags")}, old.search_summary);
5251
+ END
5252
+ `);
5253
+ // FTS リビルド
5254
+ db.exec(`INSERT INTO user_memos_fts(user_memos_fts) VALUES('rebuild')`);
5255
+ db.exec(`INSERT INTO user_plans_fts(user_plans_fts) VALUES('rebuild')`);
5256
+ db.exec(`INSERT INTO user_issues_fts(user_issues_fts) VALUES('rebuild')`);
5257
+ // ========================================================
5258
+ // 5. unified_search_items トリガー再構築(user_memo/plan/issue の content → l1_content, user_memo に user_input 追加)
5259
+ // ========================================================
5260
+ // user_memos トリガー
5261
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_unified_insert`);
5262
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_unified_update`);
5263
+ db.exec(`DROP TRIGGER IF EXISTS user_memos_unified_delete`);
5264
+ db.exec(`
5265
+ CREATE TRIGGER user_memos_unified_insert AFTER INSERT ON user_memos
5266
+ WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN
5267
+ INSERT OR REPLACE INTO unified_search_items(${columns})
5268
+ 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);
5269
+ END
5270
+ `);
5271
+ db.exec(`
5272
+ CREATE TRIGGER user_memos_unified_update AFTER UPDATE ON user_memos BEGIN
5273
+ DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id;
5274
+ INSERT INTO unified_search_items(${columns})
5275
+ 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
5276
+ WHERE new.status = 'active' AND new.usage_policy != 'human_directed';
5277
+ END
5278
+ `);
5279
+ db.exec(`
5280
+ CREATE TRIGGER user_memos_unified_delete AFTER DELETE ON user_memos BEGIN
5281
+ DELETE FROM unified_search_items WHERE entity_type = 'user_memo' AND entity_id = old.id;
5282
+ END
5283
+ `);
5284
+ // user_plans トリガー
5285
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_unified_insert`);
5286
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_unified_update`);
5287
+ db.exec(`DROP TRIGGER IF EXISTS user_plans_unified_delete`);
5288
+ db.exec(`
5289
+ CREATE TRIGGER user_plans_unified_insert AFTER INSERT ON user_plans
5290
+ WHEN new.status = 'active' AND new.usage_policy != 'human_directed' BEGIN
5291
+ INSERT OR REPLACE INTO unified_search_items(${columns})
5292
+ 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);
5293
+ END
5294
+ `);
5295
+ db.exec(`
5296
+ CREATE TRIGGER user_plans_unified_update AFTER UPDATE ON user_plans BEGIN
5297
+ DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id;
5298
+ INSERT INTO unified_search_items(${columns})
5299
+ 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
5300
+ WHERE new.status = 'active' AND new.usage_policy != 'human_directed';
5301
+ END
5302
+ `);
5303
+ db.exec(`
5304
+ CREATE TRIGGER user_plans_unified_delete AFTER DELETE ON user_plans BEGIN
5305
+ DELETE FROM unified_search_items WHERE entity_type = 'user_plan' AND entity_id = old.id;
5306
+ END
5307
+ `);
5308
+ // user_issues トリガー
5309
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_unified_insert`);
5310
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_unified_update`);
5311
+ db.exec(`DROP TRIGGER IF EXISTS user_issues_unified_delete`);
5312
+ db.exec(`
5313
+ CREATE TRIGGER user_issues_unified_insert AFTER INSERT ON user_issues
5314
+ WHEN new.status = 'open' BEGIN
5315
+ INSERT OR REPLACE INTO unified_search_items(${columns})
5316
+ 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);
5317
+ END
5318
+ `);
5319
+ db.exec(`
5320
+ CREATE TRIGGER user_issues_unified_update AFTER UPDATE ON user_issues BEGIN
5321
+ DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id;
5322
+ INSERT INTO unified_search_items(${columns})
5323
+ 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
5324
+ WHERE new.status = 'open';
5325
+ END
5326
+ `);
5327
+ db.exec(`
5328
+ CREATE TRIGGER user_issues_unified_delete AFTER DELETE ON user_issues BEGIN
5329
+ DELETE FROM unified_search_items WHERE entity_type = 'user_issue' AND entity_id = old.id;
5330
+ END
5331
+ `);
5332
+ // unified_search_items を再挿入(user_memo/plan/issue の search_text 更新のため)
5333
+ // DELETE 時のFTS 'delete' malformed エラー防止: トリガーを一時的に外す
5334
+ db.exec(`DROP TRIGGER IF EXISTS unified_search_fts_delete`);
5335
+ db.exec(`DELETE FROM unified_search_items WHERE entity_type IN ('user_memo', 'user_plan', 'user_issue')`);
5336
+ db.exec(`
5337
+ INSERT OR REPLACE INTO unified_search_items(${columns})
5338
+ SELECT 'user_memo', id, scope, NULL, title,
5339
+ title || ' ' || l1_content || ' ' || ${coal("user_input")} || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")},
5340
+ search_summary, tags, created_at, updated_at
5341
+ FROM user_memos
5342
+ WHERE status = 'active' AND usage_policy != 'human_directed'
5343
+ `);
5344
+ db.exec(`
5345
+ INSERT OR REPLACE INTO unified_search_items(${columns})
5346
+ SELECT 'user_plan', id, scope, NULL, title,
5347
+ title || ' ' || l1_content || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")},
5348
+ search_summary, tags, created_at, updated_at
5349
+ FROM user_plans
5350
+ WHERE status = 'active' AND usage_policy != 'human_directed'
5351
+ `);
5352
+ db.exec(`
5353
+ INSERT OR REPLACE INTO unified_search_items(${columns})
5354
+ SELECT 'user_issue', id, scope, NULL, title,
5355
+ title || ' ' || l1_content || ' ' || ${jc("entries")} || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")},
5356
+ search_summary, tags, created_at, updated_at
5357
+ FROM user_issues
5358
+ WHERE status = 'open'
5359
+ `);
5360
+ db.exec(`INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')`);
5361
+ // DELETE トリガーを再作成
5362
+ db.exec(`
5363
+ CREATE TRIGGER unified_search_fts_delete AFTER DELETE ON unified_search_items BEGIN
5364
+ INSERT INTO unified_search_fts(unified_search_fts, rowid, title_summary, search_text, search_summary)
5365
+ VALUES ('delete', old.rowid, old.title_summary, old.search_text, old.search_summary);
5366
+ END
5367
+ `);
5368
+ // --- 外部キー整合性チェック ---
5369
+ const fkViolations = db.pragma("foreign_key_check");
5370
+ if (fkViolations.length > 0) {
5371
+ throw new Error(`V24マイグレーション後に外部キー違反が検出されました: ${JSON.stringify(fkViolations)}`);
5372
+ }
5373
+ // --- スキーマバージョン記録 ---
5374
+ db.exec(`INSERT INTO schema_version (version) VALUES (24)`);
5375
+ });
5376
+ transaction();
5377
+ }
5378
+ finally {
5379
+ db.pragma("legacy_alter_table = OFF");
5380
+ if (fkState.length > 0 && fkState[0].foreign_keys === 1) {
5381
+ db.pragma("foreign_keys = ON");
5382
+ }
5383
+ }
5384
+ }
1187
5385
  /** テーブルに指定カラムが存在するかチェック */
1188
5386
  function hasColumn(db, table, column) {
1189
5387
  const columns = db.prepare(`PRAGMA table_info(${table})`).all();