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