watashi-db 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CLAUDE.md +36 -0
  2. package/LICENSE +1 -1
  3. package/README.md +64 -2
  4. package/cowork-plugin/skills/groom/SKILL.md +51 -15
  5. package/cowork-plugin/skills/recall/SKILL.md +5 -6
  6. package/cowork-plugin/skills/reflect/SKILL.md +4 -4
  7. package/cowork-plugin/skills/remember/SKILL.md +3 -3
  8. package/cowork-plugin/skills/session-start/SKILL.md +3 -3
  9. package/dist/auth/token.d.ts +7 -0
  10. package/dist/auth/token.js +14 -0
  11. package/dist/auth/token.js.map +1 -0
  12. package/dist/config/schema.js +1 -1
  13. package/dist/constants.d.ts +9 -9
  14. package/dist/constants.js +29 -43
  15. package/dist/constants.js.map +1 -1
  16. package/dist/database/archive.js +6 -6
  17. package/dist/database/groom.js +5 -5
  18. package/dist/database/groom.js.map +1 -1
  19. package/dist/database/queries-core.d.ts +109 -5
  20. package/dist/database/queries-core.js +546 -186
  21. package/dist/database/queries-core.js.map +1 -1
  22. package/dist/database/queries.d.ts +85 -5
  23. package/dist/database/queries.js +33 -0
  24. package/dist/database/queries.js.map +1 -1
  25. package/dist/database/schema.d.ts +1 -0
  26. package/dist/database/schema.js +2299 -406
  27. package/dist/database/schema.js.map +1 -1
  28. package/dist/embedding/embed-on-write.d.ts +7 -1
  29. package/dist/embedding/embed-on-write.js +8 -3
  30. package/dist/embedding/embed-on-write.js.map +1 -1
  31. package/dist/hook.js +17 -9
  32. package/dist/hook.js.map +1 -1
  33. package/dist/http-server.d.ts +6 -0
  34. package/dist/http-server.js +235 -0
  35. package/dist/http-server.js.map +1 -0
  36. package/dist/index.js +14 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/resources/config-guide-content.d.ts +1 -1
  39. package/dist/resources/config-guide-content.js +2 -2
  40. package/dist/server-core.d.ts +15 -0
  41. package/dist/server-core.js +113 -0
  42. package/dist/server-core.js.map +1 -0
  43. package/dist/server-instructions.js +27 -24
  44. package/dist/server-instructions.js.map +1 -1
  45. package/dist/server.d.ts +4 -1
  46. package/dist/server.js +9 -103
  47. package/dist/server.js.map +1 -1
  48. package/dist/setup.js +5 -6
  49. package/dist/setup.js.map +1 -1
  50. package/dist/store/federation.d.ts +12 -1
  51. package/dist/store/federation.js +38 -0
  52. package/dist/store/federation.js.map +1 -1
  53. package/dist/store/sync-manager.d.ts +1 -1
  54. package/dist/store/sync-manager.js +9 -9
  55. package/dist/tools/claim-tools.d.ts +1 -1
  56. package/dist/tools/claim-tools.js +30 -17
  57. package/dist/tools/claim-tools.js.map +1 -1
  58. package/dist/tools/decision-tools.d.ts +1 -1
  59. package/dist/tools/decision-tools.js +23 -7
  60. package/dist/tools/decision-tools.js.map +1 -1
  61. package/dist/tools/episode-tools.d.ts +1 -1
  62. package/dist/tools/episode-tools.js +28 -7
  63. package/dist/tools/episode-tools.js.map +1 -1
  64. package/dist/tools/file-tools.d.ts +3 -0
  65. package/dist/tools/file-tools.js +350 -0
  66. package/dist/tools/file-tools.js.map +1 -0
  67. package/dist/tools/get-tools.d.ts +1 -1
  68. package/dist/tools/get-tools.js +39 -5
  69. package/dist/tools/get-tools.js.map +1 -1
  70. package/dist/tools/helpers.d.ts +40 -0
  71. package/dist/tools/helpers.js +69 -0
  72. package/dist/tools/helpers.js.map +1 -1
  73. package/dist/tools/knowledge-tools.d.ts +1 -1
  74. package/dist/tools/knowledge-tools.js +38 -6
  75. package/dist/tools/knowledge-tools.js.map +1 -1
  76. package/dist/tools/maintenance-tools.d.ts +1 -1
  77. package/dist/tools/maintenance-tools.js +95 -52
  78. package/dist/tools/maintenance-tools.js.map +1 -1
  79. package/dist/tools/memo-tools.d.ts +7 -11
  80. package/dist/tools/memo-tools.js +509 -309
  81. package/dist/tools/memo-tools.js.map +1 -1
  82. package/dist/tools/query-tools.d.ts +1 -1
  83. package/dist/tools/query-tools.js +266 -242
  84. package/dist/tools/query-tools.js.map +1 -1
  85. package/dist/types.d.ts +513 -76
  86. package/dist/types.js +185 -33
  87. package/dist/types.js.map +1 -1
  88. package/misc/20260316_110841_groom-recipe.md +483 -0
  89. package/misc/20260316_xaml-testing-library-recipe.md +817 -0
  90. package/misc/20260331_remote-transport-recipe.md +316 -0
  91. package/package.json +4 -2
  92. package/scripts/update-license-version.sh +7 -0
@@ -1,4 +1,5 @@
1
1
  import { ulid } from "ulid";
2
+ import { DEFAULT_SEARCH_ENTITY_TYPES } from "../types.js";
2
3
  import { claimScoreExpression, computeClaimScore } from "../scoring.js";
3
4
  // === exp() サポート検出(SQLite数学関数) ===
4
5
  /** exp() が使えるかを DB ごとにキャッシュ */
