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
@@ -1,5 +1,5 @@
1
1
  import { ulid } from "ulid";
2
- import { claimScoreExpression } from "../scoring.js";
2
+ import { claimScoreExpression, computeClaimScore } from "../scoring.js";
3
3
  // === exp() サポート検出(SQLite数学関数) ===
4
4
  /** exp() が使えるかを DB ごとにキャッシュ */
5
5
  const expSupportCache = new WeakMap();
@@ -18,8 +18,13 @@ function supportsExp(db) {
18
18
  }
19
19
  }
20
20
  // === FTS5ヘルパー ===
21
+ // 2026-02-22 追加 (Issue #20): trigram tokenizer は最低3文字が必要
22
+ const TRIGRAM_MIN_CHARS = 3;
21
23
  /**
22
24
  * FTS5クエリのサニタイズ
25
+ * 2026-02-22 改善 (Issue #20): trigram tokenizer 対応
26
+ * - 全トークンが3文字未満の場合は null を返し LIKE フォールバックに誘導
27
+ * - 3文字未満のトークンをフィルタアウト
23
28
  */
24
29
  export function sanitizeFtsQuery(input) {
25
30
  const cleaned = input
@@ -32,9 +37,15 @@ export function sanitizeFtsQuery(input) {
32
37
  const tokens = cleaned.split(/\s+/).filter(t => t.length > 0);
33
38
  if (tokens.length === 0)
34
39
  return null;
35
- const quoted = tokens.map((t, i) => {
40
+ // trigram対応: 全トークンが3文字未満ならnullを返しLIKEフォールバックに誘導
41
+ const hasViableToken = tokens.some(t => t.length >= TRIGRAM_MIN_CHARS);
42
+ if (!hasViableToken)
43
+ return null;
44
+ // 3文字未満のトークンをフィルタアウト
45
+ const viableTokens = tokens.filter(t => t.length >= TRIGRAM_MIN_CHARS);
46
+ const quoted = viableTokens.map((t, i) => {
36
47
  const escaped = t.replace(/"/g, '""');
37
- if (i === tokens.length - 1) {
48
+ if (i === viableTokens.length - 1) {
38
49
  return `"${escaped}"*`;
39
50
  }
40
51
  return `"${escaped}"`;
@@ -46,8 +57,8 @@ export function insertClaim(db, params) {
46
57
  const id = ulid();
47
58
  const now = new Date().toISOString();
48
59
  db.prepare(`
49
- INSERT INTO claims (id, subject, predicate, object, category, scope, confidence, evidence, falsifier, search_summary, source_tool, source_session, client_name, client_version, status, created_at, updated_at)
50
- VALUES (@id, @subject, @predicate, @object, @category, @scope, @confidence, @evidence, @falsifier, @search_summary, @source_tool, @source_session, @client_name, @client_version, 'active', @created_at, @updated_at)
60
+ INSERT INTO claims (id, subject, predicate, object, category, scope, confidence, evidence, falsifier, l1_content, search_summary, l1_embedding, source_tool, source_session, client_name, client_version, status, created_at, updated_at)
61
+ VALUES (@id, @subject, @predicate, @object, @category, @scope, @confidence, @evidence, @falsifier, @l1_content, @search_summary, @l1_embedding, @source_tool, @source_session, @client_name, @client_version, 'active', @created_at, @updated_at)
51
62
  `).run({
52
63
  id,
53
64
  subject: params.subject,
@@ -58,7 +69,9 @@ export function insertClaim(db, params) {
58
69
  confidence: params.confidence,
59
70
  evidence: params.evidence ?? null,
60
71
  falsifier: params.falsifier ?? null,
72
+ l1_content: params.l1_content ?? null,
61
73
  search_summary: params.search_summary ?? null,
74
+ l1_embedding: params.l1_embedding ?? null,
62
75
  source_tool: params.source_tool ?? null,
63
76
  source_session: params.source_session ?? null,
64
77
  client_name: params.client_name ?? null,
@@ -76,7 +89,10 @@ export function updateClaim(db, id, updates, reason) {
76
89
  if (!existing)
77
90
  return undefined;
78
91
  const now = new Date().toISOString();
79
- const allowedFields = ["subject", "predicate", "object", "category", "scope", "confidence", "evidence", "falsifier"];
92
+ // 2026-02-22 修正 (Issue #22): search_summary を追加
93
+ // 元の実装: search_summary は store 時のみ設定可能だった
94
+ // 2026-03-03 修正 (Issue #64): l1_content を追加
95
+ const allowedFields = ["subject", "predicate", "object", "category", "scope", "confidence", "evidence", "falsifier", "l1_content", "search_summary"];
80
96
  const transaction = db.transaction(() => {
81
97
  for (const [field, newValue] of Object.entries(updates)) {
82
98
  if (!allowedFields.includes(field))
@@ -208,7 +224,7 @@ export function searchClaims(db, params) {
208
224
  values.category = params.category;
209
225
  }
210
226
  if (params.scope) {
211
- conditions.push("c.scope = @scope");
227
+ conditions.push("(c.scope = @scope OR c.scope = 'global')");
212
228
  values.scope = params.scope;
213
229
  }
214
230
  if (sanitized) {
@@ -217,13 +233,16 @@ export function searchClaims(db, params) {
217
233
  ? "AND " + conditions.join(" AND ")
218
234
  : "";
219
235
  try {
220
- return db.prepare(`
236
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
237
+ const ftsResults = db.prepare(`
221
238
  SELECT c.* FROM claims c
222
239
  JOIN claims_fts f ON c.rowid = f.rowid
223
240
  WHERE claims_fts MATCH @query ${whereClause}
224
241
  ORDER BY ${useScore ? `${scoreExpr} DESC` : "rank"}
225
242
  LIMIT @limit
226
243
  `).all(values);
244
+ if (ftsResults.length > 0)
245
+ return ftsResults;
227
246
  }
228
247
  catch {
229
248
  // FTS5エラー時はLIKEフォールバック
@@ -255,7 +274,7 @@ export function searchClaims(db, params) {
255
274
  values.category = params.category;
256
275
  }
257
276
  if (params.scope) {
258
- conditions.push("scope = @scope");
277
+ conditions.push("(scope = @scope OR scope = 'global')");
259
278
  values.scope = params.scope;
260
279
  }
261
280
  const whereClause = conditions.length > 0
@@ -267,12 +286,16 @@ export function searchClaims(db, params) {
267
286
  LIMIT @limit
268
287
  `).all(values);
269
288
  }
289
+ // 2026-02-23 修正 (Issue #40 Phase 2): 戻り値に insights, models を追加し6エンティティ対応
290
+ // 元の実装: 4エンティティ(claims, episodes, decisions, theories)のみ返却、distributeResults4 を使用
270
291
  export function queryContext(db, topic, scope, limit) {
271
292
  const claims = queryClaimContext(db, topic, scope, limit);
272
293
  const episodes = queryEpisodeContext(db, topic, scope, limit);
273
294
  const decisions = queryDecisionContext(db, topic, scope, limit);
274
295
  const theories = queryTheoryContext(db, topic, scope, limit);
275
- return distributeResults4(claims, episodes, decisions, theories, limit);
296
+ const insights = queryInsightContext(db, topic, scope, limit);
297
+ const models = queryModelContext(db, topic, scope, limit);
298
+ return distributeResults6(claims, episodes, decisions, theories, insights, models, limit);
276
299
  }
277
300
  /** 3エンティティの結果を合計limit件に均等配分して切り詰める */
278
301
  function distributeResults(claims, episodes, decisions, limit) {
@@ -300,6 +323,7 @@ function distributeResults(claims, episodes, decisions, limit) {
300
323
  decisions: decisions.slice(0, allocMap.get(2)),
301
324
  };
302
325
  }
326
+ // 2026-02-23 修正 (Issue #40 Phase 2): distributeResults6 に移行。後方互換のため残す。
303
327
  /** 4エンティティの結果を合計limit件に均等配分して切り詰める */
304
328
  function distributeResults4(claims, episodes, decisions, theories, limit) {
305
329
  const total = claims.length + episodes.length + decisions.length + theories.length;
@@ -328,17 +352,66 @@ function distributeResults4(claims, episodes, decisions, theories, limit) {
328
352
  theories: theories.slice(0, allocMap.get(3)),
329
353
  };
330
354
  }
355
+ // 2026-02-23 追加 (Issue #40 Phase 2): 6エンティティ版の均等配分
356
+ // 2026-02-23 修正 (Codex P2指摘): 低limit時のゼロ配分バイアスを防止
357
+ // 元の実装: Math.floor(remaining / N) のみ → limit<6時に先頭エンティティが0件になる
358
+ // 修正: 均等配分後、データがあるのに0件のエンティティへ余裕のあるエンティティから1件移譲
359
+ /** 6エンティティの結果を合計limit件に均等配分して切り詰める */
360
+ function distributeResults6(claims, episodes, decisions, theories, insights, models, limit) {
361
+ const total = claims.length + episodes.length + decisions.length + theories.length + insights.length + models.length;
362
+ if (total <= limit) {
363
+ return { claims, episodes, decisions, theories, insights, models };
364
+ }
365
+ const arrays = [
366
+ { items: claims, len: claims.length },
367
+ { items: episodes, len: episodes.length },
368
+ { items: decisions, len: decisions.length },
369
+ { items: theories, len: theories.length },
370
+ { items: insights, len: insights.length },
371
+ { items: models, len: models.length },
372
+ ];
373
+ let remaining = limit;
374
+ const sorted = arrays.map((a, i) => ({ ...a, idx: i, alloc: 0 }))
375
+ .sort((a, b) => a.len - b.len);
376
+ for (let i = 0; i < sorted.length; i++) {
377
+ const share = Math.floor(remaining / (sorted.length - i));
378
+ sorted[i].alloc = Math.min(sorted[i].len, share);
379
+ remaining -= sorted[i].alloc;
380
+ }
381
+ // ゼロ配分修正: データがあるのに0件のエンティティへ、余裕のあるエンティティから1件移譲
382
+ for (let i = 0; i < sorted.length; i++) {
383
+ if (sorted[i].len > 0 && sorted[i].alloc === 0) {
384
+ for (let j = sorted.length - 1; j >= 0; j--) {
385
+ if (sorted[j].alloc > 1) {
386
+ sorted[j].alloc--;
387
+ sorted[i].alloc = 1;
388
+ break;
389
+ }
390
+ }
391
+ }
392
+ }
393
+ const allocMap = new Map(sorted.map(s => [s.idx, s.alloc]));
394
+ return {
395
+ claims: claims.slice(0, allocMap.get(0)),
396
+ episodes: episodes.slice(0, allocMap.get(1)),
397
+ decisions: decisions.slice(0, allocMap.get(2)),
398
+ theories: theories.slice(0, allocMap.get(3)),
399
+ insights: insights.slice(0, allocMap.get(4)),
400
+ models: models.slice(0, allocMap.get(5)),
401
+ };
402
+ }
331
403
  function queryClaimContext(db, topic, scope, limit) {
332
404
  const sanitized = sanitizeFtsQuery(topic);
333
405
  let scopeFilter = "";
334
406
  const scopeValues = {};
335
407
  if (scope) {
336
- scopeFilter = "AND c.scope = @scope";
408
+ scopeFilter = "AND (c.scope = @scope OR c.scope = 'global')";
337
409
  scopeValues.scope = scope;
338
410
  }
339
411
  if (sanitized) {
340
412
  try {
341
- return db.prepare(`
413
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
414
+ const ftsResults = db.prepare(`
342
415
  SELECT c.* FROM claims c
343
416
  JOIN claims_fts f ON c.rowid = f.rowid
344
417
  WHERE claims_fts MATCH @query
@@ -347,6 +420,8 @@ function queryClaimContext(db, topic, scope, limit) {
347
420
  ORDER BY rank
348
421
  LIMIT @limit
349
422
  `).all({ query: sanitized, limit, ...scopeValues });
423
+ if (ftsResults.length > 0)
424
+ return ftsResults;
350
425
  }
351
426
  catch {
352
427
  // FTS5エラー時はLIKEフォールバック
@@ -367,12 +442,13 @@ function queryEpisodeContext(db, topic, scope, limit) {
367
442
  let scopeFilter = "";
368
443
  const scopeValues = {};
369
444
  if (scope) {
370
- scopeFilter = "AND e.scope = @scope";
445
+ scopeFilter = "AND (e.scope = @scope OR e.scope = 'global')";
371
446
  scopeValues.scope = scope;
372
447
  }
373
448
  if (sanitized) {
374
449
  try {
375
- return db.prepare(`
450
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
451
+ const ftsResults = db.prepare(`
376
452
  SELECT e.* FROM episodes e
377
453
  JOIN episodes_fts f ON e.rowid = f.rowid
378
454
  WHERE episodes_fts MATCH @query
@@ -381,6 +457,8 @@ function queryEpisodeContext(db, topic, scope, limit) {
381
457
  ORDER BY rank
382
458
  LIMIT @limit
383
459
  `).all({ query: sanitized, limit, ...scopeValues });
460
+ if (ftsResults.length > 0)
461
+ return ftsResults;
384
462
  }
385
463
  catch {
386
464
  // FTS5エラー時はLIKEフォールバック
@@ -390,7 +468,7 @@ function queryEpisodeContext(db, topic, scope, limit) {
390
468
  return db.prepare(`
391
469
  SELECT e.* FROM episodes e
392
470
  WHERE e.status = 'active'
393
- AND (e.title LIKE @like_query OR e.context LIKE @like_query OR e.trigger LIKE @like_query OR e.principles LIKE @like_query OR e.problems LIKE @like_query OR e.desires LIKE @like_query OR e.outcomes LIKE @like_query OR e.decisions LIKE @like_query OR e.search_summary LIKE @like_query)
471
+ AND (e.title LIKE @like_query OR e.l1_content LIKE @like_query OR e.l2_context LIKE @like_query OR e.l2_trigger LIKE @like_query OR e.l2_principles LIKE @like_query OR e.l2_problems LIKE @like_query OR e.l2_desires LIKE @like_query OR e.l2_outcomes LIKE @like_query OR e.l2_decisions LIKE @like_query OR e.search_summary LIKE @like_query)
394
472
  ${scopeFilter}
395
473
  ORDER BY e.updated_at DESC
396
474
  LIMIT @limit
@@ -401,12 +479,13 @@ function queryDecisionContext(db, topic, scope, limit) {
401
479
  let scopeFilter = "";
402
480
  const scopeValues = {};
403
481
  if (scope) {
404
- scopeFilter = "AND d.scope = @scope";
482
+ scopeFilter = "AND (d.scope = @scope OR d.scope = 'global')";
405
483
  scopeValues.scope = scope;
406
484
  }
407
485
  if (sanitized) {
408
486
  try {
409
- return db.prepare(`
487
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
488
+ const ftsResults = db.prepare(`
410
489
  SELECT d.* FROM decisions d
411
490
  JOIN decisions_fts f ON d.rowid = f.rowid
412
491
  WHERE decisions_fts MATCH @query
@@ -415,6 +494,8 @@ function queryDecisionContext(db, topic, scope, limit) {
415
494
  ORDER BY rank
416
495
  LIMIT @limit
417
496
  `).all({ query: sanitized, limit, ...scopeValues });
497
+ if (ftsResults.length > 0)
498
+ return ftsResults;
418
499
  }
419
500
  catch {
420
501
  // FTS5エラー時はLIKEフォールバック
@@ -424,7 +505,7 @@ function queryDecisionContext(db, topic, scope, limit) {
424
505
  return db.prepare(`
425
506
  SELECT d.* FROM decisions d
426
507
  WHERE d.status = 'active'
427
- AND (d.title LIKE @like_query OR d.description LIKE @like_query OR d.reasoning LIKE @like_query OR d.search_summary LIKE @like_query)
508
+ AND (d.title LIKE @like_query OR d.description LIKE @like_query OR d.l2_reasoning LIKE @like_query OR d.search_summary LIKE @like_query)
428
509
  ${scopeFilter}
429
510
  ORDER BY d.created_at DESC
430
511
  LIMIT @limit
@@ -434,24 +515,29 @@ export function getClaimHistory(db, claimId) {
434
515
  return db.prepare("SELECT * FROM claim_history WHERE claim_id = ? ORDER BY changed_at DESC").all(claimId);
435
516
  }
436
517
  // === Decision CRUD ===
518
+ // 2026-03-01 修正 (Issue #59): reasoning → l2_reasoning, alternatives → l2_alternatives, l1_content 追加
519
+ // 元の実装: reasoning, alternatives(プレフィックスなし)
437
520
  export function insertDecision(db, params) {
438
521
  const id = ulid();
439
522
  const now = new Date().toISOString();
440
523
  db.prepare(`
441
- INSERT INTO decisions (id, title, description, reasoning, alternatives, related_claim_ids, scope, status, search_summary, source_tool, client_name, client_version, created_at, updated_at)
442
- VALUES (@id, @title, @description, @reasoning, @alternatives, @related_claim_ids, @scope, 'active', @search_summary, @source_tool, @client_name, @client_version, @created_at, @updated_at)
524
+ INSERT INTO decisions (id, title, description, l1_content, l2_reasoning, l2_alternatives, related_claim_ids, scope, status, search_summary, l1_embedding, source_tool, client_name, client_version, user_input, created_at, updated_at)
525
+ VALUES (@id, @title, @description, @l1_content, @l2_reasoning, @l2_alternatives, @related_claim_ids, @scope, 'active', @search_summary, @l1_embedding, @source_tool, @client_name, @client_version, @user_input, @created_at, @updated_at)
443
526
  `).run({
444
527
  id,
445
528
  title: params.title,
446
529
  description: params.description,
447
- reasoning: params.reasoning,
448
- alternatives: JSON.stringify(params.alternatives),
530
+ l1_content: params.l1_content ?? null,
531
+ l2_reasoning: params.l2_reasoning,
532
+ l2_alternatives: JSON.stringify(params.l2_alternatives),
449
533
  related_claim_ids: JSON.stringify(params.related_claim_ids),
450
534
  scope: params.scope ?? "global",
451
535
  search_summary: params.search_summary ?? null,
536
+ l1_embedding: params.l1_embedding ?? null,
452
537
  source_tool: params.source_tool ?? null,
453
538
  client_name: params.client_name ?? null,
454
539
  client_version: params.client_version ?? null,
540
+ user_input: params.user_input ?? null,
455
541
  created_at: now,
456
542
  updated_at: now,
457
543
  });
@@ -470,7 +556,7 @@ export function listDecisions(db, params) {
470
556
  values.status = params.status;
471
557
  }
472
558
  if (params.scope) {
473
- conditions.push("d.scope = @scope");
559
+ conditions.push("(d.scope = @scope OR d.scope = 'global')");
474
560
  values.scope = params.scope;
475
561
  }
476
562
  if (sanitized) {
@@ -479,13 +565,16 @@ export function listDecisions(db, params) {
479
565
  ? "AND " + conditions.join(" AND ")
480
566
  : "";
481
567
  try {
482
- return db.prepare(`
568
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
569
+ const ftsResults = db.prepare(`
483
570
  SELECT d.* FROM decisions d
484
571
  JOIN decisions_fts f ON d.rowid = f.rowid
485
572
  WHERE decisions_fts MATCH @query ${whereClause}
486
573
  ORDER BY rank
487
574
  LIMIT @limit
488
575
  `).all(values);
576
+ if (ftsResults.length > 0)
577
+ return ftsResults;
489
578
  }
490
579
  catch {
491
580
  // FTS5エラー時はLIKEフォールバック
@@ -495,7 +584,8 @@ export function listDecisions(db, params) {
495
584
  const likeValues = { ...values, limit: params.limit };
496
585
  delete likeValues.query;
497
586
  likeValues.like_query = `%${params.query}%`;
498
- likeConditions.push("(d.title LIKE @like_query OR d.description LIKE @like_query OR d.reasoning LIKE @like_query OR d.search_summary LIKE @like_query)");
587
+ // 2026-03-01 修正 (Issue #59): reasoning l2_reasoning, l1_content 追加
588
+ likeConditions.push("(d.title LIKE @like_query OR d.description LIKE @like_query OR d.l1_content LIKE @like_query OR d.l2_reasoning LIKE @like_query OR d.search_summary LIKE @like_query OR d.user_input LIKE @like_query)");
499
589
  const likeWhere = likeConditions.length > 0
500
590
  ? "WHERE " + likeConditions.join(" AND ")
501
591
  : "";
@@ -513,7 +603,7 @@ export function listDecisions(db, params) {
513
603
  values.status = params.status;
514
604
  }
515
605
  if (params.scope) {
516
- conditions.push("scope = @scope");
606
+ conditions.push("(scope = @scope OR scope = 'global')");
517
607
  values.scope = params.scope;
518
608
  }
519
609
  const whereClause = conditions.length > 0
@@ -534,6 +624,27 @@ export function getStats(db) {
534
624
  const total_decisions = db.prepare("SELECT COUNT(*) as count FROM decisions").get().count;
535
625
  const active_decisions = db.prepare("SELECT COUNT(*) as count FROM decisions WHERE status = 'active'").get().count;
536
626
  const total_history_entries = db.prepare("SELECT COUNT(*) as count FROM claim_history").get().count;
627
+ // 2026-02-23 追加 (Issue #39 Part D): Episode/Insight/Theory/Model の安全なカウント
628
+ let active_episodes = 0;
629
+ try {
630
+ active_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE status = 'active'").get().count;
631
+ }
632
+ catch { /* テーブル未存在等 */ }
633
+ let active_insights = 0;
634
+ try {
635
+ active_insights = db.prepare("SELECT COUNT(*) as count FROM insights WHERE status = 'active'").get().count;
636
+ }
637
+ catch { /* テーブル未存在等 */ }
638
+ let active_theories = 0;
639
+ try {
640
+ active_theories = db.prepare("SELECT COUNT(*) as count FROM theories WHERE status = 'active'").get().count;
641
+ }
642
+ catch { /* テーブル未存在等 */ }
643
+ let active_models = 0;
644
+ try {
645
+ active_models = db.prepare("SELECT COUNT(*) as count FROM models WHERE status = 'active'").get().count;
646
+ }
647
+ catch { /* テーブル未存在等 */ }
537
648
  const categoryRows = db.prepare("SELECT category, COUNT(*) as count FROM claims WHERE status = 'active' GROUP BY category").all();
538
649
  const categories = {};
539
650
  for (const row of categoryRows) {
@@ -548,6 +659,10 @@ export function getStats(db) {
548
659
  active_decisions,
549
660
  total_history_entries,
550
661
  categories,
662
+ active_episodes,
663
+ active_insights,
664
+ active_theories,
665
+ active_models,
551
666
  };
552
667
  }
553
668
  export function getAllActiveClaims(db) {
@@ -626,9 +741,11 @@ export function getClaimChecks(db, claimId) {
626
741
  export function insertAuditLog(db, params) {
627
742
  const id = ulid();
628
743
  const now = new Date().toISOString();
744
+ // 2026-03-03 追加 (V21): client_id でどのデバイスで作られたか追跡
745
+ const clientIdRow = db.prepare("SELECT value FROM store_meta WHERE key = 'client_id'").get();
629
746
  db.prepare(`
630
- INSERT INTO audit_log (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, created_at)
631
- VALUES (@id, @operation, @entity_type, @entity_id, @summary, @details, @client_name, @client_version, @session_id, @source_tool, @created_at)
747
+ INSERT INTO audit_log (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, client_id, created_at)
748
+ VALUES (@id, @operation, @entity_type, @entity_id, @summary, @details, @client_name, @client_version, @session_id, @source_tool, @client_id, @created_at)
632
749
  `).run({
633
750
  id,
634
751
  operation: params.operation,
@@ -640,12 +757,19 @@ export function insertAuditLog(db, params) {
640
757
  client_version: params.provenance?.client_version ?? null,
641
758
  session_id: params.provenance?.session_id ?? null,
642
759
  source_tool: params.source_tool ?? null,
760
+ client_id: clientIdRow?.value ?? null,
643
761
  created_at: now,
644
762
  });
645
763
  }
646
764
  export function getAuditLog(db, entityType, entityId) {
647
765
  return db.prepare("SELECT * FROM audit_log WHERE entity_type = ? AND entity_id = ? ORDER BY created_at DESC").all(entityType, entityId);
648
766
  }
767
+ // 2026-02-28 追加 (Issue #49): 最後の create 操作のタイムスタンプを取得
768
+ // ナッジエスカレーションで「最後の記録からの距離」を判定するために使用
769
+ export function getLastCreateAuditTimestamp(db) {
770
+ const row = db.prepare("SELECT MAX(created_at) as last_at FROM audit_log WHERE operation = 'create'").get();
771
+ return row?.last_at ?? null;
772
+ }
649
773
  export function searchAuditLog(db, params) {
650
774
  const conditions = [];
651
775
  const values = { limit: params.limit };
@@ -671,30 +795,35 @@ export function searchAuditLog(db, params) {
671
795
  `).all(values);
672
796
  }
673
797
  // === V4: Episode CRUD ===
798
+ // 2026-03-01 修正 (Issue #59): 7フィールドリネーム + l1_content 追加
799
+ // 元の実装: context, trigger, problems, desires, decisions, outcomes, principles(プレフィックスなし)
674
800
  export function insertEpisode(db, params) {
675
801
  const id = ulid();
676
802
  const now = new Date().toISOString();
677
803
  db.prepare(`
678
- INSERT INTO episodes (id, title, context, trigger, problems, desires, decisions, outcomes, principles, evidence_refs, tags, scope, status, search_summary, client_name, client_version, source_tool, session_id, created_at, updated_at)
679
- VALUES (@id, @title, @context, @trigger, @problems, @desires, @decisions, @outcomes, @principles, @evidence_refs, @tags, @scope, 'active', @search_summary, @client_name, @client_version, @source_tool, @session_id, @created_at, @updated_at)
804
+ INSERT INTO episodes (id, title, l1_content, l2_context, l2_trigger, l2_problems, l2_desires, l2_decisions, l2_outcomes, l2_principles, evidence_refs, tags, scope, status, search_summary, l1_embedding, source_tool, session_id, client_name, client_version, user_input, created_at, updated_at)
805
+ VALUES (@id, @title, @l1_content, @l2_context, @l2_trigger, @l2_problems, @l2_desires, @l2_decisions, @l2_outcomes, @l2_principles, @evidence_refs, @tags, @scope, 'active', @search_summary, @l1_embedding, @source_tool, @session_id, @client_name, @client_version, @user_input, @created_at, @updated_at)
680
806
  `).run({
681
807
  id,
682
808
  title: params.title,
683
- context: params.context ?? null,
684
- trigger: params.trigger ?? null,
685
- problems: JSON.stringify(params.problems),
686
- desires: JSON.stringify(params.desires),
687
- decisions: JSON.stringify(params.decisions),
688
- outcomes: JSON.stringify(params.outcomes),
689
- principles: JSON.stringify(params.principles),
809
+ l1_content: params.l1_content ?? null,
810
+ l2_context: params.l2_context ?? null,
811
+ l2_trigger: params.l2_trigger ?? null,
812
+ l2_problems: JSON.stringify(params.l2_problems),
813
+ l2_desires: JSON.stringify(params.l2_desires),
814
+ l2_decisions: JSON.stringify(params.l2_decisions),
815
+ l2_outcomes: JSON.stringify(params.l2_outcomes),
816
+ l2_principles: JSON.stringify(params.l2_principles),
690
817
  evidence_refs: JSON.stringify(params.evidence_refs),
691
818
  tags: JSON.stringify(params.tags),
692
819
  scope: params.scope ?? "global",
693
820
  search_summary: params.search_summary ?? null,
694
- client_name: params.client_name ?? null,
695
- client_version: params.client_version ?? null,
821
+ l1_embedding: params.l1_embedding ?? null,
696
822
  source_tool: params.source_tool ?? null,
697
823
  session_id: params.session_id ?? null,
824
+ client_name: params.client_name ?? null,
825
+ client_version: params.client_version ?? null,
826
+ user_input: params.user_input ?? null,
698
827
  created_at: now,
699
828
  updated_at: now,
700
829
  });
@@ -717,7 +846,7 @@ export function listEpisodes(db, params) {
717
846
  values.tag_like = `%"${params.tag}"%`;
718
847
  }
719
848
  if (params.scope) {
720
- conditions.push("e.scope = @scope");
849
+ conditions.push("(e.scope = @scope OR e.scope = 'global')");
721
850
  values.scope = params.scope;
722
851
  }
723
852
  if (sanitized) {
@@ -726,13 +855,16 @@ export function listEpisodes(db, params) {
726
855
  ? "AND " + conditions.join(" AND ")
727
856
  : "";
728
857
  try {
729
- return db.prepare(`
858
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
859
+ const ftsResults = db.prepare(`
730
860
  SELECT e.* FROM episodes e
731
861
  JOIN episodes_fts f ON e.rowid = f.rowid
732
862
  WHERE episodes_fts MATCH @query ${whereClause}
733
863
  ORDER BY rank
734
864
  LIMIT @limit
735
865
  `).all(values);
866
+ if (ftsResults.length > 0)
867
+ return ftsResults;
736
868
  }
737
869
  catch {
738
870
  // FTS5エラー時はLIKEフォールバック
@@ -742,7 +874,8 @@ export function listEpisodes(db, params) {
742
874
  const likeValues = { ...values, limit: params.limit };
743
875
  delete likeValues.query;
744
876
  likeValues.like_query = `%${params.query}%`;
745
- likeConditions.push("(e.title LIKE @like_query OR e.context LIKE @like_query OR e.trigger LIKE @like_query OR e.principles LIKE @like_query OR e.problems LIKE @like_query OR e.desires LIKE @like_query OR e.outcomes LIKE @like_query OR e.decisions LIKE @like_query OR e.search_summary LIKE @like_query)");
877
+ // 2026-03-01 修正 (Issue #59): l2_ プレフィックス + l1_content
878
+ likeConditions.push("(e.title LIKE @like_query OR e.l1_content LIKE @like_query OR e.l2_context LIKE @like_query OR e.l2_trigger LIKE @like_query OR e.l2_principles LIKE @like_query OR e.l2_problems LIKE @like_query OR e.l2_desires LIKE @like_query OR e.l2_outcomes LIKE @like_query OR e.l2_decisions LIKE @like_query OR e.search_summary LIKE @like_query OR e.user_input LIKE @like_query)");
746
879
  const likeWhere = likeConditions.length > 0
747
880
  ? "WHERE " + likeConditions.join(" AND ")
748
881
  : "";
@@ -764,7 +897,7 @@ export function listEpisodes(db, params) {
764
897
  values.tag_like = `%"${params.tag}"%`;
765
898
  }
766
899
  if (params.scope) {
767
- conditions.push("scope = @scope");
900
+ conditions.push("(scope = @scope OR scope = 'global')");
768
901
  values.scope = params.scope;
769
902
  }
770
903
  const whereClause = conditions.length > 0
@@ -782,31 +915,196 @@ export function getEpisodeStats(db) {
782
915
  const archived_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE status = 'archived'").get().count;
783
916
  return { total_episodes, active_episodes, archived_episodes };
784
917
  }
918
+ // === Issue #40 Phase 3: Episode棚卸 ===
919
+ // 2026-02-23 追加 (Issue #40 Phase 3): 未グルーミングのEpisodeを取得
920
+ export function listUngroomedEpisodes(db, params) {
921
+ const conditions = ["e.status = 'active'"];
922
+ const values = { limit: params.limit };
923
+ if (!params.include_groomed) {
924
+ conditions.push("e.groomed_at IS NULL");
925
+ }
926
+ if (params.scope) {
927
+ conditions.push("(e.scope = @scope OR e.scope = 'global')");
928
+ values.scope = params.scope;
929
+ }
930
+ if (params.tag) {
931
+ conditions.push("e.tags LIKE @tag_like");
932
+ values.tag_like = `%"${params.tag}"%`;
933
+ }
934
+ const whereClause = conditions.join(" AND ");
935
+ const rows = db.prepare(`
936
+ SELECT
937
+ e.*,
938
+ (e.l2_principles != '[]') AS has_principles,
939
+ (e.l2_outcomes != '[]') AS has_outcomes,
940
+ (e.l2_decisions != '[]') AS has_decisions,
941
+ COALESCE(li.cnt, 0) AS linked_insight_count,
942
+ COALESCE(lm.cnt, 0) AS linked_model_count,
943
+ COALESCE(lt.cnt, 0) AS linked_theory_count
944
+ FROM episodes e
945
+ LEFT JOIN (
946
+ SELECT j.value AS eid, COUNT(*) AS cnt
947
+ FROM insights, json_each(insights.supporting_episode_ids) AS j
948
+ WHERE insights.status = 'active'
949
+ GROUP BY j.value
950
+ ) li ON li.eid = e.id
951
+ LEFT JOIN (
952
+ SELECT j.value AS eid, COUNT(*) AS cnt
953
+ FROM models, json_each(models.supporting_episode_ids) AS j
954
+ WHERE models.status = 'active'
955
+ GROUP BY j.value
956
+ ) lm ON lm.eid = e.id
957
+ LEFT JOIN (
958
+ SELECT j.value AS eid, COUNT(*) AS cnt
959
+ FROM theories, json_each(theories.supporting_episode_ids) AS j
960
+ WHERE theories.status = 'active'
961
+ GROUP BY j.value
962
+ ) lt ON lt.eid = e.id
963
+ WHERE ${whereClause}
964
+ ORDER BY e.created_at DESC
965
+ LIMIT @limit
966
+ `).all(values);
967
+ return rows.map(row => ({
968
+ ...row,
969
+ has_principles: row.has_principles === 1,
970
+ has_outcomes: row.has_outcomes === 1,
971
+ has_decisions: row.has_decisions === 1,
972
+ linked_insight_count: row.linked_insight_count,
973
+ linked_model_count: row.linked_model_count,
974
+ linked_theory_count: row.linked_theory_count,
975
+ }));
976
+ }
977
+ // 2026-02-23 追加 (Issue #40 Phase 3): EpisodeをグルーミングEPISODE済みにマーク
978
+ export function markEpisodesGroomed(db, episodeIds) {
979
+ const now = new Date().toISOString();
980
+ const markedIds = [];
981
+ const transaction = db.transaction(() => {
982
+ for (const id of episodeIds) {
983
+ const result = db.prepare("UPDATE episodes SET groomed_at = @now WHERE id = @id AND status = 'active' AND groomed_at IS NULL").run({ id, now });
984
+ if (result.changes > 0) {
985
+ markedIds.push(id);
986
+ }
987
+ }
988
+ });
989
+ transaction();
990
+ return markedIds;
991
+ }
992
+ // === Issue #35: トピック集計 ===
993
+ // 2026-02-22 修正 (Codex P2指摘): scope パラメータを追加し、スコープ絞り込みに対応
994
+ export function getTopicSummary(db, tagLimit = 10, decisionLimit = 5, scope) {
995
+ // スコープフィルタ条件を共通生成
996
+ const scopeFilter = scope ? " AND (scope = @scope OR scope = 'global')" : "";
997
+ const scopeValues = scope ? { scope } : {};
998
+ // カテゴリ分布
999
+ const categoryRows = db.prepare(`SELECT category, COUNT(*) as count FROM claims WHERE status = 'active'${scopeFilter} GROUP BY category`).all(scopeValues);
1000
+ const categories = {};
1001
+ for (const row of categoryRows) {
1002
+ categories[row.category] = row.count;
1003
+ }
1004
+ // タグ集計(Episodes + Theories の tags を json_each で展開)
1005
+ let topTags = [];
1006
+ try {
1007
+ topTags = db.prepare(`
1008
+ SELECT tag, COUNT(*) as count FROM (
1009
+ SELECT j.value as tag FROM episodes, json_each(episodes.tags) AS j WHERE episodes.status = 'active'${scopeFilter.replace("scope", "episodes.scope")}
1010
+ UNION ALL
1011
+ SELECT j.value as tag FROM theories, json_each(theories.tags) AS j WHERE theories.status = 'active'${scopeFilter.replace("scope", "theories.scope")}
1012
+ UNION ALL
1013
+ SELECT j.value as tag FROM insights, json_each(insights.tags) AS j WHERE insights.status = 'active'${scopeFilter.replace("scope", "insights.scope")}
1014
+ UNION ALL
1015
+ SELECT j.value as tag FROM models, json_each(models.tags) AS j WHERE models.status = 'active'${scopeFilter.replace("scope", "models.scope")}
1016
+ ) GROUP BY tag ORDER BY count DESC LIMIT @tagLimit
1017
+ `).all({ ...scopeValues, tagLimit });
1018
+ }
1019
+ catch {
1020
+ // json_each 非対応環境のフォールバック: 空配列
1021
+ }
1022
+ // 最近のDecision
1023
+ const recentDecisions = db.prepare(`SELECT title, created_at FROM decisions WHERE status = 'active'${scopeFilter} ORDER BY created_at DESC LIMIT @decisionLimit`).all({ ...scopeValues, decisionLimit });
1024
+ // アクティブスコープ一覧
1025
+ const scopeRows = db.prepare(`SELECT DISTINCT scope FROM claims WHERE status = 'active'${scopeFilter} ORDER BY scope`).all(scopeValues);
1026
+ const activeScopes = scopeRows.map(r => r.scope);
1027
+ // 4テーブルのアクティブ件数
1028
+ const activeClaims = db.prepare(`SELECT COUNT(*) as count FROM claims WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1029
+ const activeEpisodes = db.prepare(`SELECT COUNT(*) as count FROM episodes WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1030
+ const activeDecisions = db.prepare(`SELECT COUNT(*) as count FROM decisions WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1031
+ const activeTheories = db.prepare(`SELECT COUNT(*) as count FROM theories WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1032
+ // 2026-02-23 追加 (Issue #40): Insight/Model のアクティブ件数
1033
+ const activeInsights = db.prepare(`SELECT COUNT(*) as count FROM insights WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1034
+ const activeModels = db.prepare(`SELECT COUNT(*) as count FROM models WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1035
+ // 2026-02-25 リネーム: memos → user_memos (Issue #45 Phase 1)
1036
+ let activeMemos = 0;
1037
+ try {
1038
+ activeMemos = db.prepare(`SELECT COUNT(*) as count FROM user_memos WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1039
+ }
1040
+ catch { /* V16未適用 */ }
1041
+ // Issue #47: エンティティタイトルから具体的キーワード候補を抽出
1042
+ let topicKeywords = [];
1043
+ try {
1044
+ const keywordRows = db.prepare(`
1045
+ SELECT title FROM (
1046
+ SELECT title, updated_at FROM episodes WHERE status = 'active'${scopeFilter.replace("scope", "episodes.scope")}
1047
+ UNION ALL
1048
+ SELECT title, updated_at FROM decisions WHERE status = 'active'${scopeFilter.replace("scope", "decisions.scope")}
1049
+ UNION ALL
1050
+ SELECT title, updated_at FROM theories WHERE status = 'active'${scopeFilter.replace("scope", "theories.scope")}
1051
+ UNION ALL
1052
+ SELECT title, updated_at FROM insights WHERE status = 'active'${scopeFilter.replace("scope", "insights.scope")}
1053
+ UNION ALL
1054
+ SELECT title, updated_at FROM models WHERE status = 'active'${scopeFilter.replace("scope", "models.scope")}
1055
+ ) ORDER BY updated_at DESC LIMIT 20
1056
+ `).all(scopeValues);
1057
+ topicKeywords = keywordRows.map(r => r.title.length > 30 ? r.title.slice(0, 30) + "…" : r.title);
1058
+ }
1059
+ catch {
1060
+ // 古いスキーマ(theories/insights/models未対応)へのフォールバック
1061
+ }
1062
+ return {
1063
+ categories,
1064
+ topTags,
1065
+ topicKeywords,
1066
+ recentDecisions,
1067
+ activeScopes,
1068
+ counts: { activeClaims, activeEpisodes, activeDecisions, activeTheories, activeInsights, activeModels, activeMemos },
1069
+ };
1070
+ }
785
1071
  // === V9: Theory CRUD ===
1072
+ // 2026-03-01 修正 (Issue #59): core_thesis → l2_core_thesis, principles → l2_principles + 新l2カラム + l1_content
1073
+ // 元の実装: core_thesis, principles(プレフィックスなし)
786
1074
  export function insertTheory(db, params) {
787
1075
  const id = ulid();
788
1076
  const now = new Date().toISOString();
789
1077
  db.prepare(`
790
- INSERT INTO theories (id, title, description, core_thesis, principles, non_goals, open_questions,
791
- supporting_episode_ids, supporting_claim_ids, tags, scope, confidence, status, search_summary,
1078
+ INSERT INTO theories (id, title, description, l1_content,
1079
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
1080
+ non_goals, open_questions,
1081
+ supporting_episode_ids, supporting_claim_ids, evidence_refs, tags, scope, confidence, status, search_summary, l1_embedding,
792
1082
  source_tool, session_id, client_name, client_version, created_at, updated_at)
793
- VALUES (@id, @title, @description, @core_thesis, @principles, @non_goals, @open_questions,
794
- @supporting_episode_ids, @supporting_claim_ids, @tags, @scope, @confidence, 'active', @search_summary,
1083
+ VALUES (@id, @title, @description, @l1_content,
1084
+ @l2_core_thesis, @l2_principles, @l2_trigger_conditions, @l2_resolution_steps, @l2_applicable_context,
1085
+ @non_goals, @open_questions,
1086
+ @supporting_episode_ids, @supporting_claim_ids, @evidence_refs, @tags, @scope, @confidence, 'active', @search_summary, @l1_embedding,
795
1087
  @source_tool, @session_id, @client_name, @client_version, @created_at, @updated_at)
796
1088
  `).run({
797
1089
  id,
798
1090
  title: params.title,
799
1091
  description: params.description ?? null,
800
- core_thesis: params.core_thesis ?? null,
801
- principles: JSON.stringify(params.principles),
1092
+ l1_content: params.l1_content ?? null,
1093
+ l2_core_thesis: params.l2_core_thesis ?? null,
1094
+ l2_principles: JSON.stringify(params.l2_principles),
1095
+ l2_trigger_conditions: JSON.stringify(params.l2_trigger_conditions ?? []),
1096
+ l2_resolution_steps: JSON.stringify(params.l2_resolution_steps ?? []),
1097
+ l2_applicable_context: params.l2_applicable_context ?? null,
802
1098
  non_goals: JSON.stringify(params.non_goals),
803
1099
  open_questions: JSON.stringify(params.open_questions),
804
1100
  supporting_episode_ids: JSON.stringify(params.supporting_episode_ids),
805
1101
  supporting_claim_ids: JSON.stringify(params.supporting_claim_ids),
1102
+ evidence_refs: JSON.stringify(params.evidence_refs ?? []),
806
1103
  tags: JSON.stringify(params.tags),
807
1104
  scope: params.scope ?? "global",
808
1105
  confidence: params.confidence ?? 0.8,
809
1106
  search_summary: params.search_summary ?? null,
1107
+ l1_embedding: params.l1_embedding ?? null,
810
1108
  source_tool: params.source_tool ?? null,
811
1109
  session_id: params.session_id ?? null,
812
1110
  client_name: params.client_name ?? null,
@@ -833,7 +1131,7 @@ export function listTheories(db, params) {
833
1131
  values.tag_like = `%"${params.tag}"%`;
834
1132
  }
835
1133
  if (params.scope) {
836
- conditions.push("t.scope = @scope");
1134
+ conditions.push("(t.scope = @scope OR t.scope = 'global')");
837
1135
  values.scope = params.scope;
838
1136
  }
839
1137
  if (sanitized) {
@@ -842,13 +1140,16 @@ export function listTheories(db, params) {
842
1140
  ? "AND " + conditions.join(" AND ")
843
1141
  : "";
844
1142
  try {
845
- return db.prepare(`
1143
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
1144
+ const ftsResults = db.prepare(`
846
1145
  SELECT t.* FROM theories t
847
1146
  JOIN theories_fts f ON t.rowid = f.rowid
848
1147
  WHERE theories_fts MATCH @query ${whereClause}
849
1148
  ORDER BY rank
850
1149
  LIMIT @limit
851
1150
  `).all(values);
1151
+ if (ftsResults.length > 0)
1152
+ return ftsResults;
852
1153
  }
853
1154
  catch {
854
1155
  // FTS5エラー時はLIKEフォールバック
@@ -858,7 +1159,8 @@ export function listTheories(db, params) {
858
1159
  const likeValues = { ...values, limit: params.limit };
859
1160
  delete likeValues.query;
860
1161
  likeValues.like_query = `%${params.query}%`;
861
- likeConditions.push("(t.title LIKE @like_query OR t.description LIKE @like_query OR t.core_thesis LIKE @like_query OR t.principles LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)");
1162
+ // 2026-03-01 修正 (Issue #59): l2_ プレフィックス + l1_content
1163
+ likeConditions.push("(t.title LIKE @like_query OR t.description LIKE @like_query OR t.l1_content LIKE @like_query OR t.l2_core_thesis LIKE @like_query OR t.l2_principles LIKE @like_query OR t.evidence_refs LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)");
862
1164
  const likeWhere = likeConditions.length > 0
863
1165
  ? "WHERE " + likeConditions.join(" AND ")
864
1166
  : "";
@@ -880,7 +1182,7 @@ export function listTheories(db, params) {
880
1182
  values.tag_like = `%"${params.tag}"%`;
881
1183
  }
882
1184
  if (params.scope) {
883
- conditions.push("scope = @scope");
1185
+ conditions.push("(scope = @scope OR scope = 'global')");
884
1186
  values.scope = params.scope;
885
1187
  }
886
1188
  const whereClause = conditions.length > 0
@@ -892,17 +1194,329 @@ export function listTheories(db, params) {
892
1194
  LIMIT @limit
893
1195
  `).all(values);
894
1196
  }
1197
+ // 2026-02-23 追加 (Issue #40 Phase 2): Insight の queryContext 用検索
1198
+ function queryInsightContext(db, topic, scope, limit) {
1199
+ const sanitized = sanitizeFtsQuery(topic);
1200
+ let scopeFilter = "";
1201
+ const scopeValues = {};
1202
+ if (scope) {
1203
+ scopeFilter = "AND (t.scope = @scope OR t.scope = 'global')";
1204
+ scopeValues.scope = scope;
1205
+ }
1206
+ if (sanitized) {
1207
+ try {
1208
+ const ftsResults = db.prepare(`
1209
+ SELECT t.* FROM insights t
1210
+ JOIN insights_fts f ON t.rowid = f.rowid
1211
+ WHERE insights_fts MATCH @query
1212
+ AND t.status = 'active'
1213
+ ${scopeFilter}
1214
+ ORDER BY rank
1215
+ LIMIT @limit
1216
+ `).all({ query: sanitized, limit, ...scopeValues });
1217
+ if (ftsResults.length > 0)
1218
+ return ftsResults;
1219
+ }
1220
+ catch {
1221
+ // FTS5エラー時はLIKEフォールバック
1222
+ }
1223
+ }
1224
+ const likeQuery = `%${topic}%`;
1225
+ // 2026-03-01 修正 (Issue #53): l1/l2 カラム名に対応
1226
+ return db.prepare(`
1227
+ SELECT t.* FROM insights t
1228
+ WHERE t.status = 'active'
1229
+ AND (t.title LIKE @like_query OR t.description LIKE @like_query OR t.l1_content LIKE @like_query OR t.l2_core_thesis LIKE @like_query OR t.l2_principles LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)
1230
+ ${scopeFilter}
1231
+ ORDER BY t.updated_at DESC
1232
+ LIMIT @limit
1233
+ `).all({ like_query: likeQuery, limit, ...scopeValues });
1234
+ }
1235
+ // 2026-02-23 追加 (Issue #40 Phase 2): Model の queryContext 用検索
1236
+ function queryModelContext(db, topic, scope, limit) {
1237
+ const sanitized = sanitizeFtsQuery(topic);
1238
+ let scopeFilter = "";
1239
+ const scopeValues = {};
1240
+ if (scope) {
1241
+ scopeFilter = "AND (t.scope = @scope OR t.scope = 'global')";
1242
+ scopeValues.scope = scope;
1243
+ }
1244
+ if (sanitized) {
1245
+ try {
1246
+ const ftsResults = db.prepare(`
1247
+ SELECT t.* FROM models t
1248
+ JOIN models_fts f ON t.rowid = f.rowid
1249
+ WHERE models_fts MATCH @query
1250
+ AND t.status = 'active'
1251
+ ${scopeFilter}
1252
+ ORDER BY rank
1253
+ LIMIT @limit
1254
+ `).all({ query: sanitized, limit, ...scopeValues });
1255
+ if (ftsResults.length > 0)
1256
+ return ftsResults;
1257
+ }
1258
+ catch {
1259
+ // FTS5エラー時はLIKEフォールバック
1260
+ }
1261
+ }
1262
+ const likeQuery = `%${topic}%`;
1263
+ return db.prepare(`
1264
+ SELECT t.* FROM models t
1265
+ WHERE t.status = 'active'
1266
+ AND (t.title LIKE @like_query OR t.description LIKE @like_query OR t.l1_content LIKE @like_query OR t.l2_core_thesis LIKE @like_query OR t.l2_principles LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)
1267
+ ${scopeFilter}
1268
+ ORDER BY t.updated_at DESC
1269
+ LIMIT @limit
1270
+ `).all({ like_query: likeQuery, limit, ...scopeValues });
1271
+ }
1272
+ // 2026-02-23 追加 (Issue #39 Part A): ユーザープロンプトに関連する教訓・原則を横断検索
1273
+ // 2026-02-23 修正 (Issue #41 検証): description/core_thesis を返すよう拡張(L1レンダリング改善)
1274
+ export function searchPrinciplesForRecall(db, promptText, limit) {
1275
+ const results = [];
1276
+ const querySnippet = promptText.slice(0, 100);
1277
+ const sanitized = sanitizeFtsQuery(querySnippet);
1278
+ // 1. Episode検索: FTS5 → LIKEフォールバック(queryEpisodeContext と同様のパターン)
1279
+ let episodeRows = [];
1280
+ if (sanitized) {
1281
+ try {
1282
+ episodeRows = db.prepare(`
1283
+ SELECT e.* FROM episodes e
1284
+ JOIN episodes_fts f ON e.rowid = f.rowid
1285
+ WHERE episodes_fts MATCH @query
1286
+ AND e.status = 'active'
1287
+ AND e.l2_principles != '[]'
1288
+ ORDER BY rank
1289
+ LIMIT @limit
1290
+ `).all({ query: sanitized, limit });
1291
+ }
1292
+ catch {
1293
+ // FTS5エラー時はLIKEフォールバック
1294
+ }
1295
+ }
1296
+ if (episodeRows.length === 0) {
1297
+ const likeQuery = `%${querySnippet}%`;
1298
+ episodeRows = db.prepare(`
1299
+ SELECT e.* FROM episodes e
1300
+ WHERE e.status = 'active'
1301
+ AND e.l2_principles != '[]'
1302
+ AND (e.title LIKE @like_query OR e.l1_content LIKE @like_query OR e.l2_context LIKE @like_query OR e.l2_trigger LIKE @like_query OR e.l2_principles LIKE @like_query OR e.l2_problems LIKE @like_query OR e.l2_desires LIKE @like_query OR e.l2_outcomes LIKE @like_query OR e.l2_decisions LIKE @like_query OR e.search_summary LIKE @like_query)
1303
+ ORDER BY e.updated_at DESC
1304
+ LIMIT @limit
1305
+ `).all({ like_query: likeQuery, limit });
1306
+ }
1307
+ for (const ep of episodeRows) {
1308
+ try {
1309
+ const parsed = JSON.parse(ep.l2_principles);
1310
+ if (Array.isArray(parsed) && parsed.length > 0) {
1311
+ results.push({ source: "episode", title: ep.title, principles: parsed, created_at: ep.created_at });
1312
+ }
1313
+ }
1314
+ catch { /* JSON パース失敗はスキップ */ }
1315
+ }
1316
+ // 2026-02-23 修正 (Codex P2指摘): LIKE検索を単語分割 OR 条件に改善
1317
+ // 元の実装: promptText全体を1つの LIKE パターンにしていたため、通常のプロンプトでヒットしにくかった
1318
+ // 変更: 3文字以上のトークンごとに個別 LIKE 条件を OR で結合
1319
+ const tokens = querySnippet.split(/\s+/).filter(t => t.length >= 3);
1320
+ // 2026-03-01 修正 (Issue #59): Theory/Model用フィールドを l2_ プレフィックスに更新 + l1_content
1321
+ // 元の実装: core_thesis, principles(プレフィックスなし)
1322
+ const theoryFields = ["title", "description", "l1_content", "l2_core_thesis", "l2_principles", "tags", "search_summary"];
1323
+ // 2026-03-01 修正 (Issue #53): Insight用フィールド(l1_content, l2_core_thesis, l2_principles)
1324
+ const insightFields = ["title", "description", "l1_content", "l2_core_thesis", "l2_principles", "tags", "search_summary"];
1325
+ const buildLikeClause = (fields) => {
1326
+ const conditions = [];
1327
+ const values = { limit };
1328
+ if (tokens.length > 0) {
1329
+ for (let i = 0; i < tokens.length; i++) {
1330
+ const paramName = `like_t${i}`;
1331
+ values[paramName] = `%${tokens[i]}%`;
1332
+ const fieldOrs = fields.map(f => `${f} LIKE @${paramName}`).join(" OR ");
1333
+ conditions.push(`(${fieldOrs})`);
1334
+ }
1335
+ }
1336
+ else {
1337
+ values.like_query = `%${querySnippet}%`;
1338
+ const fieldOrs = fields.map(f => `${f} LIKE @like_query`).join(" OR ");
1339
+ conditions.push(`(${fieldOrs})`);
1340
+ }
1341
+ return { clause: conditions.join(" OR "), values };
1342
+ };
1343
+ const insightLike = buildLikeClause(insightFields);
1344
+ const theoryLike = buildLikeClause(theoryFields);
1345
+ // 2. Insight検索: 単語分割 LIKE
1346
+ try {
1347
+ const insightRows = db.prepare(`
1348
+ SELECT * FROM insights
1349
+ WHERE status = 'active'
1350
+ AND l2_principles != '[]'
1351
+ AND (${insightLike.clause})
1352
+ ORDER BY updated_at DESC
1353
+ LIMIT @limit
1354
+ `).all(insightLike.values);
1355
+ for (const ins of insightRows) {
1356
+ try {
1357
+ const parsed = JSON.parse(ins.l2_principles);
1358
+ if (Array.isArray(parsed) && parsed.length > 0) {
1359
+ results.push({ source: "insight", title: ins.title, principles: parsed, created_at: ins.created_at });
1360
+ }
1361
+ }
1362
+ catch { /* JSON パース失敗はスキップ */ }
1363
+ }
1364
+ }
1365
+ catch { /* テーブル未存在等はスキップ */ }
1366
+ // 3. Theory検索: 単語分割 LIKE
1367
+ try {
1368
+ const theoryRows = db.prepare(`
1369
+ SELECT * FROM theories
1370
+ WHERE status = 'active'
1371
+ AND (l2_principles != '[]' OR open_questions != '[]')
1372
+ AND (${theoryLike.clause})
1373
+ ORDER BY updated_at DESC
1374
+ LIMIT @limit
1375
+ `).all(theoryLike.values);
1376
+ for (const th of theoryRows) {
1377
+ try {
1378
+ const parsed = JSON.parse(th.l2_principles);
1379
+ if (Array.isArray(parsed) && parsed.length > 0) {
1380
+ // 2026-02-23 修正 (Issue #41 検証): description を含める(Theory ドキュメントポインタ用)
1381
+ results.push({ source: "theory", title: th.title, principles: parsed, created_at: th.created_at, description: th.description ?? undefined });
1382
+ }
1383
+ }
1384
+ catch { /* JSON パース失敗はスキップ */ }
1385
+ }
1386
+ }
1387
+ catch { /* テーブル未存在等はスキップ */ }
1388
+ // created_at DESC でソートし limit 件に切り詰め
1389
+ results.sort((a, b) => b.created_at.localeCompare(a.created_at));
1390
+ // 2026-02-23 修正 (Issue #41 検証): description/core_thesis を保持
1391
+ return results.slice(0, limit).map(({ source, title, principles, description, core_thesis }) => ({ source, title, principles, description, core_thesis }));
1392
+ }
1393
+ // 2026-02-23 追加 (Issue #39 Part D): プロファイルサマリー用 — principles 非空のアクティブ Episode を取得
1394
+ // 2026-02-23 修正 (Codex P2指摘): scope フィルタを追加
1395
+ export function listEpisodesWithPrinciples(db, limit, scope) {
1396
+ let scopeFilter = "";
1397
+ const values = { limit };
1398
+ if (scope) {
1399
+ scopeFilter = "AND (scope = @scope OR scope = 'global')";
1400
+ values.scope = scope;
1401
+ }
1402
+ return db.prepare(`
1403
+ SELECT * FROM episodes
1404
+ WHERE status = 'active' AND l2_principles != '[]'
1405
+ ${scopeFilter}
1406
+ ORDER BY updated_at DESC
1407
+ LIMIT @limit
1408
+ `).all(values);
1409
+ }
1410
+ export function listEscalationCandidates(db, params) {
1411
+ const allResults = [];
1412
+ const scopeFilter = params.scope ? "AND (scope = @scope OR scope = 'global')" : "";
1413
+ const baseValues = { limit: params.limit };
1414
+ if (params.scope)
1415
+ baseValues.scope = params.scope;
1416
+ // Insight テーブル
1417
+ try {
1418
+ // 2026-02-23 修正 (Issue #41 検証): description を追加(L1レンダリング改善)
1419
+ // 2026-03-01 修正 (Issue #53): principles → l2_principles
1420
+ const insightRows = db.prepare(`
1421
+ SELECT id, 'insight' as entity_type, title, confidence, l2_principles,
1422
+ json_array_length(supporting_episode_ids) as supporting_episode_count,
1423
+ scope, created_at, description
1424
+ FROM insights
1425
+ WHERE status = 'active'
1426
+ AND confidence >= 0.8
1427
+ AND l2_principles != '[]'
1428
+ AND json_array_length(supporting_episode_ids) >= 2
1429
+ ${scopeFilter}
1430
+ ORDER BY confidence DESC, updated_at DESC
1431
+ LIMIT @limit
1432
+ `).all(baseValues);
1433
+ for (const row of insightRows) {
1434
+ try {
1435
+ const parsed = JSON.parse(row.l2_principles);
1436
+ if (Array.isArray(parsed) && parsed.length > 0) {
1437
+ allResults.push({ ...row, entity_type: "insight", l2_principles: parsed, description: row.description ?? undefined });
1438
+ }
1439
+ }
1440
+ catch { /* JSON パース失敗はスキップ */ }
1441
+ }
1442
+ }
1443
+ catch { /* テーブル未存在等はスキップ */ }
1444
+ // Theory テーブル
1445
+ try {
1446
+ // 2026-02-23 修正 (Issue #41 検証): description を追加(ドキュメントポインタ用)
1447
+ // 2026-03-01 修正 (Issue #59): principles → l2_principles
1448
+ const theoryRows = db.prepare(`
1449
+ SELECT id, 'theory' as entity_type, title, confidence, l2_principles,
1450
+ json_array_length(supporting_episode_ids) as supporting_episode_count,
1451
+ scope, created_at, description
1452
+ FROM theories
1453
+ WHERE status = 'active'
1454
+ AND confidence >= 0.8
1455
+ AND l2_principles != '[]'
1456
+ AND json_array_length(supporting_episode_ids) >= 2
1457
+ ${scopeFilter}
1458
+ ORDER BY confidence DESC, updated_at DESC
1459
+ LIMIT @limit
1460
+ `).all(baseValues);
1461
+ for (const row of theoryRows) {
1462
+ try {
1463
+ const parsed = JSON.parse(row.l2_principles);
1464
+ if (Array.isArray(parsed) && parsed.length > 0) {
1465
+ allResults.push({ ...row, entity_type: "theory", l2_principles: parsed, description: row.description ?? undefined });
1466
+ }
1467
+ }
1468
+ catch { /* JSON パース失敗はスキップ */ }
1469
+ }
1470
+ }
1471
+ catch { /* テーブル未存在等はスキップ */ }
1472
+ // Model テーブル
1473
+ try {
1474
+ // 2026-02-23 修正 (Issue #41 検証): core_thesis を追加(ルールの背景理由表示用)
1475
+ // 2026-03-01 修正 (Issue #59): principles → l2_principles, core_thesis → l2_core_thesis
1476
+ const modelRows = db.prepare(`
1477
+ SELECT id, 'model' as entity_type, title, confidence, l2_principles,
1478
+ json_array_length(supporting_episode_ids) as supporting_episode_count,
1479
+ scope, created_at, l2_core_thesis
1480
+ FROM models
1481
+ WHERE status = 'active'
1482
+ AND confidence >= 0.8
1483
+ AND l2_principles != '[]'
1484
+ AND json_array_length(supporting_episode_ids) >= 2
1485
+ ${scopeFilter}
1486
+ ORDER BY confidence DESC, updated_at DESC
1487
+ LIMIT @limit
1488
+ `).all(baseValues);
1489
+ for (const row of modelRows) {
1490
+ try {
1491
+ const parsed = JSON.parse(row.l2_principles);
1492
+ if (Array.isArray(parsed) && parsed.length > 0) {
1493
+ allResults.push({ ...row, entity_type: "model", l2_principles: parsed, l2_core_thesis: row.l2_core_thesis ?? undefined });
1494
+ }
1495
+ }
1496
+ catch { /* JSON パース失敗はスキップ */ }
1497
+ }
1498
+ }
1499
+ catch { /* テーブル未存在等はスキップ */ }
1500
+ // confidence DESC, created_at DESC でソートし limit 件に切り詰め
1501
+ allResults.sort((a, b) => {
1502
+ if (b.confidence !== a.confidence)
1503
+ return b.confidence - a.confidence;
1504
+ return b.created_at.localeCompare(a.created_at);
1505
+ });
1506
+ return allResults.slice(0, params.limit);
1507
+ }
895
1508
  function queryTheoryContext(db, topic, scope, limit) {
896
1509
  const sanitized = sanitizeFtsQuery(topic);
897
1510
  let scopeFilter = "";
898
1511
  const scopeValues = {};
899
1512
  if (scope) {
900
- scopeFilter = "AND t.scope = @scope";
1513
+ scopeFilter = "AND (t.scope = @scope OR t.scope = 'global')";
901
1514
  scopeValues.scope = scope;
902
1515
  }
903
1516
  if (sanitized) {
904
1517
  try {
905
- return db.prepare(`
1518
+ // 2026-02-22 改善 (Issue #20): FTS空結果時もLIKEフォールバックに進む
1519
+ const ftsResults = db.prepare(`
906
1520
  SELECT t.* FROM theories t
907
1521
  JOIN theories_fts f ON t.rowid = f.rowid
908
1522
  WHERE theories_fts MATCH @query
@@ -911,6 +1525,8 @@ function queryTheoryContext(db, topic, scope, limit) {
911
1525
  ORDER BY rank
912
1526
  LIMIT @limit
913
1527
  `).all({ query: sanitized, limit, ...scopeValues });
1528
+ if (ftsResults.length > 0)
1529
+ return ftsResults;
914
1530
  }
915
1531
  catch {
916
1532
  // FTS5エラー時はLIKEフォールバック
@@ -920,10 +1536,1172 @@ function queryTheoryContext(db, topic, scope, limit) {
920
1536
  return db.prepare(`
921
1537
  SELECT t.* FROM theories t
922
1538
  WHERE t.status = 'active'
923
- AND (t.title LIKE @like_query OR t.description LIKE @like_query OR t.core_thesis LIKE @like_query OR t.principles LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)
1539
+ AND (t.title LIKE @like_query OR t.description LIKE @like_query OR t.l1_content LIKE @like_query OR t.l2_core_thesis LIKE @like_query OR t.l2_principles LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)
924
1540
  ${scopeFilter}
925
1541
  ORDER BY t.updated_at DESC
926
1542
  LIMIT @limit
927
1543
  `).all({ like_query: likeQuery, limit, ...scopeValues });
928
1544
  }
1545
+ // === Issue #17: Stale Claim レビューキュー ===
1546
+ /**
1547
+ * 陳腐化した Claim を検出してスコア付きで返す。
1548
+ * SQL で active claim + claim_checks 集計を取得し、
1549
+ * TypeScript 側で陳腐化スコアを計算してソート・フィルタする。
1550
+ */
1551
+ export function getStaleClaims(db, params) {
1552
+ const conditions = ["c.status = 'active'"];
1553
+ const values = {};
1554
+ if (params.scope) {
1555
+ conditions.push("(c.scope = @scope OR c.scope = 'global')");
1556
+ values.scope = params.scope;
1557
+ }
1558
+ if (params.category) {
1559
+ conditions.push("c.category = @category");
1560
+ values.category = params.category;
1561
+ }
1562
+ const whereClause = conditions.join(" AND ");
1563
+ // active claim + claim_checks 集計を LEFT JOIN で取得
1564
+ const rows = db.prepare(`
1565
+ SELECT
1566
+ c.*,
1567
+ COALESCE(cc.check_count, 0) AS check_count,
1568
+ cc.last_check_at,
1569
+ COALESCE(cc.has_failed, 0) AS has_failed,
1570
+ COALESCE(cc.has_falsifier_eval, 0) AS has_falsifier_eval
1571
+ FROM claims c
1572
+ LEFT JOIN (
1573
+ SELECT
1574
+ claim_id,
1575
+ COUNT(*) AS check_count,
1576
+ MAX(checked_at) AS last_check_at,
1577
+ MAX(CASE WHEN result = 'failed' THEN 1 ELSE 0 END) AS has_failed,
1578
+ MAX(CASE WHEN check_type = 'falsifier_eval' THEN 1 ELSE 0 END) AS has_falsifier_eval
1579
+ FROM claim_checks
1580
+ GROUP BY claim_id
1581
+ ) cc ON c.id = cc.claim_id
1582
+ WHERE ${whereClause}
1583
+ `).all(values);
1584
+ const staleRows = [];
1585
+ for (const row of rows) {
1586
+ const reasons = [];
1587
+ let score = 0;
1588
+ // score_decayed: computeClaimScore < 0.5(30日以上経過)→ 最大30点
1589
+ const claimScore = computeClaimScore(row.confidence, row.updated_at);
1590
+ if (claimScore < 0.5) {
1591
+ reasons.push("score_decayed");
1592
+ // スコアが低いほど高い陳腐化スコア(0に近いほど30点に近い)
1593
+ score += Math.round(30 * (1 - claimScore));
1594
+ }
1595
+ // low_confidence: confidence < 0.5 → 最大20点
1596
+ if (row.confidence < 0.5) {
1597
+ reasons.push("low_confidence");
1598
+ score += Math.round(20 * (1 - row.confidence));
1599
+ }
1600
+ // never_hit: hit_count === 0 → 10点
1601
+ if (row.hit_count === 0) {
1602
+ reasons.push("never_hit");
1603
+ score += 10;
1604
+ }
1605
+ // no_evidence: evidence === null → 10点
1606
+ if (row.evidence === null) {
1607
+ reasons.push("no_evidence");
1608
+ score += 10;
1609
+ }
1610
+ // no_checks: 検証0件 → 15点
1611
+ if (row.check_count === 0) {
1612
+ reasons.push("no_checks");
1613
+ score += 15;
1614
+ }
1615
+ // falsifier_unevaluated: falsifier設定済みだが falsifier_eval 未実施 → 20点
1616
+ if (row.falsifier && row.has_falsifier_eval === 0) {
1617
+ reasons.push("falsifier_unevaluated");
1618
+ score += 20;
1619
+ }
1620
+ // check_failed: 過去に検証失敗あり → 25点
1621
+ if (row.has_failed === 1) {
1622
+ reasons.push("check_failed");
1623
+ score += 25;
1624
+ }
1625
+ // 理由が1つもないClaimはスキップ(健全)
1626
+ if (reasons.length === 0)
1627
+ continue;
1628
+ // suggested_action を理由に応じて決定
1629
+ let suggestedAction;
1630
+ if (reasons.includes("check_failed")) {
1631
+ suggestedAction = "retract_or_update";
1632
+ }
1633
+ else if (reasons.includes("falsifier_unevaluated")) {
1634
+ suggestedAction = "evaluate_falsifier";
1635
+ }
1636
+ else if (reasons.includes("score_decayed") && reasons.includes("low_confidence")) {
1637
+ suggestedAction = "review_and_confirm";
1638
+ }
1639
+ else if (reasons.includes("score_decayed")) {
1640
+ suggestedAction = "confirm_or_update";
1641
+ }
1642
+ else if (reasons.includes("no_evidence")) {
1643
+ suggestedAction = "add_evidence";
1644
+ }
1645
+ else {
1646
+ suggestedAction = "review";
1647
+ }
1648
+ staleRows.push({
1649
+ id: row.id,
1650
+ subject: row.subject,
1651
+ predicate: row.predicate,
1652
+ object: row.object,
1653
+ category: row.category,
1654
+ scope: row.scope,
1655
+ confidence: row.confidence,
1656
+ evidence: row.evidence,
1657
+ falsifier: row.falsifier,
1658
+ hit_count: row.hit_count,
1659
+ updated_at: row.updated_at,
1660
+ staleness_score: score,
1661
+ staleness_reasons: reasons,
1662
+ suggested_action: suggestedAction,
1663
+ last_check_at: row.last_check_at,
1664
+ check_count: row.check_count,
1665
+ });
1666
+ }
1667
+ // staleness_score 降順でソート → limit で切り詰め
1668
+ staleRows.sort((a, b) => b.staleness_score - a.staleness_score);
1669
+ return staleRows.slice(0, params.limit);
1670
+ }
1671
+ /**
1672
+ * Hook用: 未統合アイテム数をカウント。
1673
+ * 最新の Theory/Insight/Model 作成日時以降に作成された
1674
+ * active な Claim + Decision + Episode の合計件数を返す。
1675
+ * Theory/Insight/Model が1件もない場合は全アクティブアイテムの合計を返す。
1676
+ */
1677
+ export function countUnconsolidatedItems(db) {
1678
+ // 最後の Theory/Insight/Model 作成日時を取得
1679
+ let lastAt = null;
1680
+ try {
1681
+ const row = db.prepare(`
1682
+ SELECT MAX(created_at) AS last_at FROM (
1683
+ SELECT created_at FROM theories WHERE status = 'active'
1684
+ UNION ALL
1685
+ SELECT created_at FROM insights WHERE status = 'active'
1686
+ UNION ALL
1687
+ SELECT created_at FROM models WHERE status = 'active'
1688
+ )
1689
+ `).get();
1690
+ lastAt = row?.last_at ?? null;
1691
+ }
1692
+ catch {
1693
+ // insights/models テーブルが存在しない古いスキーマではフォールバック
1694
+ }
1695
+ if (lastAt) {
1696
+ const result = db.prepare(`
1697
+ SELECT (
1698
+ (SELECT COUNT(*) FROM claims WHERE status = 'active' AND created_at > @since) +
1699
+ (SELECT COUNT(*) FROM decisions WHERE status = 'active' AND created_at > @since) +
1700
+ (SELECT COUNT(*) FROM episodes WHERE status = 'active' AND created_at > @since)
1701
+ ) AS count
1702
+ `).get({ since: lastAt });
1703
+ return result.count;
1704
+ }
1705
+ // Theory/Insight/Model が一件もない場合: 全アクティブアイテム合計
1706
+ const result = db.prepare(`
1707
+ SELECT (
1708
+ (SELECT COUNT(*) FROM claims WHERE status = 'active') +
1709
+ (SELECT COUNT(*) FROM decisions WHERE status = 'active') +
1710
+ (SELECT COUNT(*) FROM episodes WHERE status = 'active')
1711
+ ) AS count
1712
+ `).get();
1713
+ return result.count;
1714
+ }
1715
+ /**
1716
+ * Hook用軽量版: 30日以上更新されていない active claim の件数を返す
1717
+ */
1718
+ export function countStaleClaims(db) {
1719
+ const result = db.prepare(`
1720
+ SELECT COUNT(*) AS count FROM claims
1721
+ WHERE status = 'active'
1722
+ AND julianday('now') - julianday(updated_at) > 30
1723
+ `).get();
1724
+ return result.count;
1725
+ }
1726
+ /**
1727
+ * 検証通過時の鮮度リセット: updated_at を現在時刻に更新する
1728
+ */
1729
+ export function touchClaimUpdatedAt(db, id) {
1730
+ const now = new Date().toISOString();
1731
+ db.prepare("UPDATE claims SET updated_at = @now WHERE id = @id").run({ now, id });
1732
+ }
1733
+ // === Issue #29: Embedding CRUD ===
1734
+ /** Claim の l1_embedding を更新 */
1735
+ export function updateClaimL1Embedding(db, id, l1_embedding) {
1736
+ db.prepare("UPDATE claims SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
1737
+ }
1738
+ /** Decision の l1_embedding を更新 */
1739
+ export function updateDecisionL1Embedding(db, id, l1_embedding) {
1740
+ db.prepare("UPDATE decisions SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
1741
+ }
1742
+ /** Episode の l1_embedding を更新 */
1743
+ export function updateEpisodeL1Embedding(db, id, l1_embedding) {
1744
+ db.prepare("UPDATE episodes SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
1745
+ }
1746
+ /** Theory の l1_embedding を更新 */
1747
+ export function updateTheoryL1Embedding(db, id, l1_embedding) {
1748
+ db.prepare("UPDATE theories SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
1749
+ }
1750
+ /** l1_embedding が NULL の active Claim を取得(バックフィル用) */
1751
+ export function getClaimsWithoutL1Embedding(db, limit) {
1752
+ return db.prepare("SELECT * FROM claims WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1753
+ }
1754
+ /** l1_embedding が NULL の active Decision を取得(バックフィル用) */
1755
+ export function getDecisionsWithoutL1Embedding(db, limit) {
1756
+ return db.prepare("SELECT * FROM decisions WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1757
+ }
1758
+ /** l1_embedding が NULL の active Episode を取得(バックフィル用) */
1759
+ export function getEpisodesWithoutL1Embedding(db, limit) {
1760
+ return db.prepare("SELECT * FROM episodes WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1761
+ }
1762
+ /** l1_embedding が NULL の active Theory を取得(バックフィル用) */
1763
+ export function getTheoriesWithoutL1Embedding(db, limit) {
1764
+ return db.prepare("SELECT * FROM theories WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1765
+ }
1766
+ /** 指定テーブルで l1_embedding が存在するレコードを取得 */
1767
+ export function getClaimsWithL1Embedding(db, scope, limit = 100) {
1768
+ if (scope) {
1769
+ return db.prepare("SELECT * FROM claims WHERE status = 'active' AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1770
+ }
1771
+ return db.prepare("SELECT * FROM claims WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1772
+ }
1773
+ export function getDecisionsWithL1Embedding(db, scope, limit = 100) {
1774
+ if (scope) {
1775
+ return db.prepare("SELECT * FROM decisions WHERE status = 'active' AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1776
+ }
1777
+ return db.prepare("SELECT * FROM decisions WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1778
+ }
1779
+ export function getEpisodesWithL1Embedding(db, scope, limit = 100) {
1780
+ if (scope) {
1781
+ return db.prepare("SELECT * FROM episodes WHERE status = 'active' AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1782
+ }
1783
+ return db.prepare("SELECT * FROM episodes WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1784
+ }
1785
+ export function getTheoriesWithL1Embedding(db, scope, limit = 100) {
1786
+ if (scope) {
1787
+ return db.prepare("SELECT * FROM theories WHERE status = 'active' AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1788
+ }
1789
+ return db.prepare("SELECT * FROM theories WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1790
+ }
1791
+ // === V12: Insight CRUD (Issue #40) ===
1792
+ // 2026-03-01 修正 (Issue #53): l1/l2 カラム拡張 — core_thesis → l2_core_thesis, principles → l2_principles, 新カラム4つ追加
1793
+ export function insertInsight(db, params) {
1794
+ const id = ulid();
1795
+ const now = new Date().toISOString();
1796
+ db.prepare(`
1797
+ INSERT INTO insights (id, title, description,
1798
+ l1_content,
1799
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
1800
+ tags, scope, confidence, search_summary, l1_embedding,
1801
+ supporting_episode_ids, supporting_claim_ids, evidence_refs,
1802
+ status,
1803
+ source_tool, session_id, client_name, client_version, created_at, updated_at)
1804
+ VALUES (@id, @title, @description,
1805
+ @l1_content,
1806
+ @l2_core_thesis, @l2_principles, @l2_trigger_conditions, @l2_resolution_steps, @l2_applicable_context,
1807
+ @tags, @scope, @confidence, @search_summary, @l1_embedding,
1808
+ @supporting_episode_ids, @supporting_claim_ids, @evidence_refs,
1809
+ 'active',
1810
+ @source_tool, @session_id, @client_name, @client_version, @created_at, @updated_at)
1811
+ `).run({
1812
+ id,
1813
+ title: params.title,
1814
+ description: params.description ?? null,
1815
+ l1_content: params.l1_content ?? null,
1816
+ l2_core_thesis: params.l2_core_thesis ?? null,
1817
+ l2_principles: JSON.stringify(params.l2_principles),
1818
+ l2_trigger_conditions: JSON.stringify(params.l2_trigger_conditions ?? []),
1819
+ l2_resolution_steps: JSON.stringify(params.l2_resolution_steps ?? []),
1820
+ l2_applicable_context: params.l2_applicable_context ?? null,
1821
+ supporting_episode_ids: JSON.stringify(params.supporting_episode_ids),
1822
+ supporting_claim_ids: JSON.stringify(params.supporting_claim_ids),
1823
+ evidence_refs: JSON.stringify(params.evidence_refs ?? []),
1824
+ tags: JSON.stringify(params.tags),
1825
+ scope: params.scope ?? "global",
1826
+ confidence: params.confidence ?? 0.8,
1827
+ search_summary: params.search_summary ?? null,
1828
+ l1_embedding: params.l1_embedding ?? null,
1829
+ source_tool: params.source_tool ?? null,
1830
+ session_id: params.session_id ?? null,
1831
+ client_name: params.client_name ?? null,
1832
+ client_version: params.client_version ?? null,
1833
+ created_at: now,
1834
+ updated_at: now,
1835
+ });
1836
+ return getInsightById(db, id);
1837
+ }
1838
+ export function getInsightById(db, id) {
1839
+ return db.prepare("SELECT * FROM insights WHERE id = ?").get(id);
1840
+ }
1841
+ export function listInsights(db, params) {
1842
+ if (params.query) {
1843
+ const sanitized = sanitizeFtsQuery(params.query);
1844
+ const conditions = [];
1845
+ const values = { limit: params.limit };
1846
+ if (params.status) {
1847
+ conditions.push("t.status = @status");
1848
+ values.status = params.status;
1849
+ }
1850
+ if (params.tag) {
1851
+ conditions.push("t.tags LIKE @tag_like");
1852
+ values.tag_like = `%"${params.tag}"%`;
1853
+ }
1854
+ if (params.scope) {
1855
+ conditions.push("(t.scope = @scope OR t.scope = 'global')");
1856
+ values.scope = params.scope;
1857
+ }
1858
+ if (sanitized) {
1859
+ values.query = sanitized;
1860
+ const whereClause = conditions.length > 0
1861
+ ? "AND " + conditions.join(" AND ")
1862
+ : "";
1863
+ try {
1864
+ const ftsResults = db.prepare(`
1865
+ SELECT t.* FROM insights t
1866
+ JOIN insights_fts f ON t.rowid = f.rowid
1867
+ WHERE insights_fts MATCH @query ${whereClause}
1868
+ ORDER BY rank
1869
+ LIMIT @limit
1870
+ `).all(values);
1871
+ if (ftsResults.length > 0)
1872
+ return ftsResults;
1873
+ }
1874
+ catch {
1875
+ // FTS5エラー時はLIKEフォールバック
1876
+ }
1877
+ }
1878
+ const likeConditions = [...conditions];
1879
+ const likeValues = { ...values, limit: params.limit };
1880
+ delete likeValues.query;
1881
+ likeValues.like_query = `%${params.query}%`;
1882
+ // 2026-03-01 修正 (Issue #53): l1/l2 カラム名に対応
1883
+ likeConditions.push("(t.title LIKE @like_query OR t.description LIKE @like_query OR t.l1_content LIKE @like_query OR t.l2_core_thesis LIKE @like_query OR t.l2_principles LIKE @like_query OR t.evidence_refs LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)");
1884
+ const likeWhere = likeConditions.length > 0
1885
+ ? "WHERE " + likeConditions.join(" AND ")
1886
+ : "";
1887
+ return db.prepare(`
1888
+ SELECT t.* FROM insights t
1889
+ ${likeWhere}
1890
+ ORDER BY t.updated_at DESC
1891
+ LIMIT @limit
1892
+ `).all(likeValues);
1893
+ }
1894
+ const conditions = [];
1895
+ const values = { limit: params.limit };
1896
+ if (params.status) {
1897
+ conditions.push("status = @status");
1898
+ values.status = params.status;
1899
+ }
1900
+ if (params.tag) {
1901
+ conditions.push("tags LIKE @tag_like");
1902
+ values.tag_like = `%"${params.tag}"%`;
1903
+ }
1904
+ if (params.scope) {
1905
+ conditions.push("(scope = @scope OR scope = 'global')");
1906
+ values.scope = params.scope;
1907
+ }
1908
+ const whereClause = conditions.length > 0
1909
+ ? "WHERE " + conditions.join(" AND ")
1910
+ : "";
1911
+ return db.prepare(`
1912
+ SELECT * FROM insights ${whereClause}
1913
+ ORDER BY created_at DESC
1914
+ LIMIT @limit
1915
+ `).all(values);
1916
+ }
1917
+ /** Insight の l1_embedding を更新 */
1918
+ export function updateInsightL1Embedding(db, id, l1_embedding) {
1919
+ db.prepare("UPDATE insights SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
1920
+ }
1921
+ /** l1_embedding が NULL の active Insight を取得(バックフィル用) */
1922
+ export function getInsightsWithoutL1Embedding(db, limit) {
1923
+ return db.prepare("SELECT * FROM insights WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1924
+ }
1925
+ export function getInsightsWithL1Embedding(db, scope, limit = 100) {
1926
+ if (scope) {
1927
+ return db.prepare("SELECT * FROM insights WHERE status = 'active' AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1928
+ }
1929
+ return db.prepare("SELECT * FROM insights WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1930
+ }
1931
+ // === V12: Model CRUD (Issue #40) ===
1932
+ // 2026-03-01 修正 (Issue #59): core_thesis → l2_core_thesis, principles → l2_principles + 新l2カラム + l1_content
1933
+ // 元の実装: core_thesis, principles(プレフィックスなし)
1934
+ export function insertModel(db, params) {
1935
+ const id = ulid();
1936
+ const now = new Date().toISOString();
1937
+ db.prepare(`
1938
+ INSERT INTO models (id, title, description, l1_content,
1939
+ l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
1940
+ non_goals, open_questions,
1941
+ supporting_episode_ids, supporting_claim_ids, evidence_refs, tags, scope, confidence, status, search_summary, l1_embedding,
1942
+ source_tool, session_id, client_name, client_version, created_at, updated_at)
1943
+ VALUES (@id, @title, @description, @l1_content,
1944
+ @l2_core_thesis, @l2_principles, @l2_trigger_conditions, @l2_resolution_steps, @l2_applicable_context,
1945
+ @non_goals, @open_questions,
1946
+ @supporting_episode_ids, @supporting_claim_ids, @evidence_refs, @tags, @scope, @confidence, 'active', @search_summary, @l1_embedding,
1947
+ @source_tool, @session_id, @client_name, @client_version, @created_at, @updated_at)
1948
+ `).run({
1949
+ id,
1950
+ title: params.title,
1951
+ description: params.description ?? null,
1952
+ l1_content: params.l1_content ?? null,
1953
+ l2_core_thesis: params.l2_core_thesis ?? null,
1954
+ l2_principles: JSON.stringify(params.l2_principles),
1955
+ l2_trigger_conditions: JSON.stringify(params.l2_trigger_conditions ?? []),
1956
+ l2_resolution_steps: JSON.stringify(params.l2_resolution_steps ?? []),
1957
+ l2_applicable_context: params.l2_applicable_context ?? null,
1958
+ non_goals: JSON.stringify(params.non_goals),
1959
+ open_questions: JSON.stringify(params.open_questions),
1960
+ supporting_episode_ids: JSON.stringify(params.supporting_episode_ids),
1961
+ supporting_claim_ids: JSON.stringify(params.supporting_claim_ids),
1962
+ evidence_refs: JSON.stringify(params.evidence_refs ?? []),
1963
+ tags: JSON.stringify(params.tags),
1964
+ scope: params.scope ?? "global",
1965
+ confidence: params.confidence ?? 0.8,
1966
+ search_summary: params.search_summary ?? null,
1967
+ l1_embedding: params.l1_embedding ?? null,
1968
+ source_tool: params.source_tool ?? null,
1969
+ session_id: params.session_id ?? null,
1970
+ client_name: params.client_name ?? null,
1971
+ client_version: params.client_version ?? null,
1972
+ created_at: now,
1973
+ updated_at: now,
1974
+ });
1975
+ return getModelById(db, id);
1976
+ }
1977
+ export function getModelById(db, id) {
1978
+ return db.prepare("SELECT * FROM models WHERE id = ?").get(id);
1979
+ }
1980
+ export function listModels(db, params) {
1981
+ if (params.query) {
1982
+ const sanitized = sanitizeFtsQuery(params.query);
1983
+ const conditions = [];
1984
+ const values = { limit: params.limit };
1985
+ if (params.status) {
1986
+ conditions.push("t.status = @status");
1987
+ values.status = params.status;
1988
+ }
1989
+ if (params.tag) {
1990
+ conditions.push("t.tags LIKE @tag_like");
1991
+ values.tag_like = `%"${params.tag}"%`;
1992
+ }
1993
+ if (params.scope) {
1994
+ conditions.push("(t.scope = @scope OR t.scope = 'global')");
1995
+ values.scope = params.scope;
1996
+ }
1997
+ if (sanitized) {
1998
+ values.query = sanitized;
1999
+ const whereClause = conditions.length > 0
2000
+ ? "AND " + conditions.join(" AND ")
2001
+ : "";
2002
+ try {
2003
+ const ftsResults = db.prepare(`
2004
+ SELECT t.* FROM models t
2005
+ JOIN models_fts f ON t.rowid = f.rowid
2006
+ WHERE models_fts MATCH @query ${whereClause}
2007
+ ORDER BY rank
2008
+ LIMIT @limit
2009
+ `).all(values);
2010
+ if (ftsResults.length > 0)
2011
+ return ftsResults;
2012
+ }
2013
+ catch {
2014
+ // FTS5エラー時はLIKEフォールバック
2015
+ }
2016
+ }
2017
+ const likeConditions = [...conditions];
2018
+ const likeValues = { ...values, limit: params.limit };
2019
+ delete likeValues.query;
2020
+ likeValues.like_query = `%${params.query}%`;
2021
+ // 2026-03-01 修正 (Issue #59): l2_ プレフィックス + l1_content
2022
+ likeConditions.push("(t.title LIKE @like_query OR t.description LIKE @like_query OR t.l1_content LIKE @like_query OR t.l2_core_thesis LIKE @like_query OR t.l2_principles LIKE @like_query OR t.evidence_refs LIKE @like_query OR t.tags LIKE @like_query OR t.search_summary LIKE @like_query)");
2023
+ const likeWhere = likeConditions.length > 0
2024
+ ? "WHERE " + likeConditions.join(" AND ")
2025
+ : "";
2026
+ return db.prepare(`
2027
+ SELECT t.* FROM models t
2028
+ ${likeWhere}
2029
+ ORDER BY t.updated_at DESC
2030
+ LIMIT @limit
2031
+ `).all(likeValues);
2032
+ }
2033
+ const conditions = [];
2034
+ const values = { limit: params.limit };
2035
+ if (params.status) {
2036
+ conditions.push("status = @status");
2037
+ values.status = params.status;
2038
+ }
2039
+ if (params.tag) {
2040
+ conditions.push("tags LIKE @tag_like");
2041
+ values.tag_like = `%"${params.tag}"%`;
2042
+ }
2043
+ if (params.scope) {
2044
+ conditions.push("(scope = @scope OR scope = 'global')");
2045
+ values.scope = params.scope;
2046
+ }
2047
+ const whereClause = conditions.length > 0
2048
+ ? "WHERE " + conditions.join(" AND ")
2049
+ : "";
2050
+ return db.prepare(`
2051
+ SELECT * FROM models ${whereClause}
2052
+ ORDER BY created_at DESC
2053
+ LIMIT @limit
2054
+ `).all(values);
2055
+ }
2056
+ /** Model の l1_embedding を更新 */
2057
+ export function updateModelL1Embedding(db, id, l1_embedding) {
2058
+ db.prepare("UPDATE models SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2059
+ }
2060
+ /** l1_embedding が NULL の active Model を取得(バックフィル用) */
2061
+ export function getModelsWithoutL1Embedding(db, limit) {
2062
+ return db.prepare("SELECT * FROM models WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2063
+ }
2064
+ export function getModelsWithL1Embedding(db, scope, limit = 100) {
2065
+ if (scope) {
2066
+ return db.prepare("SELECT * FROM models WHERE status = 'active' AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2067
+ }
2068
+ return db.prepare("SELECT * FROM models WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2069
+ }
2070
+ // 2026-03-04 修正 (Issue #54): content → l1_content, user_input 追加
2071
+ export function insertUserMemo(db, params) {
2072
+ const id = ulid();
2073
+ const now = new Date().toISOString();
2074
+ db.prepare(`
2075
+ INSERT INTO user_memos (id, title, l1_content, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, status, created_at, updated_at)
2076
+ VALUES (@id, @title, @l1_content, @usage_policy, @tags, @scope, @search_summary, @user_input, @l1_embedding, @source_tool, @client_name, @client_version, 'active', @created_at, @updated_at)
2077
+ `).run({
2078
+ id,
2079
+ title: params.title,
2080
+ l1_content: params.l1_content,
2081
+ usage_policy: params.usage_policy ?? "on_request",
2082
+ tags: JSON.stringify(params.tags),
2083
+ scope: params.scope ?? "global",
2084
+ search_summary: params.search_summary ?? null,
2085
+ user_input: params.user_input ?? null,
2086
+ l1_embedding: params.l1_embedding ?? null,
2087
+ source_tool: params.source_tool ?? null,
2088
+ client_name: params.client_name ?? null,
2089
+ client_version: params.client_version ?? null,
2090
+ created_at: now,
2091
+ updated_at: now,
2092
+ });
2093
+ return db.prepare("SELECT * FROM user_memos WHERE id = ?").get(id);
2094
+ }
2095
+ export function getUserMemoById(db, id) {
2096
+ return db.prepare("SELECT * FROM user_memos WHERE id = ?").get(id);
2097
+ }
2098
+ // 2026-03-04 修正 (Issue #54): content → l1_content
2099
+ export function updateUserMemo(db, id, updates) {
2100
+ const existing = getUserMemoById(db, id);
2101
+ if (!existing)
2102
+ return undefined;
2103
+ const setClauses = [];
2104
+ const values = { id };
2105
+ if (updates.title !== undefined) {
2106
+ setClauses.push("title = @title");
2107
+ values.title = updates.title;
2108
+ }
2109
+ if (updates.l1_content !== undefined) {
2110
+ setClauses.push("l1_content = @l1_content");
2111
+ values.l1_content = updates.l1_content;
2112
+ }
2113
+ if (updates.usage_policy !== undefined) {
2114
+ setClauses.push("usage_policy = @usage_policy");
2115
+ values.usage_policy = updates.usage_policy;
2116
+ }
2117
+ if (updates.tags !== undefined) {
2118
+ setClauses.push("tags = @tags");
2119
+ values.tags = JSON.stringify(updates.tags);
2120
+ }
2121
+ if (updates.scope !== undefined) {
2122
+ setClauses.push("scope = @scope");
2123
+ values.scope = updates.scope;
2124
+ }
2125
+ if (updates.search_summary !== undefined) {
2126
+ setClauses.push("search_summary = @search_summary");
2127
+ values.search_summary = updates.search_summary;
2128
+ }
2129
+ if (updates.status !== undefined) {
2130
+ setClauses.push("status = @status");
2131
+ values.status = updates.status;
2132
+ }
2133
+ if (setClauses.length === 0)
2134
+ return existing;
2135
+ setClauses.push("updated_at = @updated_at");
2136
+ values.updated_at = new Date().toISOString();
2137
+ db.prepare(`UPDATE user_memos SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
2138
+ return db.prepare("SELECT * FROM user_memos WHERE id = ?").get(id);
2139
+ }
2140
+ export function listUserMemos(db, params) {
2141
+ const sanitized = params.query ? sanitizeFtsQuery(params.query) : null;
2142
+ const limit = params.limit ?? 20;
2143
+ const conditions = [];
2144
+ const values = { limit };
2145
+ if (params.status) {
2146
+ conditions.push("m.status = @status");
2147
+ values.status = params.status;
2148
+ }
2149
+ if (params.usage_policy) {
2150
+ conditions.push("m.usage_policy = @usage_policy");
2151
+ values.usage_policy = params.usage_policy;
2152
+ }
2153
+ if (params.tag) {
2154
+ conditions.push("m.tags LIKE @tag_like");
2155
+ values.tag_like = `%"${params.tag}"%`;
2156
+ }
2157
+ if (params.scope) {
2158
+ conditions.push("(m.scope = @scope OR m.scope = 'global')");
2159
+ values.scope = params.scope;
2160
+ }
2161
+ // FTS5検索
2162
+ if (sanitized) {
2163
+ try {
2164
+ const ftsConditions = [...conditions];
2165
+ const ftsWhere = ftsConditions.length > 0 ? "AND " + ftsConditions.join(" AND ") : "";
2166
+ const ftsResults = db.prepare(`
2167
+ SELECT m.* FROM user_memos m
2168
+ JOIN user_memos_fts f ON m.rowid = f.rowid
2169
+ WHERE user_memos_fts MATCH @query ${ftsWhere}
2170
+ ORDER BY rank
2171
+ LIMIT @limit
2172
+ `).all({ ...values, query: sanitized });
2173
+ if (ftsResults.length > 0)
2174
+ return ftsResults;
2175
+ }
2176
+ catch { /* FTSエラー時はLIKEフォールバック */ }
2177
+ }
2178
+ // LIKEフォールバック or 全件取得
2179
+ if (params.query) {
2180
+ const likeParam = `%${params.query.slice(0, 100)}%`;
2181
+ conditions.push("(m.title LIKE @like_query OR m.l1_content LIKE @like_query OR m.search_summary LIKE @like_query)");
2182
+ values.like_query = likeParam;
2183
+ }
2184
+ const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
2185
+ return db.prepare(`
2186
+ SELECT m.* FROM user_memos m ${whereClause}
2187
+ ORDER BY m.updated_at DESC
2188
+ LIMIT @limit
2189
+ `).all(values);
2190
+ }
2191
+ export function listAutoUserMemos(db, scope, limit = 5) {
2192
+ if (scope) {
2193
+ return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND usage_policy = 'auto' AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2194
+ }
2195
+ return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2196
+ }
2197
+ export function updateUserMemoL1Embedding(db, id, l1_embedding) {
2198
+ db.prepare("UPDATE user_memos SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2199
+ }
2200
+ export function getUserMemosWithoutL1Embedding(db, limit) {
2201
+ return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2202
+ }
2203
+ export function getUserMemosWithL1Embedding(db, scope, limit = 100) {
2204
+ if (scope) {
2205
+ return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2206
+ }
2207
+ return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2208
+ }
2209
+ // 2026-03-04 修正 (Issue #54): content → l1_content
2210
+ export function insertUserPlan(db, params) {
2211
+ const id = ulid();
2212
+ const now = new Date().toISOString();
2213
+ db.prepare(`
2214
+ INSERT INTO user_plans (id, title, l1_content, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, status, created_at, updated_at)
2215
+ VALUES (@id, @title, @l1_content, @usage_policy, @tags, @scope, @search_summary, @user_input, @l1_embedding, @source_tool, @client_name, @client_version, 'active', @created_at, @updated_at)
2216
+ `).run({
2217
+ id,
2218
+ title: params.title,
2219
+ l1_content: params.l1_content,
2220
+ usage_policy: params.usage_policy ?? "auto",
2221
+ tags: JSON.stringify(params.tags),
2222
+ scope: params.scope ?? "global",
2223
+ search_summary: params.search_summary ?? null,
2224
+ user_input: params.user_input ?? null,
2225
+ l1_embedding: params.l1_embedding ?? null,
2226
+ source_tool: params.source_tool ?? null,
2227
+ client_name: params.client_name ?? null,
2228
+ client_version: params.client_version ?? null,
2229
+ created_at: now,
2230
+ updated_at: now,
2231
+ });
2232
+ return db.prepare("SELECT * FROM user_plans WHERE id = ?").get(id);
2233
+ }
2234
+ export function getUserPlanById(db, id) {
2235
+ return db.prepare("SELECT * FROM user_plans WHERE id = ?").get(id);
2236
+ }
2237
+ // 2026-03-04 修正 (Issue #54): content → l1_content
2238
+ export function updateUserPlan(db, id, updates) {
2239
+ const existing = getUserPlanById(db, id);
2240
+ if (!existing)
2241
+ return undefined;
2242
+ const setClauses = [];
2243
+ const values = { id };
2244
+ if (updates.title !== undefined) {
2245
+ setClauses.push("title = @title");
2246
+ values.title = updates.title;
2247
+ }
2248
+ if (updates.l1_content !== undefined) {
2249
+ setClauses.push("l1_content = @l1_content");
2250
+ values.l1_content = updates.l1_content;
2251
+ }
2252
+ if (updates.usage_policy !== undefined) {
2253
+ setClauses.push("usage_policy = @usage_policy");
2254
+ values.usage_policy = updates.usage_policy;
2255
+ }
2256
+ if (updates.tags !== undefined) {
2257
+ setClauses.push("tags = @tags");
2258
+ values.tags = JSON.stringify(updates.tags);
2259
+ }
2260
+ if (updates.scope !== undefined) {
2261
+ setClauses.push("scope = @scope");
2262
+ values.scope = updates.scope;
2263
+ }
2264
+ if (updates.search_summary !== undefined) {
2265
+ setClauses.push("search_summary = @search_summary");
2266
+ values.search_summary = updates.search_summary;
2267
+ }
2268
+ if (updates.status !== undefined) {
2269
+ setClauses.push("status = @status");
2270
+ values.status = updates.status;
2271
+ }
2272
+ if (setClauses.length === 0)
2273
+ return existing;
2274
+ setClauses.push("updated_at = @updated_at");
2275
+ values.updated_at = new Date().toISOString();
2276
+ db.prepare(`UPDATE user_plans SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
2277
+ return db.prepare("SELECT * FROM user_plans WHERE id = ?").get(id);
2278
+ }
2279
+ export function listUserPlans(db, params) {
2280
+ const sanitized = params.query ? sanitizeFtsQuery(params.query) : null;
2281
+ const limit = params.limit ?? 20;
2282
+ const conditions = [];
2283
+ const values = { limit };
2284
+ if (params.status) {
2285
+ conditions.push("m.status = @status");
2286
+ values.status = params.status;
2287
+ }
2288
+ if (params.usage_policy) {
2289
+ conditions.push("m.usage_policy = @usage_policy");
2290
+ values.usage_policy = params.usage_policy;
2291
+ }
2292
+ if (params.tag) {
2293
+ conditions.push("m.tags LIKE @tag_like");
2294
+ values.tag_like = `%"${params.tag}"%`;
2295
+ }
2296
+ if (params.scope) {
2297
+ conditions.push("(m.scope = @scope OR m.scope = 'global')");
2298
+ values.scope = params.scope;
2299
+ }
2300
+ // FTS5検索
2301
+ if (sanitized) {
2302
+ try {
2303
+ const ftsConditions = [...conditions];
2304
+ const ftsWhere = ftsConditions.length > 0 ? "AND " + ftsConditions.join(" AND ") : "";
2305
+ const ftsResults = db.prepare(`
2306
+ SELECT m.* FROM user_plans m
2307
+ JOIN user_plans_fts f ON m.rowid = f.rowid
2308
+ WHERE user_plans_fts MATCH @query ${ftsWhere}
2309
+ ORDER BY rank
2310
+ LIMIT @limit
2311
+ `).all({ ...values, query: sanitized });
2312
+ if (ftsResults.length > 0)
2313
+ return ftsResults;
2314
+ }
2315
+ catch { /* FTSエラー時はLIKEフォールバック */ }
2316
+ }
2317
+ // LIKEフォールバック or 全件取得
2318
+ if (params.query) {
2319
+ const likeParam = `%${params.query.slice(0, 100)}%`;
2320
+ conditions.push("(m.title LIKE @like_query OR m.l1_content LIKE @like_query OR m.search_summary LIKE @like_query)");
2321
+ values.like_query = likeParam;
2322
+ }
2323
+ const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
2324
+ return db.prepare(`
2325
+ SELECT m.* FROM user_plans m ${whereClause}
2326
+ ORDER BY m.updated_at DESC
2327
+ LIMIT @limit
2328
+ `).all(values);
2329
+ }
2330
+ export function listAutoUserPlans(db, scope, limit = 10) {
2331
+ if (scope) {
2332
+ return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND usage_policy = 'auto' AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2333
+ }
2334
+ return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2335
+ }
2336
+ export function updateUserPlanL1Embedding(db, id, l1_embedding) {
2337
+ db.prepare("UPDATE user_plans SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2338
+ }
2339
+ export function getUserPlansWithoutL1Embedding(db, limit) {
2340
+ return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2341
+ }
2342
+ export function getUserPlansWithL1Embedding(db, scope, limit = 100) {
2343
+ if (scope) {
2344
+ return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2345
+ }
2346
+ return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2347
+ }
2348
+ // === Issue #57: UserIssue CRUD ===
2349
+ // 2026-03-04 修正 (Issue #54): content → l1_content
2350
+ export function insertUserIssue(db, params) {
2351
+ const id = ulid();
2352
+ const now = new Date().toISOString();
2353
+ db.prepare(`
2354
+ INSERT INTO user_issues (id, title, l1_content, entries, priority, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, status, created_at, updated_at)
2355
+ VALUES (@id, @title, @l1_content, @entries, @priority, @usage_policy, @tags, @scope, @search_summary, @user_input, @l1_embedding, @source_tool, @client_name, @client_version, 'open', @created_at, @updated_at)
2356
+ `).run({
2357
+ id,
2358
+ title: params.title,
2359
+ l1_content: params.l1_content,
2360
+ entries: params.entries ?? "[]",
2361
+ priority: params.priority ?? "medium",
2362
+ usage_policy: params.usage_policy ?? "on_request",
2363
+ tags: JSON.stringify(params.tags),
2364
+ scope: params.scope ?? "global",
2365
+ search_summary: params.search_summary ?? null,
2366
+ user_input: params.user_input ?? null,
2367
+ l1_embedding: params.l1_embedding ?? null,
2368
+ source_tool: params.source_tool ?? null,
2369
+ client_name: params.client_name ?? null,
2370
+ client_version: params.client_version ?? null,
2371
+ created_at: now,
2372
+ updated_at: now,
2373
+ });
2374
+ return db.prepare("SELECT * FROM user_issues WHERE id = ?").get(id);
2375
+ }
2376
+ export function getUserIssueById(db, id) {
2377
+ return db.prepare("SELECT * FROM user_issues WHERE id = ?").get(id);
2378
+ }
2379
+ // 2026-03-04 修正 (Issue #54): content → l1_content
2380
+ export function updateUserIssue(db, id, updates) {
2381
+ const existing = getUserIssueById(db, id);
2382
+ if (!existing)
2383
+ return undefined;
2384
+ const setClauses = [];
2385
+ const values = { id };
2386
+ if (updates.title !== undefined) {
2387
+ setClauses.push("title = @title");
2388
+ values.title = updates.title;
2389
+ }
2390
+ if (updates.l1_content !== undefined) {
2391
+ setClauses.push("l1_content = @l1_content");
2392
+ values.l1_content = updates.l1_content;
2393
+ }
2394
+ if (updates.entries !== undefined) {
2395
+ setClauses.push("entries = @entries");
2396
+ values.entries = updates.entries;
2397
+ }
2398
+ if (updates.priority !== undefined) {
2399
+ setClauses.push("priority = @priority");
2400
+ values.priority = updates.priority;
2401
+ }
2402
+ if (updates.usage_policy !== undefined) {
2403
+ setClauses.push("usage_policy = @usage_policy");
2404
+ values.usage_policy = updates.usage_policy;
2405
+ }
2406
+ if (updates.tags !== undefined) {
2407
+ setClauses.push("tags = @tags");
2408
+ values.tags = JSON.stringify(updates.tags);
2409
+ }
2410
+ if (updates.scope !== undefined) {
2411
+ setClauses.push("scope = @scope");
2412
+ values.scope = updates.scope;
2413
+ }
2414
+ if (updates.search_summary !== undefined) {
2415
+ setClauses.push("search_summary = @search_summary");
2416
+ values.search_summary = updates.search_summary;
2417
+ }
2418
+ if (updates.status !== undefined) {
2419
+ setClauses.push("status = @status");
2420
+ values.status = updates.status;
2421
+ }
2422
+ if (setClauses.length === 0)
2423
+ return existing;
2424
+ setClauses.push("updated_at = @updated_at");
2425
+ values.updated_at = new Date().toISOString();
2426
+ db.prepare(`UPDATE user_issues SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
2427
+ return db.prepare("SELECT * FROM user_issues WHERE id = ?").get(id);
2428
+ }
2429
+ export function listUserIssues(db, params) {
2430
+ const sanitized = params.query ? sanitizeFtsQuery(params.query) : null;
2431
+ const limit = params.limit ?? 20;
2432
+ const conditions = [];
2433
+ const values = { limit };
2434
+ if (params.status) {
2435
+ conditions.push("m.status = @status");
2436
+ values.status = params.status;
2437
+ }
2438
+ if (params.priority) {
2439
+ conditions.push("m.priority = @priority");
2440
+ values.priority = params.priority;
2441
+ }
2442
+ if (params.usage_policy) {
2443
+ conditions.push("m.usage_policy = @usage_policy");
2444
+ values.usage_policy = params.usage_policy;
2445
+ }
2446
+ if (params.tag) {
2447
+ conditions.push("m.tags LIKE @tag_like");
2448
+ values.tag_like = `%"${params.tag}"%`;
2449
+ }
2450
+ if (params.scope) {
2451
+ conditions.push("(m.scope = @scope OR m.scope = 'global')");
2452
+ values.scope = params.scope;
2453
+ }
2454
+ // FTS5検索
2455
+ if (sanitized) {
2456
+ try {
2457
+ const ftsConditions = [...conditions];
2458
+ const ftsWhere = ftsConditions.length > 0 ? "AND " + ftsConditions.join(" AND ") : "";
2459
+ const ftsResults = db.prepare(`
2460
+ SELECT m.* FROM user_issues m
2461
+ JOIN user_issues_fts f ON m.rowid = f.rowid
2462
+ WHERE user_issues_fts MATCH @query ${ftsWhere}
2463
+ ORDER BY rank
2464
+ LIMIT @limit
2465
+ `).all({ ...values, query: sanitized });
2466
+ if (ftsResults.length > 0)
2467
+ return ftsResults;
2468
+ }
2469
+ catch { /* FTSエラー時はLIKEフォールバック */ }
2470
+ }
2471
+ // LIKEフォールバック or 全件取得
2472
+ if (params.query) {
2473
+ const likeParam = `%${params.query.slice(0, 100)}%`;
2474
+ conditions.push("(m.title LIKE @like_query OR m.l1_content LIKE @like_query OR m.search_summary LIKE @like_query)");
2475
+ values.like_query = likeParam;
2476
+ }
2477
+ const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
2478
+ return db.prepare(`
2479
+ SELECT m.* FROM user_issues m ${whereClause}
2480
+ ORDER BY m.updated_at DESC
2481
+ LIMIT @limit
2482
+ `).all(values);
2483
+ }
2484
+ export function updateUserIssueL1Embedding(db, id, l1_embedding) {
2485
+ db.prepare("UPDATE user_issues SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2486
+ }
2487
+ export function getUserIssuesWithoutL1Embedding(db, limit) {
2488
+ return db.prepare("SELECT * FROM user_issues WHERE status = 'open' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2489
+ }
2490
+ export function getUserIssuesWithL1Embedding(db, scope, limit = 100) {
2491
+ if (scope) {
2492
+ return db.prepare("SELECT * FROM user_issues WHERE status = 'open' AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2493
+ }
2494
+ return db.prepare("SELECT * FROM user_issues WHERE status = 'open' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2495
+ }
2496
+ /**
2497
+ * 横断検索(FTS5 + LIKEフォールバック)
2498
+ * unified_search_items テーブルを使い、全6エンティティを横断してFTS5検索する。
2499
+ * FTS5で結果が得られない場合はLIKEフォールバック(既存パターンと同一)。
2500
+ */
2501
+ export function unifiedSearch(db, query, scope, entityTypes, limit) {
2502
+ const sanitized = sanitizeFtsQuery(query);
2503
+ let scopeFilter = "";
2504
+ const baseValues = { limit };
2505
+ if (scope) {
2506
+ scopeFilter = "AND (u.scope = @scope OR u.scope = 'global')";
2507
+ baseValues.scope = scope;
2508
+ }
2509
+ let typeFilter = "";
2510
+ if (entityTypes && entityTypes.length > 0) {
2511
+ const placeholders = entityTypes.map((_, i) => `@et${i}`).join(", ");
2512
+ typeFilter = `AND u.entity_type IN (${placeholders})`;
2513
+ entityTypes.forEach((t, i) => { baseValues[`et${i}`] = t; });
2514
+ }
2515
+ // 1. FTS5検索
2516
+ if (sanitized) {
2517
+ try {
2518
+ const ftsResults = db.prepare(`
2519
+ SELECT u.*, rank AS _fts_rank FROM unified_search_items u
2520
+ JOIN unified_search_fts f ON u.rowid = f.rowid
2521
+ WHERE unified_search_fts MATCH @query
2522
+ ${scopeFilter}
2523
+ ${typeFilter}
2524
+ ORDER BY rank
2525
+ LIMIT @limit
2526
+ `).all({ ...baseValues, query: sanitized });
2527
+ if (ftsResults.length > 0) {
2528
+ return ftsResults;
2529
+ }
2530
+ }
2531
+ catch {
2532
+ // FTS5エラー時はLIKEフォールバック
2533
+ }
2534
+ }
2535
+ // 2. LIKEフォールバック(単語分割OR条件 — searchPrinciplesForRecall と同一パターン)
2536
+ const querySnippet = query.slice(0, 100);
2537
+ const fields = ["title_summary", "search_text", "search_summary"];
2538
+ const tokens = querySnippet.split(/\s+/).filter(t => t.length >= 3);
2539
+ const likeValues = { ...baseValues };
2540
+ const likeConditions = [];
2541
+ if (tokens.length > 0) {
2542
+ for (let i = 0; i < tokens.length; i++) {
2543
+ const param = `like_t${i}`;
2544
+ likeValues[param] = `%${tokens[i]}%`;
2545
+ const fieldOrs = fields.map(f => `u.${f} LIKE @${param}`).join(" OR ");
2546
+ likeConditions.push(`(${fieldOrs})`);
2547
+ }
2548
+ }
2549
+ else {
2550
+ likeValues.like_query = `%${querySnippet}%`;
2551
+ const fieldOrs = fields.map(f => `u.${f} LIKE @like_query`).join(" OR ");
2552
+ likeConditions.push(`(${fieldOrs})`);
2553
+ }
2554
+ const likeWhere = likeConditions.join(" OR ");
2555
+ return db.prepare(`
2556
+ SELECT u.*, NULL AS _fts_rank FROM unified_search_items u
2557
+ WHERE (${likeWhere})
2558
+ ${scopeFilter}
2559
+ ${typeFilter}
2560
+ ORDER BY u.updated_at DESC
2561
+ LIMIT @limit
2562
+ `).all(likeValues);
2563
+ }
2564
+ /**
2565
+ * 横断検索テーブルからl1_embedding付きレコードを取得(ベクトル検索用)
2566
+ */
2567
+ export function getUnifiedSearchWithL1Embedding(db, scope, entityTypes, limit) {
2568
+ const values = { limit };
2569
+ const conditions = ["l1_embedding IS NOT NULL"];
2570
+ if (scope) {
2571
+ conditions.push("(scope = @scope OR scope = 'global')");
2572
+ values.scope = scope;
2573
+ }
2574
+ if (entityTypes && entityTypes.length > 0) {
2575
+ const placeholders = entityTypes.map((_, i) => `@et${i}`).join(", ");
2576
+ conditions.push(`entity_type IN (${placeholders})`);
2577
+ entityTypes.forEach((t, i) => { values[`et${i}`] = t; });
2578
+ }
2579
+ const whereClause = conditions.length > 0
2580
+ ? "WHERE " + conditions.join(" AND ")
2581
+ : "";
2582
+ return db.prepare(`
2583
+ SELECT *, NULL AS _fts_rank FROM unified_search_items
2584
+ ${whereClause}
2585
+ ORDER BY updated_at DESC
2586
+ LIMIT @limit
2587
+ `).all(values);
2588
+ }
2589
+ /**
2590
+ * 横断検索テーブルの全件再構築
2591
+ * 全ソーステーブルからアクティブレコードを取得し、unified_search_items を再構築する。
2592
+ * マイグレーション後の整合性チェックや手動メンテナンスに使用。
2593
+ */
2594
+ export function rebuildUnifiedSearch(db) {
2595
+ const jc = (col) => `replace(replace(replace(${col}, '["',''), '"]',''), '","',' ')`;
2596
+ const coal = (col) => `COALESCE(${col}, '')`;
2597
+ const columns = "entity_type, entity_id, scope, category, title_summary, search_text, search_summary, tags, created_at, updated_at";
2598
+ const entitySqls = [
2599
+ {
2600
+ type: "claim", table: "claims",
2601
+ categoryExpr: "category",
2602
+ titleExpr: "subject || ' ' || predicate || ' ' || object",
2603
+ searchExpr: `subject || ' ' || predicate || ' ' || object || ' ' || ${coal("evidence")} || ' ' || ${coal("falsifier")} || ' ' || ${coal("search_summary")}`,
2604
+ searchSummary: "search_summary",
2605
+ tagsExpr: "'[]'",
2606
+ },
2607
+ // 2026-03-01 修正 (Issue #59): l1/l2 カラム名に対応
2608
+ {
2609
+ type: "episode", table: "episodes",
2610
+ categoryExpr: "NULL",
2611
+ titleExpr: "title",
2612
+ searchExpr: `title || ' ' || ${coal("l1_content")} || ' ' || ${coal("l2_context")} || ' ' || ${coal("l2_trigger")} || ' ' || ${jc("l2_problems")} || ' ' || ${jc("l2_desires")} || ' ' || ${jc("l2_decisions")} || ' ' || ${jc("l2_outcomes")} || ' ' || ${jc("l2_principles")} || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")}`,
2613
+ searchSummary: "search_summary",
2614
+ tagsExpr: "tags",
2615
+ },
2616
+ // 2026-03-01 修正 (Issue #59): l1/l2 カラム名に対応
2617
+ {
2618
+ type: "decision", table: "decisions",
2619
+ categoryExpr: "NULL",
2620
+ titleExpr: "title",
2621
+ searchExpr: `title || ' ' || description || ' ' || ${coal("l1_content")} || ' ' || l2_reasoning || ' ' || ${jc("l2_alternatives")} || ' ' || ${coal("search_summary")}`,
2622
+ searchSummary: "search_summary",
2623
+ tagsExpr: "'[]'",
2624
+ },
2625
+ // 2026-03-01 修正 (Issue #59): l1/l2 カラム名に対応
2626
+ {
2627
+ type: "theory", table: "theories",
2628
+ categoryExpr: "NULL",
2629
+ titleExpr: "title",
2630
+ searchExpr: `title || ' ' || ${coal("description")} || ' ' || ${coal("l1_content")} || ' ' || ${coal("l2_core_thesis")} || ' ' || ${jc("l2_principles")} || ' ' || ${jc("evidence_refs")} || ' ' || ${jc("non_goals")} || ' ' || ${jc("open_questions")} || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")}`,
2631
+ searchSummary: "search_summary",
2632
+ tagsExpr: "tags",
2633
+ },
2634
+ // 2026-03-01 修正 (Issue #53): l1/l2 カラム名に対応
2635
+ {
2636
+ type: "insight", table: "insights",
2637
+ categoryExpr: "NULL",
2638
+ titleExpr: "title",
2639
+ searchExpr: `title || ' ' || ${coal("description")} || ' ' || ${coal("l1_content")} || ' ' || ${coal("l2_core_thesis")} || ' ' || ${jc("l2_principles")} || ' ' || ${jc("evidence_refs")} || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")}`,
2640
+ searchSummary: "search_summary",
2641
+ tagsExpr: "tags",
2642
+ },
2643
+ // 2026-03-01 修正 (Issue #59): l1/l2 カラム名に対応
2644
+ {
2645
+ type: "model", table: "models",
2646
+ categoryExpr: "NULL",
2647
+ titleExpr: "title",
2648
+ searchExpr: `title || ' ' || ${coal("description")} || ' ' || ${coal("l1_content")} || ' ' || ${coal("l2_core_thesis")} || ' ' || ${jc("l2_principles")} || ' ' || ${jc("evidence_refs")} || ' ' || ${jc("non_goals")} || ' ' || ${jc("open_questions")} || ' ' || ${jc("tags")} || ' ' || ${coal("search_summary")}`,
2649
+ searchSummary: "search_summary",
2650
+ tagsExpr: "tags",
2651
+ },
2652
+ ];
2653
+ const rebuild = db.transaction(() => {
2654
+ db.exec("DELETE FROM unified_search_items");
2655
+ for (const e of entitySqls) {
2656
+ db.exec(`
2657
+ INSERT OR REPLACE INTO unified_search_items(${columns})
2658
+ SELECT '${e.type}', id, scope, ${e.categoryExpr}, ${e.titleExpr}, ${e.searchExpr}, ${e.searchSummary}, ${e.tagsExpr}, created_at, updated_at
2659
+ FROM ${e.table}
2660
+ WHERE status = 'active'
2661
+ `);
2662
+ }
2663
+ db.exec("INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')");
2664
+ });
2665
+ rebuild();
2666
+ const row = db.prepare("SELECT COUNT(*) as cnt FROM unified_search_items").get();
2667
+ return { total: row.cnt };
2668
+ }
2669
+ // === search_summary 更新 ===
2670
+ /** entity_type → テーブル名マッピング */
2671
+ const ENTITY_TYPE_TO_TABLE = {
2672
+ claim: "claims",
2673
+ decision: "decisions",
2674
+ episode: "episodes",
2675
+ theory: "theories",
2676
+ insight: "insights",
2677
+ model: "models",
2678
+ user_memo: "user_memos",
2679
+ user_plan: "user_plans",
2680
+ user_issue: "user_issues",
2681
+ };
2682
+ /**
2683
+ * entity_id から entity_type を解決する(unified_search_items を使用)。
2684
+ * Issue #60: entity_type 省略時の自動解決に使用。
2685
+ *
2686
+ * 注意: unified_search_items には active 状態のエンティティのみ同期される。
2687
+ * retracted/superseded/promoted 等の非 active エンティティは解決できない。
2688
+ * 非 active エンティティを取得する場合は entity_type を明示指定すること。
2689
+ */
2690
+ export function resolveEntityType(db, entityId) {
2691
+ const row = db.prepare("SELECT entity_type FROM unified_search_items WHERE entity_id = ? LIMIT 1").get(entityId);
2692
+ return row?.entity_type;
2693
+ }
2694
+ /**
2695
+ * 任意エンティティの search_summary を更新する。
2696
+ * UPDATE トリガーにより unified_search_items も自動同期される。
2697
+ * @returns 更新された行数(0 = ID不在)
2698
+ */
2699
+ export function updateSearchSummary(db, entityType, entityId, searchSummary) {
2700
+ const table = ENTITY_TYPE_TO_TABLE[entityType];
2701
+ if (!table)
2702
+ return 0;
2703
+ const now = new Date().toISOString();
2704
+ const result = db.prepare(`UPDATE ${table} SET search_summary = @searchSummary, updated_at = @now WHERE id = @entityId`).run({ searchSummary, now, entityId });
2705
+ return result.changes;
2706
+ }
929
2707
  //# sourceMappingURL=queries-core.js.map