@@ -57,8 +58,8 @@ export function insertClaim(db, params) {
57
58
  const id = ulid();
58
59
  const now = new Date().toISOString();
59
60
  db.prepare(`
60
- INSERT INTO claims (id, l2_subject, l2_predicate, l2_object, category, scope, confidence, l2_evidence, l2_falsifier, l1_content, search_summary, l1_embedding, source_tool, source_session, client_name, client_version, status, created_at, updated_at)
61
- VALUES (@id, @l2_subject, @l2_predicate, @l2_object, @category, @scope, @confidence, @l2_evidence, @l2_falsifier, @l1_content, @search_summary, @l1_embedding, @source_tool, @source_session, @client_name, @client_version, 'active', @created_at, @updated_at)
61
+ INSERT INTO claims (id, l2_subject, l2_predicate, l2_object, category, scope, confidence, l2_evidence, l2_falsifier, l1_content, search_summary, l1_embedding, user_input, source_tool, session_id, client_name, client_version, validity_status, created_at, updated_at)
62
+ VALUES (@id, @l2_subject, @l2_predicate, @l2_object, @category, @scope, @confidence, @l2_evidence, @l2_falsifier, @l1_content, @search_summary, @l1_embedding, @user_input, @source_tool, @session_id, @client_name, @client_version, 'active', @created_at, @updated_at)
62
63
  `).run({
63
64
  id,
64
65
  l2_subject: params.subject,
@@ -72,8 +73,9 @@ export function insertClaim(db, params) {
72
73
  l1_content: params.l1_content ?? null,
73
74
  search_summary: params.search_summary ?? null,
74
75
  l1_embedding: params.l1_embedding ?? null,
76
+ user_input: params.user_input ?? null,
75
77
  source_tool: params.source_tool ?? null,
76
- source_session: params.source_session ?? null,
78
+ session_id: params.session_id ?? null,
77
79
  client_name: params.client_name ?? null,
78
80
  client_version: params.client_version ?? null,
79
81
  created_at: now,
@@ -140,20 +142,20 @@ export function retractClaim(db, id, reason) {
140
142
  const existing = getClaimById(db, id);
141
143
  if (!existing)
142
144
  return undefined;
143
- if (existing.status !== "active")
145
+ if (existing.validity_status !== "active")
144
146
  return existing;
145
147
  const now = new Date().toISOString();
146
148
  db.transaction(() => {
147
149
  db.prepare(`
148
150
  INSERT INTO claim_history (id, claim_id, field_name, old_value, new_value, reason, changed_at)
149
- VALUES (@id, @claim_id, 'status', 'active', 'retracted', @reason, @changed_at)
151
+ VALUES (@id, @claim_id, 'validity_status', 'active', 'invalidated', @reason, @changed_at)
150
152
  `).run({
151
153
  id: ulid(),
152
154
  claim_id: id,
153
155
  reason,
154
156
  changed_at: now,
155
157
  });
156
- db.prepare("UPDATE claims SET status = 'retracted', updated_at = @updated_at WHERE id = @id").run({
158
+ db.prepare("UPDATE claims SET validity_status = 'invalidated', updated_at = @updated_at WHERE id = @id").run({
157
159
  id,
158
160
  updated_at: now,
159
161
  });
@@ -165,20 +167,20 @@ export function supersedeClaim(db, oldId, newId, reason) {
165
167
  const newClaim = getClaimById(db, newId);
166
168
  if (!oldClaim || !newClaim)
167
169
  return undefined;
168
- if (oldClaim.status !== "active")
170
+ if (oldClaim.validity_status !== "active")
169
171
  return undefined;
170
172
  const now = new Date().toISOString();
171
173
  db.transaction(() => {
172
174
  db.prepare(`
173
175
  INSERT INTO claim_history (id, claim_id, field_name, old_value, new_value, reason, changed_at)
174
- VALUES (@id, @claim_id, 'status', 'active', 'superseded', @reason, @changed_at)
176
+ VALUES (@id, @claim_id, 'validity_status', 'active', 'superseded', @reason, @changed_at)
175
177
  `).run({
176
178
  id: ulid(),
177
179
  claim_id: oldId,
178
180
  reason,
179
181
  changed_at: now,
180
182
  });
181
- db.prepare("UPDATE claims SET status = 'superseded', updated_at = @updated_at WHERE id = @id").run({
183
+ db.prepare("UPDATE claims SET validity_status = 'superseded', updated_at = @updated_at WHERE id = @id").run({
182
184
  id: oldId,
183
185
  updated_at: now,
184
186
  });
@@ -202,12 +204,12 @@ export function updateDecisionStatus(db, id, newStatus, reason) {
202
204
  const existing = getDecisionById(db, id);
203
205
  if (!existing)
204
206
  return undefined;
205
- if (existing.status !== "active")
207
+ if (existing.validity_status !== "active")
206
208
  return existing;
207
209
  const now = new Date().toISOString();
208
- db.prepare("UPDATE decisions SET status = @status, updated_at = @updated_at WHERE id = @id").run({
210
+ db.prepare("UPDATE decisions SET validity_status = @validity_status, updated_at = @updated_at WHERE id = @id").run({
209
211
  id,
210
- status: newStatus,
212
+ validity_status: newStatus,
211
213
  updated_at: now,
212
214
  });
213
215
  return getDecisionById(db, id);
@@ -223,8 +225,10 @@ export function searchClaims(db, params) {
223
225
  const conditions = [];
224
226
  const values = { limit: params.limit };
225
227
  if (params.status) {
226
- conditions.push("c.status = @status");
227
- values.status = params.status;
228
+ conditions.push("c.validity_status = @validity_status");
229
+ conditions.push("c.is_archived = 0");
230
+ conditions.push("c.promoted_to IS NULL");
231
+ values.validity_status = params.status;
228
232
  }
229
233
  if (params.category) {
230
234
  conditions.push("c.category = @category");
@@ -273,8 +277,10 @@ export function searchClaims(db, params) {
273
277
  const conditions = [];
274
278
  const values = { limit: params.limit };
275
279
  if (params.status) {
276
- conditions.push("status = @status");
277
- values.status = params.status;
280
+ conditions.push("validity_status = @validity_status");
281
+ conditions.push("is_archived = 0");
282
+ conditions.push("promoted_to IS NULL");
283
+ values.validity_status = params.status;
278
284
  }
279
285
  if (params.category) {
280
286
  conditions.push("category = @category");
@@ -422,7 +428,7 @@ function queryClaimContext(db, topic, scope, limit) {
422
428
  SELECT c.* FROM claims c
423
429
  JOIN claims_fts f ON c.rowid = f.rowid
424
430
  WHERE claims_fts MATCH @query
425
- AND c.status = 'active'
431
+ AND c.validity_status = 'active' AND c.is_archived = 0 AND c.promoted_to IS NULL
426
432
  ${scopeFilter}
427
433
  ORDER BY rank
428
434
  LIMIT @limit
@@ -437,7 +443,7 @@ function queryClaimContext(db, topic, scope, limit) {
437
443
  const likeQuery = `%${topic}%`;
438
444
  return db.prepare(`
439
445
  SELECT c.* FROM claims c
440
- WHERE c.status = 'active'
446
+ WHERE c.validity_status = 'active' AND c.is_archived = 0 AND c.promoted_to IS NULL
441
447
  AND (c.l2_subject LIKE @like_query OR c.l2_predicate LIKE @like_query OR c.l2_object LIKE @like_query OR c.l2_evidence LIKE @like_query OR c.search_summary LIKE @like_query)
442
448
  ${scopeFilter}
443
449
  ORDER BY c.updated_at DESC
@@ -459,7 +465,7 @@ function queryEpisodeContext(db, topic, scope, limit) {
459
465
  SELECT e.* FROM episodes e
460
466
  JOIN episodes_fts f ON e.rowid = f.rowid
461
467
  WHERE episodes_fts MATCH @query
462
- AND e.status = 'active'
468
+ AND e.validity_status = 'active' AND e.is_archived = 0 AND e.promoted_to IS NULL
463
469
  ${scopeFilter}
464
470
  ORDER BY rank
465
471
  LIMIT @limit
@@ -474,7 +480,7 @@ function queryEpisodeContext(db, topic, scope, limit) {
474
480
  const likeQuery = `%${topic}%`;
475
481
  return db.prepare(`
476
482
  SELECT e.* FROM episodes e
477
- WHERE e.status = 'active'
483
+ WHERE e.validity_status = 'active' AND e.is_archived = 0 AND e.promoted_to IS NULL
478
484
  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)
479
485
  ${scopeFilter}
480
486
  ORDER BY e.updated_at DESC
@@ -496,7 +502,7 @@ function queryDecisionContext(db, topic, scope, limit) {
496
502
  SELECT d.* FROM decisions d
497
503
  JOIN decisions_fts f ON d.rowid = f.rowid
498
504
  WHERE decisions_fts MATCH @query
499
- AND d.status = 'active'
505
+ AND d.validity_status = 'active' AND d.is_archived = 0 AND d.promoted_to IS NULL
500
506
  ${scopeFilter}
501
507
  ORDER BY rank
502
508
  LIMIT @limit
@@ -511,7 +517,7 @@ function queryDecisionContext(db, topic, scope, limit) {
511
517
  const likeQuery = `%${topic}%`;
512
518
  return db.prepare(`
513
519
  SELECT d.* FROM decisions d
514
- WHERE d.status = 'active'
520
+ WHERE d.validity_status = 'active' AND d.is_archived = 0 AND d.promoted_to IS NULL
515
521
  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)
516
522
  ${scopeFilter}
517
523
  ORDER BY d.created_at DESC
@@ -528,8 +534,8 @@ export function insertDecision(db, params) {
528
534
  const id = ulid();
529
535
  const now = new Date().toISOString();
530
536
  db.prepare(`
531
- 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)
532
- 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)
537
+ INSERT INTO decisions (id, title, description, l1_content, l2_reasoning, l2_alternatives, related_claim_ids, scope, validity_status, search_summary, l1_embedding, source_tool, session_id, client_name, client_version, user_input, created_at, updated_at)
538
+ VALUES (@id, @title, @description, @l1_content, @l2_reasoning, @l2_alternatives, @related_claim_ids, @scope, 'active', @search_summary, @l1_embedding, @source_tool, @session_id, @client_name, @client_version, @user_input, @created_at, @updated_at)
533
539
  `).run({
534
540
  id,
535
541
  title: params.title,
@@ -542,6 +548,7 @@ export function insertDecision(db, params) {
542
548
  search_summary: params.search_summary ?? null,
543
549
  l1_embedding: params.l1_embedding ?? null,
544
550
  source_tool: params.source_tool ?? null,
551
+ session_id: params.session_id ?? null,
545
552
  client_name: params.client_name ?? null,
546
553
  client_version: params.client_version ?? null,
547
554
  user_input: params.user_input ?? null,
@@ -559,8 +566,10 @@ export function listDecisions(db, params) {
559
566
  const conditions = [];
560
567
  const values = { limit: params.limit };
561
568
  if (params.status) {
562
- conditions.push("d.status = @status");
563
- values.status = params.status;
569
+ conditions.push("d.validity_status = @validity_status");
570
+ conditions.push("d.is_archived = 0");
571
+ conditions.push("d.promoted_to IS NULL");
572
+ values.validity_status = params.status;
564
573
  }
565
574
  if (params.scope) {
566
575
  conditions.push("(d.scope = @scope OR d.scope = 'global')");
@@ -606,8 +615,10 @@ export function listDecisions(db, params) {
606
615
  const conditions = [];
607
616
  const values = { limit: params.limit };
608
617
  if (params.status) {
609
- conditions.push("status = @status");
610
- values.status = params.status;
618
+ conditions.push("validity_status = @validity_status");
619
+ conditions.push("is_archived = 0");
620
+ conditions.push("promoted_to IS NULL");
621
+ values.validity_status = params.status;
611
622
  }
612
623
  if (params.scope) {
613
624
  conditions.push("(scope = @scope OR scope = 'global')");
@@ -625,34 +636,34 @@ export function listDecisions(db, params) {
625
636
  // === 統計 ===
626
637
  export function getStats(db) {
627
638
  const total_claims = db.prepare("SELECT COUNT(*) as count FROM claims").get().count;
628
- const active_claims = db.prepare("SELECT COUNT(*) as count FROM claims WHERE status = 'active'").get().count;
629
- const retracted_claims = db.prepare("SELECT COUNT(*) as count FROM claims WHERE status = 'retracted'").get().count;
630
- const superseded_claims = db.prepare("SELECT COUNT(*) as count FROM claims WHERE status = 'superseded'").get().count;
639
+ const active_claims = db.prepare("SELECT COUNT(*) as count FROM claims WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL").get().count;
640
+ const retracted_claims = db.prepare("SELECT COUNT(*) as count FROM claims WHERE validity_status = 'invalidated'").get().count;
641
+ const superseded_claims = db.prepare("SELECT COUNT(*) as count FROM claims WHERE validity_status = 'superseded'").get().count;
631
642
  const total_decisions = db.prepare("SELECT COUNT(*) as count FROM decisions").get().count;
632
- const active_decisions = db.prepare("SELECT COUNT(*) as count FROM decisions WHERE status = 'active'").get().count;
643
+ const active_decisions = db.prepare("SELECT COUNT(*) as count FROM decisions WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL").get().count;
633
644
  const total_history_entries = db.prepare("SELECT COUNT(*) as count FROM claim_history").get().count;
634
645
  // 2026-02-23 追加 (Issue #39 Part D): Episode/Insight/Theory/Model の安全なカウント
635
646
  let active_episodes = 0;
636
647
  try {
637
- active_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE status = 'active'").get().count;
648
+ active_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL").get().count;
638
649
  }
639
650
  catch { /* テーブル未存在等 */ }
640
651
  let active_insights = 0;
641
652
  try {
642
- active_insights = db.prepare("SELECT COUNT(*) as count FROM insights WHERE status = 'active'").get().count;
653
+ active_insights = db.prepare("SELECT COUNT(*) as count FROM insights WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL").get().count;
643
654
  }
644
655
  catch { /* テーブル未存在等 */ }
645
656
  let active_theories = 0;
646
657
  try {
647
- active_theories = db.prepare("SELECT COUNT(*) as count FROM theories WHERE status = 'active'").get().count;
658
+ active_theories = db.prepare("SELECT COUNT(*) as count FROM theories WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL").get().count;
648
659
  }
649
660
  catch { /* テーブル未存在等 */ }
650
661
  let active_models = 0;
651
662
  try {
652
- active_models = db.prepare("SELECT COUNT(*) as count FROM models WHERE status = 'active'").get().count;
663
+ active_models = db.prepare("SELECT COUNT(*) as count FROM models WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL").get().count;
653
664
  }
654
665
  catch { /* テーブル未存在等 */ }
655
- const categoryRows = db.prepare("SELECT category, COUNT(*) as count FROM claims WHERE status = 'active' GROUP BY category").all();
666
+ const categoryRows = db.prepare("SELECT category, COUNT(*) as count FROM claims WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL GROUP BY category").all();
656
667
  const categories = {};
657
668
  for (const row of categoryRows) {
658
669
  categories[row.category] = row.count;
@@ -673,10 +684,10 @@ export function getStats(db) {
673
684
  };
674
685
  }
675
686
  export function getAllActiveClaims(db) {
676
- return db.prepare("SELECT * FROM claims WHERE status = 'active' ORDER BY category, updated_at DESC").all();
687
+ return db.prepare("SELECT * FROM claims WHERE validity_status = 'active' AND is_archived = 0 ORDER BY category, updated_at DESC").all();
677
688
  }
678
689
  export function getRecentDecisions(db, limit = 10) {
679
- return db.prepare("SELECT * FROM decisions WHERE status = 'active' ORDER BY created_at DESC LIMIT ?").all(limit);
690
+ return db.prepare("SELECT * FROM decisions WHERE validity_status = 'active' AND is_archived = 0 ORDER BY created_at DESC LIMIT ?").all(limit);
680
691
  }
681
692
  // === L2 Core v0: Claim関係 ===
682
693
  export function insertClaimRelation(db, params) {
@@ -748,11 +759,11 @@ export function getClaimChecks(db, claimId) {
748
759
  export function insertAuditLog(db, params) {
749
760
  const id = ulid();
750
761
  const now = new Date().toISOString();
751
- // 2026-03-03 追加 (V21): client_id でどのデバイスで作られたか追跡
752
- const clientIdRow = db.prepare("SELECT value FROM store_meta WHERE key = 'client_id'").get();
762
+ // 2026-03-03 追加 (V21): device_id でどのデバイスで作られたか追跡
763
+ const deviceIdRow = db.prepare("SELECT value FROM store_meta WHERE key = 'device_id'").get();
753
764
  db.prepare(`
754
- INSERT INTO audit_log (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, client_id, created_at)
755
- VALUES (@id, @operation, @entity_type, @entity_id, @summary, @details, @client_name, @client_version, @session_id, @source_tool, @client_id, @created_at)
765
+ INSERT INTO audit_log (id, operation, entity_type, entity_id, summary, details, client_name, client_version, session_id, source_tool, device_id, created_at)
766
+ VALUES (@id, @operation, @entity_type, @entity_id, @summary, @details, @client_name, @client_version, @session_id, @source_tool, @device_id, @created_at)
756
767
  `).run({
757
768
  id,
758
769
  operation: params.operation,
@@ -764,7 +775,7 @@ export function insertAuditLog(db, params) {
764
775
  client_version: params.provenance?.client_version ?? null,
765
776
  session_id: params.provenance?.session_id ?? null,
766
777
  source_tool: params.source_tool ?? null,
767
- client_id: clientIdRow?.value ?? null,
778
+ device_id: deviceIdRow?.value ?? null,
768
779
  created_at: now,
769
780
  });
770
781
  }
@@ -808,7 +819,7 @@ export function insertEpisode(db, params) {
808
819
  const id = ulid();
809
820
  const now = new Date().toISOString();
810
821
  db.prepare(`
811
- 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)
822
+ 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, validity_status, search_summary, l1_embedding, source_tool, session_id, client_name, client_version, user_input, created_at, updated_at)
812
823
  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)
813
824
  `).run({
814
825
  id,
@@ -845,8 +856,10 @@ export function listEpisodes(db, params) {
845
856
  const conditions = [];
846
857
  const values = { limit: params.limit };
847
858
  if (params.status) {
848
- conditions.push("e.status = @status");
849
- values.status = params.status;
859
+ conditions.push("e.validity_status = @validity_status");
860
+ conditions.push("e.is_archived = 0");
861
+ conditions.push("e.promoted_to IS NULL");
862
+ values.validity_status = params.status;
850
863
  }
851
864
  if (params.tag) {
852
865
  conditions.push("e.tags LIKE @tag_like");
@@ -896,8 +909,10 @@ export function listEpisodes(db, params) {
896
909
  const conditions = [];
897
910
  const values = { limit: params.limit };
898
911
  if (params.status) {
899
- conditions.push("status = @status");
900
- values.status = params.status;
912
+ conditions.push("validity_status = @validity_status");
913
+ conditions.push("is_archived = 0");
914
+ conditions.push("promoted_to IS NULL");
915
+ values.validity_status = params.status;
901
916
  }
902
917
  if (params.tag) {
903
918
  conditions.push("tags LIKE @tag_like");
@@ -918,14 +933,14 @@ export function listEpisodes(db, params) {
918
933
  }
919
934
  export function getEpisodeStats(db) {
920
935
  const total_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes").get().count;
921
- const active_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE status = 'active'").get().count;
922
- const archived_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE status = 'archived'").get().count;
936
+ const active_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE validity_status = 'active' AND is_archived = 0").get().count;
937
+ const archived_episodes = db.prepare("SELECT COUNT(*) as count FROM episodes WHERE is_archived = 1").get().count;
923
938
  return { total_episodes, active_episodes, archived_episodes };
924
939
  }
925
940
  // === Issue #40 Phase 3: Episode棚卸 ===
926
941
  // 2026-02-23 追加 (Issue #40 Phase 3): 未グルーミングのEpisodeを取得
927
942
  export function listUngroomedEpisodes(db, params) {
928
- const conditions = ["e.status = 'active'"];
943
+ const conditions = ["e.validity_status = 'active' AND e.is_archived = 0"];
929
944
  const values = { limit: params.limit };
930
945
  if (!params.include_groomed) {
931
946
  conditions.push("e.groomed_at IS NULL");
@@ -952,19 +967,19 @@ export function listUngroomedEpisodes(db, params) {
952
967
  LEFT JOIN (
953
968
  SELECT j.value AS eid, COUNT(*) AS cnt
954
969
  FROM insights, json_each(insights.supporting_episode_ids) AS j
955
- WHERE insights.status = 'active'
970
+ WHERE insights.validity_status = 'active' AND insights.is_archived = 0
956
971
  GROUP BY j.value
957
972
  ) li ON li.eid = e.id
958
973
  LEFT JOIN (
959
974
  SELECT j.value AS eid, COUNT(*) AS cnt
960
975
  FROM models, json_each(models.supporting_episode_ids) AS j
961
- WHERE models.status = 'active'
976
+ WHERE models.validity_status = 'active' AND models.is_archived = 0
962
977
  GROUP BY j.value
963
978
  ) lm ON lm.eid = e.id
964
979
  LEFT JOIN (
965
980
  SELECT j.value AS eid, COUNT(*) AS cnt
966
981
  FROM theories, json_each(theories.supporting_episode_ids) AS j
967
- WHERE theories.status = 'active'
982
+ WHERE theories.validity_status = 'active' AND theories.is_archived = 0
968
983
  GROUP BY j.value
969
984
  ) lt ON lt.eid = e.id
970
985
  WHERE ${whereClause}
@@ -987,7 +1002,7 @@ export function markEpisodesGroomed(db, episodeIds) {
987
1002
  const markedIds = [];
988
1003
  const transaction = db.transaction(() => {
989
1004
  for (const id of episodeIds) {
990
- const result = db.prepare("UPDATE episodes SET groomed_at = @now WHERE id = @id AND status = 'active' AND groomed_at IS NULL").run({ id, now });
1005
+ const result = db.prepare("UPDATE episodes SET groomed_at = @now WHERE id = @id AND validity_status = 'active' AND is_archived = 0 AND groomed_at IS NULL").run({ id, now });
991
1006
  if (result.changes > 0) {
992
1007
  markedIds.push(id);
993
1008
  }
@@ -1003,7 +1018,7 @@ export function archiveEpisodes(db, params) {
1003
1018
  if (params.episodeIds && params.episodeIds.length > 0) {
1004
1019
  // 個別指定モード: 指定IDのみアーカイブ
1005
1020
  const updated = [];
1006
- const stmt = db.prepare("UPDATE episodes SET status = 'archived', updated_at = @now WHERE id = @id AND status = 'active'");
1021
+ const stmt = db.prepare("UPDATE episodes SET is_archived = 1, updated_at = @now WHERE id = @id AND validity_status = 'active' AND is_archived = 0");
1007
1022
  const now = new Date().toISOString();
1008
1023
  for (const id of params.episodeIds) {
1009
1024
  const result = stmt.run({ id, now });
@@ -1018,7 +1033,7 @@ export function archiveEpisodes(db, params) {
1018
1033
  // 最新 keepLatest 件の ID を取得(保護対象)
1019
1034
  const protectedIds = db.prepare(`
1020
1035
  SELECT id FROM episodes
1021
- WHERE status = 'active'${scopeFilter}
1036
+ WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}
1022
1037
  ORDER BY created_at DESC
1023
1038
  LIMIT @keepLatest
1024
1039
  `).all({ ...scopeValues, keepLatest });
@@ -1026,11 +1041,11 @@ export function archiveEpisodes(db, params) {
1026
1041
  // groomed_at IS NOT NULL かつ保護対象外をアーカイブ
1027
1042
  const candidates = db.prepare(`
1028
1043
  SELECT id FROM episodes
1029
- WHERE status = 'active' AND groomed_at IS NOT NULL${scopeFilter}
1044
+ WHERE validity_status = 'active' AND is_archived = 0 AND groomed_at IS NOT NULL${scopeFilter}
1030
1045
  `).all(scopeValues);
1031
1046
  const toArchive = candidates.filter(r => !protectedSet.has(r.id));
1032
1047
  const now = new Date().toISOString();
1033
- const stmt = db.prepare("UPDATE episodes SET status = 'archived', updated_at = @now WHERE id = @id");
1048
+ const stmt = db.prepare("UPDATE episodes SET is_archived = 1, updated_at = @now WHERE id = @id");
1034
1049
  for (const r of toArchive) {
1035
1050
  stmt.run({ id: r.id, now });
1036
1051
  }
@@ -1041,7 +1056,7 @@ export function archiveDecisions(db, params) {
1041
1056
  const keepLatest = params.keepLatest ?? 20;
1042
1057
  if (params.decisionIds && params.decisionIds.length > 0) {
1043
1058
  const updated = [];
1044
- const stmt = db.prepare("UPDATE decisions SET status = 'archived', updated_at = @now WHERE id = @id AND status = 'active'");
1059
+ const stmt = db.prepare("UPDATE decisions SET is_archived = 1, updated_at = @now WHERE id = @id AND validity_status = 'active' AND is_archived = 0");
1045
1060
  const now = new Date().toISOString();
1046
1061
  for (const id of params.decisionIds) {
1047
1062
  const result = stmt.run({ id, now });
@@ -1055,18 +1070,18 @@ export function archiveDecisions(db, params) {
1055
1070
  const scopeValues = params.scope ? { scope: params.scope } : {};
1056
1071
  const protectedIds = db.prepare(`
1057
1072
  SELECT id FROM decisions
1058
- WHERE status = 'active'${scopeFilter}
1073
+ WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}
1059
1074
  ORDER BY created_at DESC
1060
1075
  LIMIT @keepLatest
1061
1076
  `).all({ ...scopeValues, keepLatest });
1062
1077
  const protectedSet = new Set(protectedIds.map(r => r.id));
1063
1078
  const candidates = db.prepare(`
1064
1079
  SELECT id FROM decisions
1065
- WHERE status = 'active'${scopeFilter}
1080
+ WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}
1066
1081
  `).all(scopeValues);
1067
1082
  const toArchive = candidates.filter(r => !protectedSet.has(r.id));
1068
1083
  const now = new Date().toISOString();
1069
- const stmt = db.prepare("UPDATE decisions SET status = 'archived', updated_at = @now WHERE id = @id");
1084
+ const stmt = db.prepare("UPDATE decisions SET is_archived = 1, updated_at = @now WHERE id = @id");
1070
1085
  for (const r of toArchive) {
1071
1086
  stmt.run({ id: r.id, now });
1072
1087
  }
@@ -1079,7 +1094,7 @@ export function getTopicSummary(db, tagLimit = 10, decisionLimit = 5, scope) {
1079
1094
  const scopeFilter = scope ? " AND (scope = @scope OR scope = 'global')" : "";
1080
1095
  const scopeValues = scope ? { scope } : {};
1081
1096
  // カテゴリ分布
1082
- const categoryRows = db.prepare(`SELECT category, COUNT(*) as count FROM claims WHERE status = 'active'${scopeFilter} GROUP BY category`).all(scopeValues);
1097
+ const categoryRows = db.prepare(`SELECT category, COUNT(*) as count FROM claims WHERE validity_status = 'active' AND is_archived = 0${scopeFilter} GROUP BY category`).all(scopeValues);
1083
1098
  const categories = {};
1084
1099
  for (const row of categoryRows) {
1085
1100
  categories[row.category] = row.count;
@@ -1089,13 +1104,13 @@ export function getTopicSummary(db, tagLimit = 10, decisionLimit = 5, scope) {
1089
1104
  try {
1090
1105
  topTags = db.prepare(`
1091
1106
  SELECT tag, COUNT(*) as count FROM (
1092
- SELECT j.value as tag FROM episodes, json_each(episodes.tags) AS j WHERE episodes.status = 'active'${scopeFilter.replace("scope", "episodes.scope")}
1107
+ SELECT j.value as tag FROM episodes, json_each(episodes.tags) AS j WHERE episodes.validity_status = 'active' AND episodes.is_archived = 0${scopeFilter.replace("scope", "episodes.scope")}
1093
1108
  UNION ALL
1094
- SELECT j.value as tag FROM theories, json_each(theories.tags) AS j WHERE theories.status = 'active'${scopeFilter.replace("scope", "theories.scope")}
1109
+ SELECT j.value as tag FROM theories, json_each(theories.tags) AS j WHERE theories.validity_status = 'active' AND theories.is_archived = 0${scopeFilter.replace("scope", "theories.scope")}
1095
1110
  UNION ALL
1096
- SELECT j.value as tag FROM insights, json_each(insights.tags) AS j WHERE insights.status = 'active'${scopeFilter.replace("scope", "insights.scope")}
1111
+ SELECT j.value as tag FROM insights, json_each(insights.tags) AS j WHERE insights.validity_status = 'active' AND insights.is_archived = 0${scopeFilter.replace("scope", "insights.scope")}
1097
1112
  UNION ALL
1098
- SELECT j.value as tag FROM models, json_each(models.tags) AS j WHERE models.status = 'active'${scopeFilter.replace("scope", "models.scope")}
1113
+ SELECT j.value as tag FROM models, json_each(models.tags) AS j WHERE models.validity_status = 'active' AND models.is_archived = 0${scopeFilter.replace("scope", "models.scope")}
1099
1114
  ) GROUP BY tag ORDER BY count DESC LIMIT @tagLimit
1100
1115
  `).all({ ...scopeValues, tagLimit });
1101
1116
  }
@@ -1103,38 +1118,43 @@ export function getTopicSummary(db, tagLimit = 10, decisionLimit = 5, scope) {
1103
1118
  // json_each 非対応環境のフォールバック: 空配列
1104
1119
  }
1105
1120
  // 最近のDecision
1106
- const recentDecisions = db.prepare(`SELECT title, created_at FROM decisions WHERE status = 'active'${scopeFilter} ORDER BY created_at DESC LIMIT @decisionLimit`).all({ ...scopeValues, decisionLimit });
1121
+ const recentDecisions = db.prepare(`SELECT title, created_at FROM decisions WHERE validity_status = 'active' AND is_archived = 0${scopeFilter} ORDER BY created_at DESC LIMIT @decisionLimit`).all({ ...scopeValues, decisionLimit });
1107
1122
  // アクティブスコープ一覧
1108
- const scopeRows = db.prepare(`SELECT DISTINCT scope FROM claims WHERE status = 'active'${scopeFilter} ORDER BY scope`).all(scopeValues);
1123
+ const scopeRows = db.prepare(`SELECT DISTINCT scope FROM claims WHERE validity_status = 'active' AND is_archived = 0${scopeFilter} ORDER BY scope`).all(scopeValues);
1109
1124
  const activeScopes = scopeRows.map(r => r.scope);
1110
1125
  // 4テーブルのアクティブ件数
1111
- const activeClaims = db.prepare(`SELECT COUNT(*) as count FROM claims WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1112
- const activeEpisodes = db.prepare(`SELECT COUNT(*) as count FROM episodes WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1113
- const activeDecisions = db.prepare(`SELECT COUNT(*) as count FROM decisions WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1114
- const activeTheories = db.prepare(`SELECT COUNT(*) as count FROM theories WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1126
+ const activeClaims = db.prepare(`SELECT COUNT(*) as count FROM claims WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1127
+ const activeEpisodes = db.prepare(`SELECT COUNT(*) as count FROM episodes WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1128
+ const activeDecisions = db.prepare(`SELECT COUNT(*) as count FROM decisions WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1129
+ const activeTheories = db.prepare(`SELECT COUNT(*) as count FROM theories WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1115
1130
  // 2026-02-23 追加 (Issue #40): Insight/Model のアクティブ件数
1116
- const activeInsights = db.prepare(`SELECT COUNT(*) as count FROM insights WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1117
- const activeModels = db.prepare(`SELECT COUNT(*) as count FROM models WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1131
+ const activeInsights = db.prepare(`SELECT COUNT(*) as count FROM insights WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1132
+ const activeModels = db.prepare(`SELECT COUNT(*) as count FROM models WHERE validity_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1118
1133
  // 2026-02-25 リネーム: memos → user_memos (Issue #45 Phase 1)
1119
1134
  let activeMemos = 0;
1120
1135
  try {
1121
- activeMemos = db.prepare(`SELECT COUNT(*) as count FROM user_memos WHERE status = 'active'${scopeFilter}`).get(scopeValues).count;
1136
+ activeMemos = db.prepare(`SELECT COUNT(*) as count FROM user_memos WHERE memo_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1122
1137
  }
1123
1138
  catch { /* V16未適用 */ }
1139
+ let activeFiles = 0;
1140
+ try {
1141
+ activeFiles = db.prepare(`SELECT COUNT(*) as count FROM user_files WHERE file_status = 'active' AND is_archived = 0${scopeFilter}`).get(scopeValues).count;
1142
+ }
1143
+ catch { /* V31未適用 */ }
1124
1144
  // Issue #47: エンティティタイトルから具体的キーワード候補を抽出
1125
1145
  let topicKeywords = [];
1126
1146
  try {
1127
1147
  const keywordRows = db.prepare(`
1128
1148
  SELECT title FROM (
1129
- SELECT title, updated_at FROM episodes WHERE status = 'active'${scopeFilter.replace("scope", "episodes.scope")}
1149
+ SELECT title, updated_at FROM episodes WHERE validity_status = 'active' AND is_archived = 0${scopeFilter.replace("scope", "episodes.scope")}
1130
1150
  UNION ALL
1131
- SELECT title, updated_at FROM decisions WHERE status = 'active'${scopeFilter.replace("scope", "decisions.scope")}
1151
+ SELECT title, updated_at FROM decisions WHERE validity_status = 'active' AND is_archived = 0${scopeFilter.replace("scope", "decisions.scope")}
1132
1152
  UNION ALL
1133
- SELECT title, updated_at FROM theories WHERE status = 'active'${scopeFilter.replace("scope", "theories.scope")}
1153
+ SELECT title, updated_at FROM theories WHERE validity_status = 'active' AND is_archived = 0${scopeFilter.replace("scope", "theories.scope")}
1134
1154
  UNION ALL
1135
- SELECT title, updated_at FROM insights WHERE status = 'active'${scopeFilter.replace("scope", "insights.scope")}
1155
+ SELECT title, updated_at FROM insights WHERE validity_status = 'active' AND is_archived = 0${scopeFilter.replace("scope", "insights.scope")}
1136
1156
  UNION ALL
1137
- SELECT title, updated_at FROM models WHERE status = 'active'${scopeFilter.replace("scope", "models.scope")}
1157
+ SELECT title, updated_at FROM models WHERE validity_status = 'active' AND is_archived = 0${scopeFilter.replace("scope", "models.scope")}
1138
1158
  ) ORDER BY updated_at DESC LIMIT 20
1139
1159
  `).all(scopeValues);
1140
1160
  topicKeywords = keywordRows.map(r => r.title.length > 30 ? r.title.slice(0, 30) + "…" : r.title);
@@ -1148,7 +1168,7 @@ export function getTopicSummary(db, tagLimit = 10, decisionLimit = 5, scope) {
1148
1168
  topicKeywords,
1149
1169
  recentDecisions,
1150
1170
  activeScopes,
1151
- counts: { activeClaims, activeEpisodes, activeDecisions, activeTheories, activeInsights, activeModels, activeMemos },
1171
+ counts: { activeClaims, activeEpisodes, activeDecisions, activeTheories, activeInsights, activeModels, activeMemos, activeFiles },
1152
1172
  };
1153
1173
  }
1154
1174
  // === V9: Theory CRUD ===
@@ -1161,7 +1181,7 @@ export function insertTheory(db, params) {
1161
1181
  INSERT INTO theories (id, title, description, l1_content,
1162
1182
  l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
1163
1183
  non_goals, open_questions,
1164
- supporting_episode_ids, supporting_claim_ids, evidence_refs, tags, scope, confidence, status, search_summary, l1_embedding,
1184
+ supporting_episode_ids, supporting_claim_ids, evidence_refs, tags, scope, confidence, validity_status, search_summary, l1_embedding,
1165
1185
  source_tool, session_id, client_name, client_version, created_at, updated_at)
1166
1186
  VALUES (@id, @title, @description, @l1_content,
1167
1187
  @l2_core_thesis, @l2_principles, @l2_trigger_conditions, @l2_resolution_steps, @l2_applicable_context,
@@ -1206,8 +1226,10 @@ export function listTheories(db, params) {
1206
1226
  const conditions = [];
1207
1227
  const values = { limit: params.limit };
1208
1228
  if (params.status) {
1209
- conditions.push("t.status = @status");
1210
- values.status = params.status;
1229
+ conditions.push("t.validity_status = @validity_status");
1230
+ conditions.push("t.is_archived = 0");
1231
+ conditions.push("t.promoted_to IS NULL");
1232
+ values.validity_status = params.status;
1211
1233
  }
1212
1234
  if (params.tag) {
1213
1235
  conditions.push("t.tags LIKE @tag_like");
@@ -1257,8 +1279,10 @@ export function listTheories(db, params) {
1257
1279
  const conditions = [];
1258
1280
  const values = { limit: params.limit };
1259
1281
  if (params.status) {
1260
- conditions.push("status = @status");
1261
- values.status = params.status;
1282
+ conditions.push("validity_status = @validity_status");
1283
+ conditions.push("is_archived = 0");
1284
+ conditions.push("promoted_to IS NULL");
1285
+ values.validity_status = params.status;
1262
1286
  }
1263
1287
  if (params.tag) {
1264
1288
  conditions.push("tags LIKE @tag_like");
@@ -1292,7 +1316,7 @@ function queryInsightContext(db, topic, scope, limit) {
1292
1316
  SELECT t.* FROM insights t
1293
1317
  JOIN insights_fts f ON t.rowid = f.rowid
1294
1318
  WHERE insights_fts MATCH @query
1295
- AND t.status = 'active'
1319
+ AND t.validity_status = 'active' AND t.is_archived = 0 AND t.promoted_to IS NULL
1296
1320
  ${scopeFilter}
1297
1321
  ORDER BY rank
1298
1322
  LIMIT @limit
@@ -1308,7 +1332,7 @@ function queryInsightContext(db, topic, scope, limit) {
1308
1332
  // 2026-03-01 修正 (Issue #53): l1/l2 カラム名に対応
1309
1333
  return db.prepare(`
1310
1334
  SELECT t.* FROM insights t
1311
- WHERE t.status = 'active'
1335
+ WHERE t.validity_status = 'active' AND t.is_archived = 0 AND t.promoted_to IS NULL
1312
1336
  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)
1313
1337
  ${scopeFilter}
1314
1338
  ORDER BY t.updated_at DESC
@@ -1330,7 +1354,7 @@ function queryModelContext(db, topic, scope, limit) {
1330
1354
  SELECT t.* FROM models t
1331
1355
  JOIN models_fts f ON t.rowid = f.rowid
1332
1356
  WHERE models_fts MATCH @query
1333
- AND t.status = 'active'
1357
+ AND t.validity_status = 'active' AND t.is_archived = 0 AND t.promoted_to IS NULL
1334
1358
  ${scopeFilter}
1335
1359
  ORDER BY rank
1336
1360
  LIMIT @limit
@@ -1345,7 +1369,7 @@ function queryModelContext(db, topic, scope, limit) {
1345
1369
  const likeQuery = `%${topic}%`;
1346
1370
  return db.prepare(`
1347
1371
  SELECT t.* FROM models t
1348
- WHERE t.status = 'active'
1372
+ WHERE t.validity_status = 'active' AND t.is_archived = 0 AND t.promoted_to IS NULL
1349
1373
  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)
1350
1374
  ${scopeFilter}
1351
1375
  ORDER BY t.updated_at DESC
@@ -1366,7 +1390,7 @@ export function searchPrinciplesForRecall(db, promptText, limit) {
1366
1390
  SELECT e.* FROM episodes e
1367
1391
  JOIN episodes_fts f ON e.rowid = f.rowid
1368
1392
  WHERE episodes_fts MATCH @query
1369
- AND e.status = 'active'
1393
+ AND e.validity_status = 'active' AND e.is_archived = 0 AND e.promoted_to IS NULL
1370
1394
  AND e.l2_principles != '[]'
1371
1395
  ORDER BY rank
1372
1396
  LIMIT @limit
@@ -1380,7 +1404,7 @@ export function searchPrinciplesForRecall(db, promptText, limit) {
1380
1404
  const likeQuery = `%${querySnippet}%`;
1381
1405
  episodeRows = db.prepare(`
1382
1406
  SELECT e.* FROM episodes e
1383
- WHERE e.status = 'active'
1407
+ WHERE e.validity_status = 'active' AND e.is_archived = 0 AND e.promoted_to IS NULL
1384
1408
  AND e.l2_principles != '[]'
1385
1409
  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)
1386
1410
  ORDER BY e.updated_at DESC
@@ -1429,7 +1453,7 @@ export function searchPrinciplesForRecall(db, promptText, limit) {
1429
1453
  try {
1430
1454
  const insightRows = db.prepare(`
1431
1455
  SELECT * FROM insights
1432
- WHERE status = 'active'
1456
+ WHERE validity_status = 'active' AND is_archived = 0
1433
1457
  AND l2_principles != '[]'
1434
1458
  AND (${insightLike.clause})
1435
1459
  ORDER BY updated_at DESC
@@ -1450,7 +1474,7 @@ export function searchPrinciplesForRecall(db, promptText, limit) {
1450
1474
  try {
1451
1475
  const theoryRows = db.prepare(`
1452
1476
  SELECT * FROM theories
1453
- WHERE status = 'active'
1477
+ WHERE validity_status = 'active' AND is_archived = 0
1454
1478
  AND (l2_principles != '[]' OR open_questions != '[]')
1455
1479
  AND (${theoryLike.clause})
1456
1480
  ORDER BY updated_at DESC
@@ -1484,7 +1508,7 @@ export function listEpisodesWithPrinciples(db, limit, scope) {
1484
1508
  }
1485
1509
  return db.prepare(`
1486
1510
  SELECT * FROM episodes
1487
- WHERE status = 'active' AND l2_principles != '[]'
1511
+ WHERE validity_status = 'active' AND is_archived = 0 AND l2_principles != '[]'
1488
1512
  ${scopeFilter}
1489
1513
  ORDER BY updated_at DESC
1490
1514
  LIMIT @limit
@@ -1505,7 +1529,7 @@ export function listEscalationCandidates(db, params) {
1505
1529
  json_array_length(supporting_episode_ids) as supporting_episode_count,
1506
1530
  scope, created_at, description
1507
1531
  FROM insights
1508
- WHERE status = 'active'
1532
+ WHERE validity_status = 'active' AND is_archived = 0
1509
1533
  AND confidence >= 0.8
1510
1534
  AND l2_principles != '[]'
1511
1535
  AND json_array_length(supporting_episode_ids) >= 2
@@ -1533,7 +1557,7 @@ export function listEscalationCandidates(db, params) {
1533
1557
  json_array_length(supporting_episode_ids) as supporting_episode_count,
1534
1558
  scope, created_at, description
1535
1559
  FROM theories
1536
- WHERE status = 'active'
1560
+ WHERE validity_status = 'active' AND is_archived = 0
1537
1561
  AND confidence >= 0.8
1538
1562
  AND l2_principles != '[]'
1539
1563
  AND json_array_length(supporting_episode_ids) >= 2
@@ -1561,7 +1585,7 @@ export function listEscalationCandidates(db, params) {
1561
1585
  json_array_length(supporting_episode_ids) as supporting_episode_count,
1562
1586
  scope, created_at, l2_core_thesis
1563
1587
  FROM models
1564
- WHERE status = 'active'
1588
+ WHERE validity_status = 'active' AND is_archived = 0
1565
1589
  AND confidence >= 0.8
1566
1590
  AND l2_principles != '[]'
1567
1591
  AND json_array_length(supporting_episode_ids) >= 2
@@ -1603,7 +1627,7 @@ function queryTheoryContext(db, topic, scope, limit) {
1603
1627
  SELECT t.* FROM theories t
1604
1628
  JOIN theories_fts f ON t.rowid = f.rowid
1605
1629
  WHERE theories_fts MATCH @query
1606
- AND t.status = 'active'
1630
+ AND t.validity_status = 'active' AND t.is_archived = 0 AND t.promoted_to IS NULL
1607
1631
  ${scopeFilter}
1608
1632
  ORDER BY rank
1609
1633
  LIMIT @limit
@@ -1618,7 +1642,7 @@ function queryTheoryContext(db, topic, scope, limit) {
1618
1642
  const likeQuery = `%${topic}%`;
1619
1643
  return db.prepare(`
1620
1644
  SELECT t.* FROM theories t
1621
- WHERE t.status = 'active'
1645
+ WHERE t.validity_status = 'active' AND t.is_archived = 0 AND t.promoted_to IS NULL
1622
1646
  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)
1623
1647
  ${scopeFilter}
1624
1648
  ORDER BY t.updated_at DESC
@@ -1632,7 +1656,7 @@ function queryTheoryContext(db, topic, scope, limit) {
1632
1656
  * TypeScript 側で陳腐化スコアを計算してソート・フィルタする。
1633
1657
  */
1634
1658
  export function getStaleClaims(db, params) {
1635
- const conditions = ["c.status = 'active'"];
1659
+ const conditions = ["c.validity_status = 'active'", "c.is_archived = 0"];
1636
1660
  const values = {};
1637
1661
  if (params.scope) {
1638
1662
  conditions.push("(c.scope = @scope OR c.scope = 'global')");
@@ -1763,11 +1787,11 @@ export function countUnconsolidatedItems(db) {
1763
1787
  try {
1764
1788
  const row = db.prepare(`
1765
1789
  SELECT MAX(created_at) AS last_at FROM (
1766
- SELECT created_at FROM theories WHERE status = 'active'
1790
+ SELECT created_at FROM theories WHERE validity_status = 'active' AND is_archived = 0
1767
1791
  UNION ALL
1768
- SELECT created_at FROM insights WHERE status = 'active'
1792
+ SELECT created_at FROM insights WHERE validity_status = 'active' AND is_archived = 0
1769
1793
  UNION ALL
1770
- SELECT created_at FROM models WHERE status = 'active'
1794
+ SELECT created_at FROM models WHERE validity_status = 'active' AND is_archived = 0
1771
1795
  )
1772
1796
  `).get();
1773
1797
  lastAt = row?.last_at ?? null;
@@ -1778,9 +1802,9 @@ export function countUnconsolidatedItems(db) {
1778
1802
  if (lastAt) {
1779
1803
  const result = db.prepare(`
1780
1804
  SELECT (
1781
- (SELECT COUNT(*) FROM claims WHERE status = 'active' AND created_at > @since) +
1782
- (SELECT COUNT(*) FROM decisions WHERE status = 'active' AND created_at > @since) +
1783
- (SELECT COUNT(*) FROM episodes WHERE status = 'active' AND created_at > @since)
1805
+ (SELECT COUNT(*) FROM claims WHERE validity_status = 'active' AND is_archived = 0 AND created_at > @since) +
1806
+ (SELECT COUNT(*) FROM decisions WHERE validity_status = 'active' AND is_archived = 0 AND created_at > @since) +
1807
+ (SELECT COUNT(*) FROM episodes WHERE validity_status = 'active' AND is_archived = 0 AND created_at > @since)
1784
1808
  ) AS count
1785
1809
  `).get({ since: lastAt });
1786
1810
  return result.count;
@@ -1788,9 +1812,9 @@ export function countUnconsolidatedItems(db) {
1788
1812
  // Theory/Insight/Model が一件もない場合: 全アクティブアイテム合計
1789
1813
  const result = db.prepare(`
1790
1814
  SELECT (
1791
- (SELECT COUNT(*) FROM claims WHERE status = 'active') +
1792
- (SELECT COUNT(*) FROM decisions WHERE status = 'active') +
1793
- (SELECT COUNT(*) FROM episodes WHERE status = 'active')
1815
+ (SELECT COUNT(*) FROM claims WHERE validity_status = 'active' AND is_archived = 0) +
1816
+ (SELECT COUNT(*) FROM decisions WHERE validity_status = 'active' AND is_archived = 0) +
1817
+ (SELECT COUNT(*) FROM episodes WHERE validity_status = 'active' AND is_archived = 0)
1794
1818
  ) AS count
1795
1819
  `).get();
1796
1820
  return result.count;
@@ -1801,7 +1825,7 @@ export function countUnconsolidatedItems(db) {
1801
1825
  export function countStaleClaims(db) {
1802
1826
  const result = db.prepare(`
1803
1827
  SELECT COUNT(*) AS count FROM claims
1804
- WHERE status = 'active'
1828
+ WHERE validity_status = 'active' AND is_archived = 0
1805
1829
  AND julianday('now') - julianday(updated_at) > 30
1806
1830
  `).get();
1807
1831
  return result.count;
@@ -1884,44 +1908,44 @@ export function updateTheoryL1Embedding(db, id, l1_embedding) {
1884
1908
  }
1885
1909
  /** l1_embedding が NULL の active Claim を取得(バックフィル用) */
1886
1910
  export function getClaimsWithoutL1Embedding(db, limit) {
1887
- return db.prepare("SELECT * FROM claims WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1911
+ return db.prepare("SELECT * FROM claims WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1888
1912
  }
1889
1913
  /** l1_embedding が NULL の active Decision を取得(バックフィル用) */
1890
1914
  export function getDecisionsWithoutL1Embedding(db, limit) {
1891
- return db.prepare("SELECT * FROM decisions WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1915
+ return db.prepare("SELECT * FROM decisions WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1892
1916
  }
1893
1917
  /** l1_embedding が NULL の active Episode を取得(バックフィル用) */
1894
1918
  export function getEpisodesWithoutL1Embedding(db, limit) {
1895
- return db.prepare("SELECT * FROM episodes WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1919
+ return db.prepare("SELECT * FROM episodes WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1896
1920
  }
1897
1921
  /** l1_embedding が NULL の active Theory を取得(バックフィル用) */
1898
1922
  export function getTheoriesWithoutL1Embedding(db, limit) {
1899
- return db.prepare("SELECT * FROM theories WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1923
+ return db.prepare("SELECT * FROM theories WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1900
1924
  }
1901
1925
  /** 指定テーブルで l1_embedding が存在するレコードを取得 */
1902
1926
  export function getClaimsWithL1Embedding(db, scope, limit = 100) {
1903
1927
  if (scope) {
1904
- 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);
1928
+ return db.prepare("SELECT * FROM claims WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1905
1929
  }
1906
- return db.prepare("SELECT * FROM claims WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1930
+ return db.prepare("SELECT * FROM claims WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1907
1931
  }
1908
1932
  export function getDecisionsWithL1Embedding(db, scope, limit = 100) {
1909
1933
  if (scope) {
1910
- 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);
1934
+ return db.prepare("SELECT * FROM decisions WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1911
1935
  }
1912
- return db.prepare("SELECT * FROM decisions WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1936
+ return db.prepare("SELECT * FROM decisions WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1913
1937
  }
1914
1938
  export function getEpisodesWithL1Embedding(db, scope, limit = 100) {
1915
1939
  if (scope) {
1916
- 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);
1940
+ return db.prepare("SELECT * FROM episodes WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1917
1941
  }
1918
- return db.prepare("SELECT * FROM episodes WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1942
+ return db.prepare("SELECT * FROM episodes WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1919
1943
  }
1920
1944
  export function getTheoriesWithL1Embedding(db, scope, limit = 100) {
1921
1945
  if (scope) {
1922
- 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);
1946
+ return db.prepare("SELECT * FROM theories WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
1923
1947
  }
1924
- return db.prepare("SELECT * FROM theories WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1948
+ return db.prepare("SELECT * FROM theories WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
1925
1949
  }
1926
1950
  // === V12: Insight CRUD (Issue #40) ===
1927
1951
  // 2026-03-01 修正 (Issue #53): l1/l2 カラム拡張 — core_thesis → l2_core_thesis, principles → l2_principles, 新カラム4つ追加
@@ -1934,7 +1958,7 @@ export function insertInsight(db, params) {
1934
1958
  l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
1935
1959
  tags, scope, confidence, search_summary, l1_embedding,
1936
1960
  supporting_episode_ids, supporting_claim_ids, evidence_refs,
1937
- status,
1961
+ validity_status,
1938
1962
  source_tool, session_id, client_name, client_version, created_at, updated_at)
1939
1963
  VALUES (@id, @title, @description,
1940
1964
  @l1_content,
@@ -1979,8 +2003,10 @@ export function listInsights(db, params) {
1979
2003
  const conditions = [];
1980
2004
  const values = { limit: params.limit };
1981
2005
  if (params.status) {
1982
- conditions.push("t.status = @status");
1983
- values.status = params.status;
2006
+ conditions.push("t.validity_status = @validity_status");
2007
+ conditions.push("t.is_archived = 0");
2008
+ conditions.push("t.promoted_to IS NULL");
2009
+ values.validity_status = params.status;
1984
2010
  }
1985
2011
  if (params.tag) {
1986
2012
  conditions.push("t.tags LIKE @tag_like");
@@ -2029,8 +2055,10 @@ export function listInsights(db, params) {
2029
2055
  const conditions = [];
2030
2056
  const values = { limit: params.limit };
2031
2057
  if (params.status) {
2032
- conditions.push("status = @status");
2033
- values.status = params.status;
2058
+ conditions.push("validity_status = @validity_status");
2059
+ conditions.push("is_archived = 0");
2060
+ conditions.push("promoted_to IS NULL");
2061
+ values.validity_status = params.status;
2034
2062
  }
2035
2063
  if (params.tag) {
2036
2064
  conditions.push("tags LIKE @tag_like");
@@ -2055,13 +2083,13 @@ export function updateInsightL1Embedding(db, id, l1_embedding) {
2055
2083
  }
2056
2084
  /** l1_embedding が NULL の active Insight を取得(バックフィル用) */
2057
2085
  export function getInsightsWithoutL1Embedding(db, limit) {
2058
- return db.prepare("SELECT * FROM insights WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2086
+ return db.prepare("SELECT * FROM insights WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2059
2087
  }
2060
2088
  export function getInsightsWithL1Embedding(db, scope, limit = 100) {
2061
2089
  if (scope) {
2062
- 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);
2090
+ return db.prepare("SELECT * FROM insights WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2063
2091
  }
2064
- return db.prepare("SELECT * FROM insights WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2092
+ return db.prepare("SELECT * FROM insights WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2065
2093
  }
2066
2094
  // === V12: Model CRUD (Issue #40) ===
2067
2095
  // 2026-03-01 修正 (Issue #59): core_thesis → l2_core_thesis, principles → l2_principles + 新l2カラム + l1_content
@@ -2073,7 +2101,7 @@ export function insertModel(db, params) {
2073
2101
  INSERT INTO models (id, title, description, l1_content,
2074
2102
  l2_core_thesis, l2_principles, l2_trigger_conditions, l2_resolution_steps, l2_applicable_context,
2075
2103
  non_goals, open_questions,
2076
- supporting_episode_ids, supporting_claim_ids, evidence_refs, tags, scope, confidence, status, search_summary, l1_embedding,
2104
+ supporting_episode_ids, supporting_claim_ids, evidence_refs, tags, scope, confidence, validity_status, search_summary, l1_embedding,
2077
2105
  source_tool, session_id, client_name, client_version, created_at, updated_at)
2078
2106
  VALUES (@id, @title, @description, @l1_content,
2079
2107
  @l2_core_thesis, @l2_principles, @l2_trigger_conditions, @l2_resolution_steps, @l2_applicable_context,
@@ -2118,8 +2146,10 @@ export function listModels(db, params) {
2118
2146
  const conditions = [];
2119
2147
  const values = { limit: params.limit };
2120
2148
  if (params.status) {
2121
- conditions.push("t.status = @status");
2122
- values.status = params.status;
2149
+ conditions.push("t.validity_status = @validity_status");
2150
+ conditions.push("t.is_archived = 0");
2151
+ conditions.push("t.promoted_to IS NULL");
2152
+ values.validity_status = params.status;
2123
2153
  }
2124
2154
  if (params.tag) {
2125
2155
  conditions.push("t.tags LIKE @tag_like");
@@ -2168,8 +2198,10 @@ export function listModels(db, params) {
2168
2198
  const conditions = [];
2169
2199
  const values = { limit: params.limit };
2170
2200
  if (params.status) {
2171
- conditions.push("status = @status");
2172
- values.status = params.status;
2201
+ conditions.push("validity_status = @validity_status");
2202
+ conditions.push("is_archived = 0");
2203
+ conditions.push("promoted_to IS NULL");
2204
+ values.validity_status = params.status;
2173
2205
  }
2174
2206
  if (params.tag) {
2175
2207
  conditions.push("tags LIKE @tag_like");
@@ -2192,22 +2224,55 @@ export function listModels(db, params) {
2192
2224
  export function updateModelL1Embedding(db, id, l1_embedding) {
2193
2225
  db.prepare("UPDATE models SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2194
2226
  }
2227
+ /** Theory/Insight/Model の共通フィールドを更新 (Issue #3 user_issue) */
2228
+ export function updateKnowledge(db, kind, id, updates) {
2229
+ const table = kind === "theory" ? "theories" : kind === "insight" ? "insights" : "models";
2230
+ const getById = kind === "theory" ? getTheoryById : kind === "insight" ? getInsightById : getModelById;
2231
+ const existing = getById(db, id);
2232
+ if (!existing)
2233
+ return undefined;
2234
+ const now = new Date().toISOString();
2235
+ const setClauses = ["updated_at = @updated_at"];
2236
+ const values = { id, updated_at: now };
2237
+ const stringFields = ["title", "description", "l1_content", "l2_core_thesis", "l2_applicable_context", "scope", "search_summary", "validity_status"];
2238
+ for (const field of stringFields) {
2239
+ if (updates[field] !== undefined) {
2240
+ setClauses.push(`${field} = @${field}`);
2241
+ values[field] = updates[field];
2242
+ }
2243
+ }
2244
+ if (updates.confidence !== undefined) {
2245
+ setClauses.push("confidence = @confidence");
2246
+ values.confidence = updates.confidence;
2247
+ }
2248
+ const arrayFields = ["l2_principles", "l2_trigger_conditions", "l2_resolution_steps", "supporting_episode_ids", "supporting_claim_ids", "evidence_refs", "tags", "non_goals", "open_questions"];
2249
+ for (const field of arrayFields) {
2250
+ if (updates[field] !== undefined) {
2251
+ setClauses.push(`${field} = @${field}`);
2252
+ values[field] = JSON.stringify(updates[field]);
2253
+ }
2254
+ }
2255
+ if (setClauses.length <= 1)
2256
+ return existing; // updated_at のみ = 変更なし
2257
+ db.prepare(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
2258
+ return getById(db, id);
2259
+ }
2195
2260
  /** l1_embedding が NULL の active Model を取得(バックフィル用) */
2196
2261
  export function getModelsWithoutL1Embedding(db, limit) {
2197
- return db.prepare("SELECT * FROM models WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2262
+ return db.prepare("SELECT * FROM models WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2198
2263
  }
2199
2264
  export function getModelsWithL1Embedding(db, scope, limit = 100) {
2200
2265
  if (scope) {
2201
- 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);
2266
+ return db.prepare("SELECT * FROM models WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND scope = ? ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2202
2267
  }
2203
- return db.prepare("SELECT * FROM models WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2268
+ return db.prepare("SELECT * FROM models WHERE validity_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2204
2269
  }
2205
2270
  // 2026-03-04 修正 (Issue #54): content → l1_content, user_input 追加
2206
2271
  export function insertUserMemo(db, params) {
2207
2272
  const id = ulid();
2208
2273
  const now = new Date().toISOString();
2209
2274
  db.prepare(`
2210
- 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)
2275
+ INSERT INTO user_memos (id, title, l1_content, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, memo_status, created_at, updated_at)
2211
2276
  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)
2212
2277
  `).run({
2213
2278
  id,
@@ -2261,9 +2326,13 @@ export function updateUserMemo(db, id, updates) {
2261
2326
  setClauses.push("search_summary = @search_summary");
2262
2327
  values.search_summary = updates.search_summary;
2263
2328
  }
2264
- if (updates.status !== undefined) {
2265
- setClauses.push("status = @status");
2266
- values.status = updates.status;
2329
+ if (updates.memo_status !== undefined) {
2330
+ setClauses.push("memo_status = @memo_status");
2331
+ values.memo_status = updates.memo_status;
2332
+ }
2333
+ if (updates.is_archived !== undefined) {
2334
+ setClauses.push("is_archived = @is_archived");
2335
+ values.is_archived = updates.is_archived;
2267
2336
  }
2268
2337
  if (setClauses.length === 0)
2269
2338
  return existing;
@@ -2278,8 +2347,9 @@ export function listUserMemos(db, params) {
2278
2347
  const conditions = [];
2279
2348
  const values = { limit };
2280
2349
  if (params.status) {
2281
- conditions.push("m.status = @status");
2282
- values.status = params.status;
2350
+ conditions.push("m.memo_status = @memo_status");
2351
+ conditions.push("m.is_archived = 0");
2352
+ values.memo_status = params.status;
2283
2353
  }
2284
2354
  if (params.usage_policy) {
2285
2355
  conditions.push("m.usage_policy = @usage_policy");
@@ -2325,28 +2395,28 @@ export function listUserMemos(db, params) {
2325
2395
  }
2326
2396
  export function listAutoUserMemos(db, scope, limit = 5) {
2327
2397
  if (scope) {
2328
- 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);
2398
+ return db.prepare("SELECT * FROM user_memos WHERE memo_status = 'active' AND is_archived = 0 AND usage_policy = 'auto' AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2329
2399
  }
2330
- return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2400
+ return db.prepare("SELECT * FROM user_memos WHERE memo_status = 'active' AND is_archived = 0 AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2331
2401
  }
2332
2402
  export function updateUserMemoL1Embedding(db, id, l1_embedding) {
2333
2403
  db.prepare("UPDATE user_memos SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2334
2404
  }
2335
2405
  export function getUserMemosWithoutL1Embedding(db, limit) {
2336
- return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2406
+ return db.prepare("SELECT * FROM user_memos WHERE memo_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2337
2407
  }
2338
2408
  export function getUserMemosWithL1Embedding(db, scope, limit = 100) {
2339
2409
  if (scope) {
2340
- 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);
2410
+ return db.prepare("SELECT * FROM user_memos WHERE memo_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2341
2411
  }
2342
- return db.prepare("SELECT * FROM user_memos WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2412
+ return db.prepare("SELECT * FROM user_memos WHERE memo_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2343
2413
  }
2344
2414
  // 2026-03-04 修正 (Issue #54): content → l1_content
2345
2415
  export function insertUserPlan(db, params) {
2346
2416
  const id = ulid();
2347
2417
  const now = new Date().toISOString();
2348
2418
  db.prepare(`
2349
- 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)
2419
+ INSERT INTO user_plans (id, title, l1_content, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, plan_status, created_at, updated_at)
2350
2420
  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)
2351
2421
  `).run({
2352
2422
  id,
@@ -2400,9 +2470,13 @@ export function updateUserPlan(db, id, updates) {
2400
2470
  setClauses.push("search_summary = @search_summary");
2401
2471
  values.search_summary = updates.search_summary;
2402
2472
  }
2403
- if (updates.status !== undefined) {
2404
- setClauses.push("status = @status");
2405
- values.status = updates.status;
2473
+ if (updates.plan_status !== undefined) {
2474
+ setClauses.push("plan_status = @plan_status");
2475
+ values.plan_status = updates.plan_status;
2476
+ }
2477
+ if (updates.is_archived !== undefined) {
2478
+ setClauses.push("is_archived = @is_archived");
2479
+ values.is_archived = updates.is_archived;
2406
2480
  }
2407
2481
  if (setClauses.length === 0)
2408
2482
  return existing;
@@ -2414,11 +2488,11 @@ export function updateUserPlan(db, id, updates) {
2414
2488
  export function listUserPlans(db, params) {
2415
2489
  const sanitized = params.query ? sanitizeFtsQuery(params.query) : null;
2416
2490
  const limit = params.limit ?? 20;
2417
- const conditions = [];
2491
+ const conditions = ["m.is_archived = 0"];
2418
2492
  const values = { limit };
2419
2493
  if (params.status) {
2420
- conditions.push("m.status = @status");
2421
- values.status = params.status;
2494
+ conditions.push("m.plan_status = @plan_status");
2495
+ values.plan_status = params.status;
2422
2496
  }
2423
2497
  if (params.usage_policy) {
2424
2498
  conditions.push("m.usage_policy = @usage_policy");
@@ -2464,27 +2538,29 @@ export function listUserPlans(db, params) {
2464
2538
  }
2465
2539
  export function listAutoUserPlans(db, scope, limit = 10) {
2466
2540
  if (scope) {
2467
- 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);
2541
+ return db.prepare("SELECT * FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND usage_policy = 'auto' AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2468
2542
  }
2469
- return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2543
+ return db.prepare("SELECT * FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2470
2544
  }
2471
2545
  /**
2472
2546
  * Hook 用: アクティブなプランの軽量情報を取得する。
2473
2547
  * auto compact 後にLLMがプランを再認識できるよう、Hook の additionalContext に注入する。
2474
2548
  * l1_content は先頭500文字に切り詰めてトークン消費を抑える。
2475
2549
  */
2476
- export function getActivePlansForHook(db, scope, limit = 3) {
2550
+ // 2026-03-25 修正: limit デフォルト 3→2、プレビュー 500→200 文字に短縮
2551
+ // 元の実装: limit=3, 500文字 → Hook の情報量が多いとのフィードバック
2552
+ export function getActivePlansForHook(db, scope, limit = 2) {
2477
2553
  let rows;
2478
2554
  if (scope) {
2479
- rows = db.prepare("SELECT id, title, l1_content, updated_at FROM user_plans WHERE status = 'active' AND usage_policy = 'auto' AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2555
+ rows = db.prepare("SELECT id, title, l1_content, updated_at FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND usage_policy = 'auto' AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2480
2556
  }
2481
2557
  else {
2482
- rows = db.prepare("SELECT id, title, l1_content, updated_at FROM user_plans WHERE status = 'active' AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2558
+ rows = db.prepare("SELECT id, title, l1_content, updated_at FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND usage_policy = 'auto' ORDER BY updated_at DESC LIMIT ?").all(limit);
2483
2559
  }
2484
2560
  return rows.map(r => ({
2485
2561
  id: r.id,
2486
2562
  title: r.title,
2487
- l1_content_preview: r.l1_content.length > 500 ? r.l1_content.slice(0, 500) + "…" : r.l1_content,
2563
+ l1_content_preview: r.l1_content.length > 200 ? r.l1_content.slice(0, 200) + "…" : r.l1_content,
2488
2564
  updated_at: r.updated_at,
2489
2565
  }));
2490
2566
  }
@@ -2492,13 +2568,13 @@ export function updateUserPlanL1Embedding(db, id, l1_embedding) {
2492
2568
  db.prepare("UPDATE user_plans SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2493
2569
  }
2494
2570
  export function getUserPlansWithoutL1Embedding(db, limit) {
2495
- return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2571
+ return db.prepare("SELECT * FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2496
2572
  }
2497
2573
  export function getUserPlansWithL1Embedding(db, scope, limit = 100) {
2498
2574
  if (scope) {
2499
- 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);
2575
+ return db.prepare("SELECT * FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2500
2576
  }
2501
- return db.prepare("SELECT * FROM user_plans WHERE status = 'active' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2577
+ return db.prepare("SELECT * FROM user_plans WHERE plan_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2502
2578
  }
2503
2579
  // === Issue #57: UserIssue CRUD ===
2504
2580
  // 2026-03-04 修正 (Issue #54): content → l1_content
@@ -2506,7 +2582,7 @@ export function insertUserIssue(db, params) {
2506
2582
  const id = ulid();
2507
2583
  const now = new Date().toISOString();
2508
2584
  db.prepare(`
2509
- INSERT INTO user_issues (id, title, l1_content, entries, kind, priority, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, status, created_at, updated_at)
2585
+ INSERT INTO user_issues (id, title, l1_content, entries, kind, priority, usage_policy, tags, scope, search_summary, user_input, l1_embedding, source_tool, client_name, client_version, issue_status, created_at, updated_at)
2510
2586
  VALUES (@id, @title, @l1_content, @entries, @kind, @priority, @usage_policy, @tags, @scope, @search_summary, @user_input, @l1_embedding, @source_tool, @client_name, @client_version, 'open', @created_at, @updated_at)
2511
2587
  `).run({
2512
2588
  id,
@@ -2551,6 +2627,10 @@ export function updateUserIssue(db, id, updates) {
2551
2627
  setClauses.push("entries = @entries");
2552
2628
  values.entries = updates.entries;
2553
2629
  }
2630
+ if (updates.l1_embedding !== undefined) {
2631
+ setClauses.push("l1_embedding = @l1_embedding");
2632
+ values.l1_embedding = updates.l1_embedding;
2633
+ }
2554
2634
  if (updates.priority !== undefined) {
2555
2635
  setClauses.push("priority = @priority");
2556
2636
  values.priority = updates.priority;
@@ -2571,9 +2651,13 @@ export function updateUserIssue(db, id, updates) {
2571
2651
  setClauses.push("search_summary = @search_summary");
2572
2652
  values.search_summary = updates.search_summary;
2573
2653
  }
2574
- if (updates.status !== undefined) {
2575
- setClauses.push("status = @status");
2576
- values.status = updates.status;
2654
+ if (updates.issue_status !== undefined) {
2655
+ setClauses.push("issue_status = @issue_status");
2656
+ values.issue_status = updates.issue_status;
2657
+ }
2658
+ if (updates.is_archived !== undefined) {
2659
+ setClauses.push("is_archived = @is_archived");
2660
+ values.is_archived = updates.is_archived;
2577
2661
  }
2578
2662
  if (setClauses.length === 0)
2579
2663
  return existing;
@@ -2585,11 +2669,11 @@ export function updateUserIssue(db, id, updates) {
2585
2669
  export function listUserIssues(db, params) {
2586
2670
  const sanitized = params.query ? sanitizeFtsQuery(params.query) : null;
2587
2671
  const limit = params.limit ?? 20;
2588
- const conditions = [];
2672
+ const conditions = ["m.is_archived = 0"];
2589
2673
  const values = { limit };
2590
2674
  if (params.status) {
2591
- conditions.push("m.status = @status");
2592
- values.status = params.status;
2675
+ conditions.push("m.issue_status = @issue_status");
2676
+ values.issue_status = params.status;
2593
2677
  }
2594
2678
  if (params.priority) {
2595
2679
  conditions.push("m.priority = @priority");
@@ -2641,13 +2725,168 @@ export function updateUserIssueL1Embedding(db, id, l1_embedding) {
2641
2725
  db.prepare("UPDATE user_issues SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2642
2726
  }
2643
2727
  export function getUserIssuesWithoutL1Embedding(db, limit) {
2644
- return db.prepare("SELECT * FROM user_issues WHERE status = 'open' AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2728
+ return db.prepare("SELECT * FROM user_issues WHERE issue_status = 'open' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2645
2729
  }
2646
2730
  export function getUserIssuesWithL1Embedding(db, scope, limit = 100) {
2647
2731
  if (scope) {
2648
- 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);
2732
+ return db.prepare("SELECT * FROM user_issues WHERE issue_status = 'open' AND is_archived = 0 AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2649
2733
  }
2650
- return db.prepare("SELECT * FROM user_issues WHERE status = 'open' AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2734
+ return db.prepare("SELECT * FROM user_issues WHERE issue_status = 'open' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2735
+ }
2736
+ export function insertUserTopic(db, params) {
2737
+ const id = ulid();
2738
+ const now = new Date().toISOString();
2739
+ // device_id を自動取得(store_meta から)
2740
+ let deviceId = params.device_id ?? null;
2741
+ if (!deviceId) {
2742
+ const row = db.prepare("SELECT value FROM store_meta WHERE key = 'device_id'").get();
2743
+ deviceId = row?.value ?? null;
2744
+ }
2745
+ db.prepare(`
2746
+ INSERT INTO user_topics (id, title, summary, device_id, cwd, conversation_id, priority, tags, scope, search_summary, l1_embedding, source_tool, client_name, client_version, topic_status, created_at, updated_at)
2747
+ VALUES (@id, @title, @summary, @device_id, @cwd, @conversation_id, @priority, @tags, @scope, @search_summary, @l1_embedding, @source_tool, @client_name, @client_version, 'active', @created_at, @updated_at)
2748
+ `).run({
2749
+ id,
2750
+ title: params.title,
2751
+ summary: params.summary,
2752
+ device_id: deviceId,
2753
+ cwd: params.cwd ?? null,
2754
+ conversation_id: params.conversation_id ?? null,
2755
+ priority: params.priority ?? "medium",
2756
+ tags: JSON.stringify(params.tags),
2757
+ scope: params.scope ?? "global",
2758
+ search_summary: params.search_summary ?? null,
2759
+ l1_embedding: params.l1_embedding ?? null,
2760
+ source_tool: params.source_tool ?? null,
2761
+ client_name: params.client_name ?? null,
2762
+ client_version: params.client_version ?? null,
2763
+ created_at: now,
2764
+ updated_at: now,
2765
+ });
2766
+ return db.prepare("SELECT * FROM user_topics WHERE id = ?").get(id);
2767
+ }
2768
+ export function getUserTopicById(db, id) {
2769
+ return db.prepare("SELECT * FROM user_topics WHERE id = ?").get(id);
2770
+ }
2771
+ export function updateUserTopic(db, id, updates) {
2772
+ const existing = getUserTopicById(db, id);
2773
+ if (!existing)
2774
+ return undefined;
2775
+ const setClauses = [];
2776
+ const values = { id };
2777
+ if (updates.title !== undefined) {
2778
+ setClauses.push("title = @title");
2779
+ values.title = updates.title;
2780
+ }
2781
+ if (updates.summary !== undefined) {
2782
+ setClauses.push("summary = @summary");
2783
+ values.summary = updates.summary;
2784
+ }
2785
+ if (updates.conversation_id !== undefined) {
2786
+ setClauses.push("conversation_id = @conversation_id");
2787
+ values.conversation_id = updates.conversation_id;
2788
+ }
2789
+ if (updates.cwd !== undefined) {
2790
+ setClauses.push("cwd = @cwd");
2791
+ values.cwd = updates.cwd;
2792
+ }
2793
+ if (updates.priority !== undefined) {
2794
+ setClauses.push("priority = @priority");
2795
+ values.priority = updates.priority;
2796
+ }
2797
+ if (updates.tags !== undefined) {
2798
+ setClauses.push("tags = @tags");
2799
+ values.tags = JSON.stringify(updates.tags);
2800
+ }
2801
+ if (updates.scope !== undefined) {
2802
+ setClauses.push("scope = @scope");
2803
+ values.scope = updates.scope;
2804
+ }
2805
+ if (updates.search_summary !== undefined) {
2806
+ setClauses.push("search_summary = @search_summary");
2807
+ values.search_summary = updates.search_summary;
2808
+ }
2809
+ if (updates.topic_status !== undefined) {
2810
+ setClauses.push("topic_status = @topic_status");
2811
+ values.topic_status = updates.topic_status;
2812
+ }
2813
+ if (updates.is_archived !== undefined) {
2814
+ setClauses.push("is_archived = @is_archived");
2815
+ values.is_archived = updates.is_archived;
2816
+ }
2817
+ if (setClauses.length === 0)
2818
+ return existing;
2819
+ // summary/title 変更時は l1_embedding を無効化(陳腐化防止、backfill_embeddings で再生成)
2820
+ if (updates.summary !== undefined || updates.title !== undefined) {
2821
+ setClauses.push("l1_embedding = NULL");
2822
+ }
2823
+ setClauses.push("updated_at = @updated_at");
2824
+ values.updated_at = new Date().toISOString();
2825
+ db.prepare(`UPDATE user_topics SET ${setClauses.join(", ")} WHERE id = @id`).run(values);
2826
+ return db.prepare("SELECT * FROM user_topics WHERE id = ?").get(id);
2827
+ }
2828
+ export function listUserTopics(db, params) {
2829
+ const sanitized = params.query ? sanitizeFtsQuery(params.query) : null;
2830
+ const limit = params.limit ?? 20;
2831
+ const conditions = ["m.is_archived = 0"];
2832
+ const values = { limit };
2833
+ if (params.status) {
2834
+ conditions.push("m.topic_status = @topic_status");
2835
+ values.topic_status = params.status;
2836
+ }
2837
+ if (params.priority) {
2838
+ conditions.push("m.priority = @priority");
2839
+ values.priority = params.priority;
2840
+ }
2841
+ if (params.tag) {
2842
+ conditions.push("m.tags LIKE @tag_like");
2843
+ values.tag_like = `%"${params.tag}"%`;
2844
+ }
2845
+ if (params.scope) {
2846
+ conditions.push("(m.scope = @scope OR m.scope = 'global')");
2847
+ values.scope = params.scope;
2848
+ }
2849
+ // FTS5検索
2850
+ if (sanitized) {
2851
+ try {
2852
+ const ftsConditions = [...conditions];
2853
+ const ftsWhere = ftsConditions.length > 0 ? "AND " + ftsConditions.join(" AND ") : "";
2854
+ const ftsResults = db.prepare(`
2855
+ SELECT m.* FROM user_topics m
2856
+ JOIN user_topics_fts f ON m.rowid = f.rowid
2857
+ WHERE user_topics_fts MATCH @query ${ftsWhere}
2858
+ ORDER BY rank
2859
+ LIMIT @limit
2860
+ `).all({ ...values, query: sanitized });
2861
+ if (ftsResults.length > 0)
2862
+ return ftsResults;
2863
+ }
2864
+ catch { /* FTSエラー時はLIKEフォールバック */ }
2865
+ }
2866
+ // LIKEフォールバック or 全件取得
2867
+ if (params.query) {
2868
+ const likeParam = `%${params.query.slice(0, 100)}%`;
2869
+ conditions.push("(m.title LIKE @like_query OR m.summary LIKE @like_query OR m.search_summary LIKE @like_query)");
2870
+ values.like_query = likeParam;
2871
+ }
2872
+ const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
2873
+ return db.prepare(`
2874
+ SELECT m.* FROM user_topics m ${whereClause}
2875
+ ORDER BY m.updated_at DESC
2876
+ LIMIT @limit
2877
+ `).all(values);
2878
+ }
2879
+ export function updateUserTopicL1Embedding(db, id, l1_embedding) {
2880
+ db.prepare("UPDATE user_topics SET l1_embedding = @l1_embedding WHERE id = @id").run({ id, l1_embedding });
2881
+ }
2882
+ export function getUserTopicsWithoutL1Embedding(db, limit) {
2883
+ return db.prepare("SELECT * FROM user_topics WHERE topic_status = 'active' AND is_archived = 0 AND l1_embedding IS NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2884
+ }
2885
+ export function getUserTopicsWithL1Embedding(db, scope, limit = 100) {
2886
+ if (scope) {
2887
+ return db.prepare("SELECT * FROM user_topics WHERE topic_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL AND (scope = ? OR scope = 'global') ORDER BY updated_at DESC LIMIT ?").all(scope, limit);
2888
+ }
2889
+ return db.prepare("SELECT * FROM user_topics WHERE topic_status = 'active' AND is_archived = 0 AND l1_embedding IS NOT NULL ORDER BY updated_at DESC LIMIT ?").all(limit);
2651
2890
  }
2652
2891
  /**
2653
2892
  * 横断検索(FTS5 + LIKEフォールバック)
@@ -2662,11 +2901,13 @@ export function unifiedSearch(db, query, scope, entityTypes, limit) {
2662
2901
  scopeFilter = "AND (u.scope = @scope OR u.scope = 'global')";
2663
2902
  baseValues.scope = scope;
2664
2903
  }
2904
+ // entity_types 未指定時はデフォルト検索対象を使用
2905
+ const effectiveTypes = (entityTypes && entityTypes.length > 0) ? entityTypes : [...DEFAULT_SEARCH_ENTITY_TYPES];
2665
2906
  let typeFilter = "";
2666
- if (entityTypes && entityTypes.length > 0) {
2667
- const placeholders = entityTypes.map((_, i) => `@et${i}`).join(", ");
2907
+ {
2908
+ const placeholders = effectiveTypes.map((_, i) => `@et${i}`).join(", ");
2668
2909
  typeFilter = `AND u.entity_type IN (${placeholders})`;
2669
- entityTypes.forEach((t, i) => { baseValues[`et${i}`] = t; });
2910
+ effectiveTypes.forEach((t, i) => { baseValues[`et${i}`] = t; });
2670
2911
  }
2671
2912
  // 1. FTS5検索
2672
2913
  if (sanitized) {
@@ -2727,10 +2968,12 @@ export function getUnifiedSearchWithL1Embedding(db, scope, entityTypes, limit) {
2727
2968
  conditions.push("(scope = @scope OR scope = 'global')");
2728
2969
  values.scope = scope;
2729
2970
  }
2730
- if (entityTypes && entityTypes.length > 0) {
2731
- const placeholders = entityTypes.map((_, i) => `@et${i}`).join(", ");
2971
+ // entity_types 未指定時はデフォルト検索対象を使用
2972
+ const effectiveTypes = (entityTypes && entityTypes.length > 0) ? entityTypes : [...DEFAULT_SEARCH_ENTITY_TYPES];
2973
+ {
2974
+ const placeholders = effectiveTypes.map((_, i) => `@et${i}`).join(", ");
2732
2975
  conditions.push(`entity_type IN (${placeholders})`);
2733
- entityTypes.forEach((t, i) => { values[`et${i}`] = t; });
2976
+ effectiveTypes.forEach((t, i) => { values[`et${i}`] = t; });
2734
2977
  }
2735
2978
  const whereClause = conditions.length > 0
2736
2979
  ? "WHERE " + conditions.join(" AND ")
@@ -2813,7 +3056,7 @@ export function rebuildUnifiedSearch(db) {
2813
3056
  INSERT OR REPLACE INTO unified_search_items(${columns})
2814
3057
  SELECT '${e.type}', id, scope, ${e.categoryExpr}, ${e.titleExpr}, ${e.searchExpr}, ${e.searchSummary}, ${e.tagsExpr}, created_at, updated_at
2815
3058
  FROM ${e.table}
2816
- WHERE status = 'active'
3059
+ WHERE validity_status = 'active' AND is_archived = 0 AND promoted_to IS NULL
2817
3060
  `);
2818
3061
  }
2819
3062
  db.exec("INSERT INTO unified_search_fts(unified_search_fts) VALUES('rebuild')");
@@ -2834,6 +3077,8 @@ const ENTITY_TYPE_TO_TABLE = {
2834
3077
  user_memo: "user_memos",
2835
3078
  user_plan: "user_plans",
2836
3079
  user_issue: "user_issues",
3080
+ user_topic: "user_topics",
3081
+ user_file: "user_files",
2837
3082
  };
2838
3083
  /**
2839
3084
  * entity_id から entity_type を解決する(unified_search_items を使用)。
@@ -2982,4 +3227,119 @@ export function applyTombstones(db) {
2982
3227
  }
2983
3228
  return deleted;
2984
3229
  }
3230
+ // ============================================================
3231
+ // UserFile (File Vault)
3232
+ // ============================================================
3233
+ export function insertUserFile(db, params) {
3234
+ const id = ulid();
3235
+ const now = new Date().toISOString();
3236
+ // device_id を自動取得(store_meta から)
3237
+ let deviceId = params.device_id ?? null;
3238
+ if (!deviceId) {
3239
+ const row = db.prepare("SELECT value FROM store_meta WHERE key = 'device_id'").get();
3240
+ deviceId = row?.value ?? null;
3241
+ }
3242
+ db.prepare(`
3243
+ INSERT INTO user_files (id, title, description, device_id, original_filename, original_encoding, file_data, file_hash, file_size, tags, scope, search_summary, l1_embedding, source_tool, client_name, client_version, file_status, created_at, updated_at)
3244
+ VALUES (@id, @title, @description, @device_id, @original_filename, @original_encoding, @file_data, @file_hash, @file_size, @tags, @scope, @search_summary, @l1_embedding, @source_tool, @client_name, @client_version, 'active', @created_at, @updated_at)
3245
+ `).run({
3246
+ id,
3247
+ title: params.title,
3248
+ description: params.description ?? null,
3249
+ device_id: deviceId,
3250
+ original_filename: params.original_filename,
3251
+ original_encoding: params.original_encoding ?? "utf-8",
3252
+ file_data: params.file_data,
3253
+ file_hash: params.file_hash,
3254
+ file_size: params.file_size,
3255
+ tags: JSON.stringify(params.tags),
3256
+ scope: params.scope ?? "global",
3257
+ search_summary: params.search_summary ?? null,
3258
+ l1_embedding: params.l1_embedding ?? null,
3259
+ source_tool: params.source_tool ?? null,
3260
+ client_name: params.client_name ?? null,
3261
+ client_version: params.client_version ?? null,
3262
+ created_at: now,
3263
+ updated_at: now,
3264
+ });
3265
+ return db.prepare("SELECT * FROM user_files WHERE id = ?").get(id);
3266
+ }
3267
+ export function getUserFileById(db, id) {
3268
+ return db.prepare("SELECT * FROM user_files WHERE id = ?").get(id);
3269
+ }
3270
+ export function getUserFileByTitle(db, title) {
3271
+ // 完全一致を優先、なければ部分一致
3272
+ const exact = db.prepare("SELECT * FROM user_files WHERE title = ? AND file_status = 'active' AND is_archived = 0").get(title);
3273
+ if (exact)
3274
+ return exact;
3275
+ return db.prepare("SELECT * FROM user_files WHERE title LIKE ? AND file_status = 'active' AND is_archived = 0 ORDER BY updated_at DESC LIMIT 1").get(`%${title}%`);
3276
+ }
3277
+ export function updateUserFile(db, id, updates) {
3278
+ const setClauses = [];
3279
+ const params = { id };
3280
+ if (updates.title !== undefined) {
3281
+ setClauses.push("title = @title");
3282
+ params.title = updates.title;
3283
+ }
3284
+ if (updates.description !== undefined) {
3285
+ setClauses.push("description = @description");
3286
+ params.description = updates.description;
3287
+ }
3288
+ if (updates.file_data !== undefined) {
3289
+ setClauses.push("file_data = @file_data");
3290
+ params.file_data = updates.file_data;
3291
+ }
3292
+ if (updates.file_hash !== undefined) {
3293
+ setClauses.push("file_hash = @file_hash");
3294
+ params.file_hash = updates.file_hash;
3295
+ }
3296
+ if (updates.file_size !== undefined) {
3297
+ setClauses.push("file_size = @file_size");
3298
+ params.file_size = updates.file_size;
3299
+ }
3300
+ if (updates.tags !== undefined) {
3301
+ setClauses.push("tags = @tags");
3302
+ params.tags = JSON.stringify(updates.tags);
3303
+ }
3304
+ if (updates.scope !== undefined) {
3305
+ setClauses.push("scope = @scope");
3306
+ params.scope = updates.scope;
3307
+ }
3308
+ if (updates.search_summary !== undefined) {
3309
+ setClauses.push("search_summary = @search_summary");
3310
+ params.search_summary = updates.search_summary;
3311
+ }
3312
+ if (updates.file_status !== undefined) {
3313
+ setClauses.push("file_status = @file_status");
3314
+ params.file_status = updates.file_status;
3315
+ }
3316
+ if (updates.is_archived !== undefined) {
3317
+ setClauses.push("is_archived = @is_archived");
3318
+ params.is_archived = updates.is_archived;
3319
+ }
3320
+ if (setClauses.length === 0)
3321
+ return getUserFileById(db, id);
3322
+ setClauses.push("updated_at = @updated_at");
3323
+ params.updated_at = new Date().toISOString();
3324
+ db.prepare(`UPDATE user_files SET ${setClauses.join(", ")} WHERE id = @id`).run(params);
3325
+ return getUserFileById(db, id);
3326
+ }
3327
+ export function listUserFiles(db, params) {
3328
+ const conditions = ["file_status = @file_status", "is_archived = @is_archived"];
3329
+ const bindParams = { file_status: params.file_status ?? "active", is_archived: params.is_archived ?? 0 };
3330
+ if (params.scope) {
3331
+ conditions.push("(scope = @scope OR scope = 'global')");
3332
+ bindParams.scope = params.scope;
3333
+ }
3334
+ const sql = `SELECT id, title, description, original_filename, original_encoding, file_hash, file_size, tags, scope, search_summary, file_status, is_archived, client_name, client_version, source_tool, created_at, updated_at FROM user_files WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC`;
3335
+ let rows = db.prepare(sql).all(bindParams);
3336
+ // タグフィルタリング(アプリケーション層)
3337
+ if (params.tags && params.tags.length > 0) {
3338
+ rows = rows.filter(r => {
3339
+ const rowTags = JSON.parse(r.tags);
3340
+ return params.tags.some(t => rowTags.includes(t));
3341
+ });
3342
+ }
3343
+ return rows;
3344
+ }
2985
3345
  //# sourceMappingURL=queries-core.js.map