thincoder 0.7.8 → 0.8.0
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 +32 -13
- package/bin/thincoder.mjs +27 -346
- package/package.json +1 -1
- package/src/agent/dispatch.mjs +98 -0
- package/src/agent/helpers.mjs +185 -0
- package/src/agent/setup.mjs +117 -0
- package/src/agent-tools/goal.mjs +71 -0
- package/src/agent-tools/plan.mjs +31 -0
- package/src/agent-tools/recent-changes.mjs +23 -0
- package/src/agent-tools/skill.mjs +46 -0
- package/src/agent-tools/subagent.mjs +113 -0
- package/src/agent-tools/task.mjs +67 -0
- package/src/agent-tools/verify.mjs +198 -0
- package/src/agent-tools.mjs +12 -0
- package/src/agent.mjs +90 -1040
- package/src/cli/distill-command.mjs +85 -0
- package/src/cli/make-agent.mjs +85 -0
- package/src/cli/memory-command.mjs +63 -0
- package/src/cli/permission.mjs +41 -0
- package/src/cli/setup-wizard.mjs +70 -0
- package/src/config.mjs +5 -8
- package/src/context.mjs +10 -13
- package/src/distill.mjs +4 -3
- package/src/embedding.mjs +4 -2
- package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
- package/src/mcp/helpers.mjs +37 -0
- package/src/mcp/transport-http.mjs +176 -0
- package/src/mcp/transport-stdio.mjs +84 -0
- package/src/mcp/transport-ws.mjs +87 -0
- package/src/mcp.mjs +4 -428
- package/src/memory/code-index.mjs +211 -0
- package/src/memory/code-sync.mjs +306 -0
- package/src/memory/core.mjs +277 -0
- package/src/memory/docs.mjs +262 -0
- package/src/memory/schema.mjs +426 -0
- package/src/memory.mjs +12 -1403
- package/src/provider/core.mjs +239 -0
- package/src/provider/index.mjs +6 -0
- package/src/provider/rate.mjs +104 -0
- package/src/session.mjs +18 -5
- package/src/tools/bash.mjs +144 -0
- package/src/tools/file.mjs +205 -0
- package/src/tools/git.mjs +166 -0
- package/src/tools/glob.mjs +51 -0
- package/src/tools/grep.mjs +100 -0
- package/src/tools/index.mjs +22 -0
- package/src/tools/ls.mjs +36 -0
- package/src/tools/patch.mjs +226 -0
- package/src/tools/repomap-parse.mjs +168 -0
- package/src/tools/shared.mjs +257 -0
- package/src/tools/system.mjs +336 -0
- package/src/tools/web.mjs +121 -0
- package/src/tools.mjs +2 -1194
- package/src/tui/agent-turn.mjs +254 -0
- package/src/tui/ansi.mjs +32 -0
- package/src/tui/clipboard.mjs +48 -0
- package/src/tui/cmd-auto.mjs +21 -0
- package/src/tui/cmd-clear.mjs +26 -0
- package/src/tui/cmd-config.mjs +72 -0
- package/src/tui/cmd-exit.mjs +5 -0
- package/src/tui/cmd-extract.mjs +5 -0
- package/src/tui/cmd-goal.mjs +47 -0
- package/src/tui/cmd-help.mjs +25 -0
- package/src/tui/cmd-init.mjs +91 -0
- package/src/tui/cmd-mcp.mjs +146 -0
- package/src/tui/cmd-model.mjs +7 -0
- package/src/tui/cmd-new.mjs +18 -0
- package/src/tui/cmd-plan.mjs +21 -0
- package/src/tui/cmd-reindex.mjs +44 -0
- package/src/tui/cmd-restore.mjs +39 -0
- package/src/tui/cmd-session.mjs +42 -0
- package/src/tui/cmd-skills.mjs +17 -0
- package/src/tui/cmd-think.mjs +56 -0
- package/src/tui/config-helpers.mjs +34 -0
- package/src/tui/distill-cmd.mjs +45 -0
- package/src/tui/index.mjs +330 -0
- package/src/tui/interaction.mjs +79 -0
- package/src/tui/key-handler.mjs +267 -0
- package/src/tui/layout.mjs +115 -0
- package/src/tui/pickers.mjs +279 -0
- package/src/tui/render-frame.mjs +304 -0
- package/src/tui/render.mjs +205 -0
- package/src/tui/slash-commands.mjs +138 -0
- package/src/tui/startup.mjs +113 -0
- package/src/tui/wizard.mjs +168 -0
- package/src/tui-render.mjs +4 -0
- package/src/tui.mjs +3 -2566
- package/src/provider.mjs +0 -383
- /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
- /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
- /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
- /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
- /package/src/{main-overlay.md → prompts/main.md} +0 -0
- /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
- /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
- /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory/docs.mjs — 文档索引同步、检索、agent 工具生成
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, readdir, stat } from "node:fs/promises"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
|
|
8
|
+
import { commitAndPush } from "../git/gitmem.mjs"
|
|
9
|
+
import { DOC_EXTS, SKIP_DIRS } from "./schema.mjs"
|
|
10
|
+
import { buildFtsQuery, put, search, putMarkdown } from "./core.mjs"
|
|
11
|
+
import { _upsertDocFile, yieldTick } from "./code-index.mjs"
|
|
12
|
+
import { markIndexedCommit } from "./code-sync.mjs"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 同步文档索引:扫描 dir 下所有 .md/.mdc/.txt/.rst/.adoc → 分块 → upsert 到 doc_chunks。
|
|
16
|
+
* 按 mtime 增量。
|
|
17
|
+
*/
|
|
18
|
+
export async function docSync(memory, dir, { onProgress } = {}) {
|
|
19
|
+
const files = []
|
|
20
|
+
async function walk(d) {
|
|
21
|
+
let entries
|
|
22
|
+
try { entries = await readdir(d, { withFileTypes: true }) } catch { return }
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
if (e.isDirectory()) {
|
|
25
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith(".")) continue
|
|
26
|
+
await walk(join(d, e.name))
|
|
27
|
+
} else if (e.isFile()) {
|
|
28
|
+
const ext = e.name.slice(e.name.lastIndexOf(".")).toLowerCase()
|
|
29
|
+
if (DOC_EXTS.has(ext)) files.push(join(d, e.name))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
await walk(dir)
|
|
34
|
+
|
|
35
|
+
const indexed = new Map(
|
|
36
|
+
memory.db.prepare(`SELECT path, mtime_ms FROM doc_chunks WHERE origin = ?`).all(dir).map((r) => [r.path, r.mtime_ms])
|
|
37
|
+
)
|
|
38
|
+
const seen = new Set()
|
|
39
|
+
|
|
40
|
+
onProgress?.({ phase: "scan", total: files.length })
|
|
41
|
+
|
|
42
|
+
let updated = 0, removed = 0, skipped = 0, failed = 0
|
|
43
|
+
const errors = []
|
|
44
|
+
for (let i = 0; i < files.length; i++) {
|
|
45
|
+
const abs = files[i]
|
|
46
|
+
const rel = abs.slice(dir.length + 1).replaceAll("\\", "/")
|
|
47
|
+
seen.add(rel)
|
|
48
|
+
|
|
49
|
+
let mtimeMs
|
|
50
|
+
try { mtimeMs = Math.floor((await stat(abs)).mtimeMs) } catch { continue }
|
|
51
|
+
if (indexed.get(rel) === mtimeMs) {
|
|
52
|
+
skipped++
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const text = await readFile(abs, "utf8")
|
|
58
|
+
const lines = text.split("\n")
|
|
59
|
+
_upsertDocFile(memory, dir, rel, lines, mtimeMs)
|
|
60
|
+
updated++
|
|
61
|
+
} catch (e) {
|
|
62
|
+
failed++
|
|
63
|
+
if (errors.length < 5) errors.push(`${rel}: ${e.message}`)
|
|
64
|
+
}
|
|
65
|
+
await yieldTick()
|
|
66
|
+
|
|
67
|
+
if (onProgress && i % 10 === 0) {
|
|
68
|
+
onProgress({ phase: "index", current: i + 1, total: files.length, updated, removed, skipped, failed })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const stale of indexed.keys()) {
|
|
73
|
+
if (!seen.has(stale)) {
|
|
74
|
+
memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(dir, stale)
|
|
75
|
+
removed++
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed })
|
|
80
|
+
markIndexedCommit(memory, dir)
|
|
81
|
+
return { updated, removed, skipped, failed, errors, total: files.length }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 文档检索:FTS5(BM25) + 可选向量余弦,RRF 合并。
|
|
86
|
+
* 无 embedder 时退化为纯 FTS;ftsQuery 为空且有 embedder 时退化为纯向量。
|
|
87
|
+
*/
|
|
88
|
+
export async function docSearch(memory, query, { limit = 5 } = {}) {
|
|
89
|
+
const ftsQuery = buildFtsQuery(query)
|
|
90
|
+
if (!ftsQuery && !memory.embedder) return []
|
|
91
|
+
|
|
92
|
+
const ftsOriginFilter = memory.codeOrigin ? `AND d.origin = ?` : ""
|
|
93
|
+
const vecOriginFilter = memory.codeOrigin ? `AND origin = ?` : ""
|
|
94
|
+
const originParams = memory.codeOrigin ? [memory.codeOrigin] : []
|
|
95
|
+
|
|
96
|
+
const ftsList = ftsQuery ? memory.db.prepare(`
|
|
97
|
+
SELECT d.rowid, d.path, d.language, d.heading, d.content, d.line_start, d.line_end, bm25(doc_chunks_fts) AS rank
|
|
98
|
+
FROM doc_chunks_fts JOIN doc_chunks d ON d.rowid = doc_chunks_fts.rowid
|
|
99
|
+
WHERE doc_chunks_fts MATCH ? ${ftsOriginFilter}
|
|
100
|
+
ORDER BY rank LIMIT ?
|
|
101
|
+
`).all(ftsQuery, ...originParams, Math.max(limit * 4, 20)) : []
|
|
102
|
+
|
|
103
|
+
if (!memory.embedder) return ftsList.slice(0, limit)
|
|
104
|
+
|
|
105
|
+
try { await ensureDocEmbeddings(memory) } catch (e) {
|
|
106
|
+
console.error(`[docs] embedding ensure failed, falling back to FTS-only: ${e.message}`)
|
|
107
|
+
return ftsList.slice(0, limit)
|
|
108
|
+
}
|
|
109
|
+
let qvec
|
|
110
|
+
try { [qvec] = await embed(memory.embedder, [query]) } catch (e) {
|
|
111
|
+
console.error(`[docs] query embedding failed, falling back to FTS-only: ${e.message}`)
|
|
112
|
+
return ftsList.slice(0, limit)
|
|
113
|
+
}
|
|
114
|
+
const rows = memory.db.prepare(`SELECT rowid, embedding FROM doc_chunks WHERE embedding IS NOT NULL ${vecOriginFilter}`).all(...originParams)
|
|
115
|
+
const vecList = rows
|
|
116
|
+
.map((r) => ({ rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }))
|
|
117
|
+
.sort((a, b) => b.score - a.score)
|
|
118
|
+
.slice(0, Math.max(limit * 4, 20))
|
|
119
|
+
|
|
120
|
+
const K = 60
|
|
121
|
+
const scores = new Map()
|
|
122
|
+
ftsList.forEach((r, i) => scores.set(r.rowid, (scores.get(r.rowid) ?? 0) + 1 / (K + i + 1)))
|
|
123
|
+
vecList.forEach((r, i) => scores.set(r.rowid, (scores.get(r.rowid) ?? 0) + 1 / (K + i + 1)))
|
|
124
|
+
|
|
125
|
+
const fetchChunk = memory.db.prepare(`
|
|
126
|
+
SELECT path, language, heading, content, line_start, line_end FROM doc_chunks WHERE rowid = ?
|
|
127
|
+
`)
|
|
128
|
+
return [...scores.entries()]
|
|
129
|
+
.sort((a, b) => b[1] - a[1])
|
|
130
|
+
.slice(0, limit)
|
|
131
|
+
.map(([rowid]) => fetchChunk.get(rowid))
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 惰性补算 doc_chunks 缺失的向量 */
|
|
136
|
+
export async function ensureDocEmbeddings(memory) {
|
|
137
|
+
if (!memory.embedder) return
|
|
138
|
+
const modelKey = memory.embedder.model
|
|
139
|
+
const stored = memory.db.prepare(`SELECT value FROM meta WHERE key = 'doc_embedding_model'`).get()?.value
|
|
140
|
+
if (stored !== modelKey) {
|
|
141
|
+
memory.db.prepare(`UPDATE doc_chunks SET embedding = NULL`).run()
|
|
142
|
+
memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('doc_embedding_model', ?)
|
|
143
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(modelKey)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pending = memory.db.prepare(`SELECT rowid, path, heading, content FROM doc_chunks WHERE embedding IS NULL LIMIT 64`).all()
|
|
147
|
+
if (pending.length === 0) return
|
|
148
|
+
|
|
149
|
+
const texts = pending.map((r) => `${r.heading || r.path}\n${r.content.slice(0, 2000)}`)
|
|
150
|
+
const vecs = await embed(memory.embedder, texts)
|
|
151
|
+
|
|
152
|
+
const update = memory.db.prepare(`UPDATE doc_chunks SET embedding = ? WHERE rowid = ?`)
|
|
153
|
+
pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 生成 doc_search 工具(只读)。 */
|
|
157
|
+
export function docSearchTool(memory) {
|
|
158
|
+
return {
|
|
159
|
+
name: "doc_search",
|
|
160
|
+
description:
|
|
161
|
+
"Search the project's documentation (README, design docs, guides, markdown files) for relevant information. Use this to find design decisions, coding conventions, architecture docs, or project rules. Prefer this over code_search when you need to understand the project's intended design rather than existing implementation.",
|
|
162
|
+
parameters: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
query: { type: "string", description: "Natural language search query" },
|
|
166
|
+
limit: { type: "number", description: "Max results (default 5)" },
|
|
167
|
+
},
|
|
168
|
+
required: ["query"],
|
|
169
|
+
},
|
|
170
|
+
readonly: true,
|
|
171
|
+
async execute(args) {
|
|
172
|
+
const results = await docSearch(memory, args.query, { limit: args.limit ?? 5 })
|
|
173
|
+
if (results.length === 0) return "(no matching documentation)"
|
|
174
|
+
return results.map((r) =>
|
|
175
|
+
`${r.path}${r.heading ? ` > ${r.heading}` : ""} (L${r.line_start}-L${r.line_end}):\n${r.content.slice(0, 2000)}`
|
|
176
|
+
).join("\n\n---\n\n")
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------- agent 工具
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 生成记忆相关的两个 agent 工具(遵循 tools.mjs 的工具形状)。
|
|
185
|
+
* memory_put 是有副作用工具(需权限确认),memory_search 只读。
|
|
186
|
+
* opts: { cwd, projectDir, author, team: { dir, name } | null }
|
|
187
|
+
*/
|
|
188
|
+
export function memoryTools(memory, opts = {}) {
|
|
189
|
+
const projectDir = opts.projectDir ? join(opts.cwd ?? process.cwd(), opts.projectDir) : null
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
name: "memory_put",
|
|
193
|
+
description:
|
|
194
|
+
"Save a piece of knowledge to long-term memory. Use when you learn something worth remembering across sessions: a project convention, a debugging insight, an architecture decision. Types: rule (coding standards), knowledge (project facts), decision (architecture decisions), pattern (debugging/workflow patterns). Scopes: personal (default, private to you), project (shared via this repo's .thincoder/memory/), team (org-wide team repo, if configured).",
|
|
195
|
+
parameters: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
type: { type: "string", enum: ["rule", "knowledge", "decision", "pattern"] },
|
|
199
|
+
title: { type: "string", description: "Short title" },
|
|
200
|
+
content: { type: "string", description: "Full content to remember" },
|
|
201
|
+
tags: { type: "string", description: "Space-separated tags" },
|
|
202
|
+
scope: { type: "string", enum: ["personal", "project", "team"], description: "Where to save (default personal)" },
|
|
203
|
+
},
|
|
204
|
+
required: ["type", "title", "content"],
|
|
205
|
+
},
|
|
206
|
+
readonly: false,
|
|
207
|
+
async execute(args) {
|
|
208
|
+
const scope = args.scope ?? "personal"
|
|
209
|
+
if (scope === "personal") {
|
|
210
|
+
const id = await put(memory, args)
|
|
211
|
+
return `Saved to personal memory (id=${id}): [${args.type}] ${args.title}`
|
|
212
|
+
}
|
|
213
|
+
if (scope === "project") {
|
|
214
|
+
if (!projectDir) throw new Error("project scope unavailable: no project directory configured")
|
|
215
|
+
const filename = await putMarkdown(memory, {
|
|
216
|
+
layer: "project",
|
|
217
|
+
dir: projectDir,
|
|
218
|
+
type: args.type,
|
|
219
|
+
title: args.title,
|
|
220
|
+
content: args.content,
|
|
221
|
+
tags: (args.tags ?? "").split(/\s+/).filter(Boolean),
|
|
222
|
+
author: opts.author ?? "unknown",
|
|
223
|
+
})
|
|
224
|
+
return `Saved to project memory (${filename}): [${args.type}] ${args.title}\nNote: file written to the repo; commit it yourself when ready.`
|
|
225
|
+
}
|
|
226
|
+
if (!opts.team?.dir) {
|
|
227
|
+
throw new Error("team scope not configured: set memory.team in ~/.thincoder/config.json")
|
|
228
|
+
}
|
|
229
|
+
const filename = await putMarkdown(memory, {
|
|
230
|
+
layer: "team",
|
|
231
|
+
dir: opts.team.dir,
|
|
232
|
+
type: args.type,
|
|
233
|
+
title: args.title,
|
|
234
|
+
content: args.content,
|
|
235
|
+
tags: (args.tags ?? "").split(/\s+/).filter(Boolean),
|
|
236
|
+
author: opts.author ?? "unknown",
|
|
237
|
+
})
|
|
238
|
+
await commitAndPush(opts.team.dir, filename, `memory: [${args.type}] ${args.title}`)
|
|
239
|
+
return `Saved to team memory and pushed (${filename}): [${args.type}] ${args.title}`
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "memory_search",
|
|
244
|
+
description:
|
|
245
|
+
"Search long-term memory across all layers (personal/project/team) for relevant knowledge saved in previous sessions. Use the same language as the memories being searched.",
|
|
246
|
+
parameters: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
query: { type: "string", description: "Natural language search query" },
|
|
250
|
+
limit: { type: "number", description: "Max results (default 5)" },
|
|
251
|
+
},
|
|
252
|
+
required: ["query"],
|
|
253
|
+
},
|
|
254
|
+
readonly: true,
|
|
255
|
+
async execute(args) {
|
|
256
|
+
const results = await search(memory, args.query, { limit: args.limit ?? 5 })
|
|
257
|
+
if (results.length === 0) return "(no matching memories)"
|
|
258
|
+
return results.map((r) => `[${r.layer}][${r.type}] ${r.title}\n${r.content}`).join("\n\n")
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory/schema.mjs — 数据库 schema 定义、迁移、CJK 分词
|
|
3
|
+
* v1:node:sqlite + FTS5 单机实现,零依赖。
|
|
4
|
+
*
|
|
5
|
+
* 中文检索方案:FTS5 unicode61 分词 + CJK 逐字加空格(写入和查询两侧同样处理)。
|
|
6
|
+
* 效果:中文按字索引,"分号" 这类双字词也能命中;ASCII 仍按整词。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { DatabaseSync } from "node:sqlite"
|
|
10
|
+
import { mkdirSync } from "node:fs"
|
|
11
|
+
import { dirname } from "node:path"
|
|
12
|
+
|
|
13
|
+
export const VALID_TYPES = new Set(["rule", "knowledge", "decision", "pattern"])
|
|
14
|
+
export const SCHEMA_VERSION = 9
|
|
15
|
+
|
|
16
|
+
// 代码索引:源码文件扩展名
|
|
17
|
+
export const CODE_EXTS = new Set([".mjs", ".js", ".ts", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".h", ".cpp", ".hpp", ".rb", ".swift", ".kt", ".sh", ".bash", ".sql", ".yaml", ".yml", ".toml", ".json", ".css", ".html", ".vue", ".svelte"])
|
|
18
|
+
// 文档索引:markdown / 纯文本(分开索引,便于 LLM 区分"设计规范"和"现存代码")
|
|
19
|
+
export const DOC_EXTS = new Set([".md", ".mdc", ".txt", ".rst", ".adoc"])
|
|
20
|
+
// 总是跳过的目录名
|
|
21
|
+
export const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage", "__pycache__", ".venv", "venv", "target", ".next", ".nuxt", ".svelte-kit"])
|
|
22
|
+
// 大文件阈值(行数):超过此行数按符号分块,否则整文件入索引
|
|
23
|
+
export const BIG_FILE_LINES = 2000
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CJK 逐字加空格:让 unicode61 把每个汉字/日韩字当独立 token。
|
|
27
|
+
* 写入和查询必须使用同一处理,检索才能对上。
|
|
28
|
+
*/
|
|
29
|
+
export function segmentCJK(text) {
|
|
30
|
+
return text.replace(
|
|
31
|
+
/[-ヿ㐀-䶿一-鿿豈-가-]+/g,
|
|
32
|
+
(run) => [...run].join(" "),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 打开/初始化记忆库。dbPath 不存在会自动创建。
|
|
38
|
+
* 返回的 memory 对象即接口,后续函数的第一个参数都是它。
|
|
39
|
+
*/
|
|
40
|
+
export function createMemory({ dbPath }) {
|
|
41
|
+
mkdirSync(dirname(dbPath), { recursive: true })
|
|
42
|
+
const db = new DatabaseSync(dbPath)
|
|
43
|
+
// WAL 读写不互锁(TUI 检索和后台索引可并发);busy_timeout 防多进程同库直接 SQLITE_BUSY
|
|
44
|
+
db.exec(`PRAGMA journal_mode = WAL`)
|
|
45
|
+
db.exec(`PRAGMA busy_timeout = 3000`)
|
|
46
|
+
|
|
47
|
+
db.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
|
+
type TEXT NOT NULL CHECK(type IN ('rule','knowledge','decision','pattern')),
|
|
51
|
+
title TEXT NOT NULL,
|
|
52
|
+
content TEXT NOT NULL,
|
|
53
|
+
tags TEXT NOT NULL DEFAULT '',
|
|
54
|
+
seg_title TEXT NOT NULL DEFAULT '',
|
|
55
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
56
|
+
seg_tags TEXT NOT NULL DEFAULT '',
|
|
57
|
+
created_at INTEGER NOT NULL,
|
|
58
|
+
updated_at INTEGER NOT NULL
|
|
59
|
+
)
|
|
60
|
+
`)
|
|
61
|
+
|
|
62
|
+
migrate(db)
|
|
63
|
+
return { db }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 按 user_version 逐步迁移。整体单事务——任何一步失败都回滚,不留半成品 schema */
|
|
67
|
+
export function migrate(db) {
|
|
68
|
+
const { user_version: version } = db.prepare(`PRAGMA user_version`).get()
|
|
69
|
+
if (version >= SCHEMA_VERSION) return
|
|
70
|
+
|
|
71
|
+
db.exec("BEGIN IMMEDIATE")
|
|
72
|
+
try {
|
|
73
|
+
if (version < 2) {
|
|
74
|
+
// v1(trigram) 或空库 → v2(unicode61 + CJK 逐字):重建 FTS 和触发器
|
|
75
|
+
db.exec(`
|
|
76
|
+
DROP TRIGGER IF EXISTS entries_ai;
|
|
77
|
+
DROP TRIGGER IF EXISTS entries_ad;
|
|
78
|
+
DROP TRIGGER IF EXISTS entries_au;
|
|
79
|
+
DROP TABLE IF EXISTS entries_fts;
|
|
80
|
+
`)
|
|
81
|
+
// 老库(v1)没有 seg 列,补上
|
|
82
|
+
const columns = db.prepare(`PRAGMA table_info(entries)`).all().map((c) => c.name)
|
|
83
|
+
for (const col of ["seg_title", "seg_content", "seg_tags"]) {
|
|
84
|
+
if (!columns.includes(col)) {
|
|
85
|
+
db.exec(`ALTER TABLE entries ADD COLUMN ${col} TEXT NOT NULL DEFAULT ''`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 回填 seg 列(JS 侧分字,SQL 做不了)
|
|
89
|
+
const rows = db.prepare(`SELECT id, title, content, tags FROM entries`).all()
|
|
90
|
+
const update = db.prepare(`UPDATE entries SET seg_title = ?, seg_content = ?, seg_tags = ? WHERE id = ?`)
|
|
91
|
+
for (const r of rows) {
|
|
92
|
+
update.run(segmentCJK(r.title), segmentCJK(r.content), segmentCJK(r.tags), r.id)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
db.exec(`
|
|
96
|
+
CREATE VIRTUAL TABLE entries_fts USING fts5(
|
|
97
|
+
seg_title, seg_content, seg_tags,
|
|
98
|
+
content='entries', content_rowid='id',
|
|
99
|
+
tokenize='unicode61'
|
|
100
|
+
)
|
|
101
|
+
`)
|
|
102
|
+
db.exec(`
|
|
103
|
+
CREATE TRIGGER entries_ai AFTER INSERT ON entries BEGIN
|
|
104
|
+
INSERT INTO entries_fts(rowid, seg_title, seg_content, seg_tags)
|
|
105
|
+
VALUES (new.id, new.seg_title, new.seg_content, new.seg_tags);
|
|
106
|
+
END;
|
|
107
|
+
CREATE TRIGGER entries_ad AFTER DELETE ON entries BEGIN
|
|
108
|
+
INSERT INTO entries_fts(entries_fts, rowid, seg_title, seg_content, seg_tags)
|
|
109
|
+
VALUES ('delete', old.id, old.seg_title, old.seg_content, old.seg_tags);
|
|
110
|
+
END;
|
|
111
|
+
CREATE TRIGGER entries_au AFTER UPDATE ON entries BEGIN
|
|
112
|
+
INSERT INTO entries_fts(entries_fts, rowid, seg_title, seg_content, seg_tags)
|
|
113
|
+
VALUES ('delete', old.id, old.seg_title, old.seg_content, old.seg_tags);
|
|
114
|
+
INSERT INTO entries_fts(rowid, seg_title, seg_content, seg_tags)
|
|
115
|
+
VALUES (new.id, new.seg_title, new.seg_content, new.seg_tags);
|
|
116
|
+
END;
|
|
117
|
+
`)
|
|
118
|
+
db.exec(`INSERT INTO entries_fts(entries_fts) VALUES('rebuild')`)
|
|
119
|
+
db.exec(`PRAGMA user_version = 2`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (version < 3) {
|
|
123
|
+
// v3:markdown 层(project/team)的 files 表 + FTS + 触发器
|
|
124
|
+
db.exec(`
|
|
125
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
126
|
+
layer TEXT NOT NULL CHECK(layer IN ('project','team')),
|
|
127
|
+
path TEXT NOT NULL,
|
|
128
|
+
type TEXT NOT NULL CHECK(type IN ('rule','knowledge','decision','pattern')),
|
|
129
|
+
title TEXT NOT NULL,
|
|
130
|
+
content TEXT NOT NULL,
|
|
131
|
+
tags TEXT NOT NULL DEFAULT '',
|
|
132
|
+
author TEXT NOT NULL DEFAULT '',
|
|
133
|
+
embedding BLOB,
|
|
134
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
135
|
+
seg_title TEXT NOT NULL DEFAULT '',
|
|
136
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
137
|
+
seg_tags TEXT NOT NULL DEFAULT '',
|
|
138
|
+
updated_at INTEGER NOT NULL,
|
|
139
|
+
PRIMARY KEY (layer, path)
|
|
140
|
+
)
|
|
141
|
+
`)
|
|
142
|
+
db.exec(`
|
|
143
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5(
|
|
144
|
+
seg_title, seg_content, seg_tags,
|
|
145
|
+
content='files', content_rowid='rowid',
|
|
146
|
+
tokenize='unicode61'
|
|
147
|
+
)
|
|
148
|
+
`)
|
|
149
|
+
db.exec(`
|
|
150
|
+
CREATE TRIGGER files_ai AFTER INSERT ON files BEGIN
|
|
151
|
+
INSERT INTO files_fts(rowid, seg_title, seg_content, seg_tags)
|
|
152
|
+
VALUES (new.rowid, new.seg_title, new.seg_content, new.seg_tags);
|
|
153
|
+
END;
|
|
154
|
+
CREATE TRIGGER files_ad AFTER DELETE ON files BEGIN
|
|
155
|
+
INSERT INTO files_fts(files_fts, rowid, seg_title, seg_content, seg_tags)
|
|
156
|
+
VALUES ('delete', old.rowid, old.seg_title, old.seg_content, old.seg_tags);
|
|
157
|
+
END;
|
|
158
|
+
CREATE TRIGGER files_au AFTER UPDATE ON files BEGIN
|
|
159
|
+
INSERT INTO files_fts(files_fts, rowid, seg_title, seg_content, seg_tags)
|
|
160
|
+
VALUES ('delete', old.rowid, old.seg_title, old.seg_content, old.seg_tags);
|
|
161
|
+
INSERT INTO files_fts(rowid, seg_title, seg_content, seg_tags)
|
|
162
|
+
VALUES (new.rowid, new.seg_title, new.seg_content, new.seg_tags);
|
|
163
|
+
END;
|
|
164
|
+
`)
|
|
165
|
+
db.exec(`PRAGMA user_version = 3`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (version < 4) {
|
|
169
|
+
// v4:personal 的 entries 表加向量列(files 表在 v3 已带);meta 表存 embedding 模型名
|
|
170
|
+
const columns = db.prepare(`PRAGMA table_info(entries)`).all().map((c) => c.name)
|
|
171
|
+
if (!columns.includes("embedding")) {
|
|
172
|
+
db.exec(`ALTER TABLE entries ADD COLUMN embedding BLOB`)
|
|
173
|
+
}
|
|
174
|
+
db.exec(`CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)`)
|
|
175
|
+
db.exec(`PRAGMA user_version = 4`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (version < 5) {
|
|
179
|
+
// v5:files 表加 origin 列(项目绝对路径),防止跨项目记忆串台
|
|
180
|
+
const columns = db.prepare(`PRAGMA table_info(files)`).all().map((c) => c.name)
|
|
181
|
+
if (!columns.includes("origin")) {
|
|
182
|
+
db.exec(`ALTER TABLE files ADD COLUMN origin TEXT NOT NULL DEFAULT ''`)
|
|
183
|
+
}
|
|
184
|
+
db.exec(`PRAGMA user_version = 5`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (version < 6) {
|
|
188
|
+
// v6:代码索引——code_chunks 表 + FTS5(与 files 表同样模式)
|
|
189
|
+
db.exec(`
|
|
190
|
+
CREATE TABLE IF NOT EXISTS code_chunks (
|
|
191
|
+
path TEXT NOT NULL,
|
|
192
|
+
language TEXT NOT NULL,
|
|
193
|
+
chunk_type TEXT NOT NULL CHECK(chunk_type IN ('file','symbol')),
|
|
194
|
+
symbol_name TEXT NOT NULL DEFAULT '',
|
|
195
|
+
content TEXT NOT NULL,
|
|
196
|
+
line_start INTEGER NOT NULL DEFAULT 0,
|
|
197
|
+
line_end INTEGER NOT NULL DEFAULT 0,
|
|
198
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
199
|
+
embedding BLOB,
|
|
200
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
201
|
+
PRIMARY KEY (path, line_start)
|
|
202
|
+
)
|
|
203
|
+
`)
|
|
204
|
+
db.exec(`
|
|
205
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS code_chunks_fts USING fts5(
|
|
206
|
+
path, symbol_name, seg_content,
|
|
207
|
+
content='code_chunks', content_rowid='rowid',
|
|
208
|
+
tokenize='unicode61'
|
|
209
|
+
)
|
|
210
|
+
`)
|
|
211
|
+
db.exec(`
|
|
212
|
+
CREATE TRIGGER code_chunks_ai AFTER INSERT ON code_chunks BEGIN
|
|
213
|
+
INSERT INTO code_chunks_fts(rowid, path, symbol_name, seg_content)
|
|
214
|
+
VALUES (new.rowid, new.path, new.symbol_name, new.seg_content);
|
|
215
|
+
END;
|
|
216
|
+
CREATE TRIGGER code_chunks_ad AFTER DELETE ON code_chunks BEGIN
|
|
217
|
+
INSERT INTO code_chunks_fts(code_chunks_fts, rowid, path, symbol_name, seg_content)
|
|
218
|
+
VALUES ('delete', old.rowid, old.path, old.symbol_name, old.seg_content);
|
|
219
|
+
END;
|
|
220
|
+
CREATE TRIGGER code_chunks_au AFTER UPDATE ON code_chunks BEGIN
|
|
221
|
+
INSERT INTO code_chunks_fts(code_chunks_fts, rowid, path, symbol_name, seg_content)
|
|
222
|
+
VALUES ('delete', old.rowid, old.path, old.symbol_name, old.seg_content);
|
|
223
|
+
INSERT INTO code_chunks_fts(rowid, path, symbol_name, seg_content)
|
|
224
|
+
VALUES (new.rowid, new.path, new.symbol_name, new.seg_content);
|
|
225
|
+
END;
|
|
226
|
+
`)
|
|
227
|
+
db.exec(`PRAGMA user_version = 6`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (version < 7) {
|
|
231
|
+
// v7:文档索引——doc_chunks 表 + FTS5(与 code_chunks 同模式),markdown 按 ## 标题分块
|
|
232
|
+
db.exec(`
|
|
233
|
+
CREATE TABLE IF NOT EXISTS doc_chunks (
|
|
234
|
+
path TEXT NOT NULL,
|
|
235
|
+
language TEXT NOT NULL DEFAULT 'markdown',
|
|
236
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
237
|
+
content TEXT NOT NULL,
|
|
238
|
+
line_start INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
line_end INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
241
|
+
embedding BLOB,
|
|
242
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
243
|
+
PRIMARY KEY (path, line_start)
|
|
244
|
+
)
|
|
245
|
+
`)
|
|
246
|
+
db.exec(`
|
|
247
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS doc_chunks_fts USING fts5(
|
|
248
|
+
path, heading, seg_content,
|
|
249
|
+
content='doc_chunks', content_rowid='rowid',
|
|
250
|
+
tokenize='unicode61'
|
|
251
|
+
)
|
|
252
|
+
`)
|
|
253
|
+
db.exec(`
|
|
254
|
+
CREATE TRIGGER doc_chunks_ai AFTER INSERT ON doc_chunks BEGIN
|
|
255
|
+
INSERT INTO doc_chunks_fts(rowid, path, heading, seg_content)
|
|
256
|
+
VALUES (new.rowid, new.path, new.heading, new.seg_content);
|
|
257
|
+
END;
|
|
258
|
+
CREATE TRIGGER doc_chunks_ad AFTER DELETE ON doc_chunks BEGIN
|
|
259
|
+
INSERT INTO doc_chunks_fts(doc_chunks_fts, rowid, path, heading, seg_content)
|
|
260
|
+
VALUES ('delete', old.rowid, old.path, old.heading, old.seg_content);
|
|
261
|
+
END;
|
|
262
|
+
CREATE TRIGGER doc_chunks_au AFTER UPDATE ON doc_chunks BEGIN
|
|
263
|
+
INSERT INTO doc_chunks_fts(doc_chunks_fts, rowid, path, heading, seg_content)
|
|
264
|
+
VALUES ('delete', old.rowid, old.path, old.heading, old.seg_content);
|
|
265
|
+
INSERT INTO doc_chunks_fts(rowid, path, heading, seg_content)
|
|
266
|
+
VALUES (new.rowid, new.path, new.heading, new.seg_content);
|
|
267
|
+
END;
|
|
268
|
+
`)
|
|
269
|
+
db.exec(`PRAGMA user_version = 7`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (version < 8) {
|
|
273
|
+
// v8:code_chunks/doc_chunks 加 origin 列(项目根目录绝对路径),主键改为 (origin, path, line_start)。
|
|
274
|
+
// SQLite 不能 ALTER 主键 → 直接删表重建(下次 codeSync/docSync 自动重索引)。
|
|
275
|
+
db.exec(`
|
|
276
|
+
DROP TRIGGER IF EXISTS code_chunks_ai;
|
|
277
|
+
DROP TRIGGER IF EXISTS code_chunks_ad;
|
|
278
|
+
DROP TRIGGER IF EXISTS code_chunks_au;
|
|
279
|
+
DROP TABLE IF EXISTS code_chunks_fts;
|
|
280
|
+
DROP TABLE IF EXISTS code_chunks;
|
|
281
|
+
DROP TRIGGER IF EXISTS doc_chunks_ai;
|
|
282
|
+
DROP TRIGGER IF EXISTS doc_chunks_ad;
|
|
283
|
+
DROP TRIGGER IF EXISTS doc_chunks_au;
|
|
284
|
+
DROP TABLE IF EXISTS doc_chunks_fts;
|
|
285
|
+
DROP TABLE IF EXISTS doc_chunks;
|
|
286
|
+
`)
|
|
287
|
+
db.exec(`
|
|
288
|
+
CREATE TABLE code_chunks (
|
|
289
|
+
origin TEXT NOT NULL DEFAULT '',
|
|
290
|
+
path TEXT NOT NULL,
|
|
291
|
+
language TEXT NOT NULL,
|
|
292
|
+
chunk_type TEXT NOT NULL CHECK(chunk_type IN ('file','symbol')),
|
|
293
|
+
symbol_name TEXT NOT NULL DEFAULT '',
|
|
294
|
+
content TEXT NOT NULL,
|
|
295
|
+
line_start INTEGER NOT NULL DEFAULT 0,
|
|
296
|
+
line_end INTEGER NOT NULL DEFAULT 0,
|
|
297
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
298
|
+
embedding BLOB,
|
|
299
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
300
|
+
PRIMARY KEY (origin, path, line_start)
|
|
301
|
+
)
|
|
302
|
+
`)
|
|
303
|
+
db.exec(`
|
|
304
|
+
CREATE VIRTUAL TABLE code_chunks_fts USING fts5(
|
|
305
|
+
path, symbol_name, seg_content,
|
|
306
|
+
content='code_chunks', content_rowid='rowid',
|
|
307
|
+
tokenize='unicode61'
|
|
308
|
+
)
|
|
309
|
+
`)
|
|
310
|
+
db.exec(`
|
|
311
|
+
CREATE TRIGGER code_chunks_ai AFTER INSERT ON code_chunks BEGIN
|
|
312
|
+
INSERT INTO code_chunks_fts(rowid, path, symbol_name, seg_content)
|
|
313
|
+
VALUES (new.rowid, new.path, new.symbol_name, new.seg_content);
|
|
314
|
+
END;
|
|
315
|
+
CREATE TRIGGER code_chunks_ad AFTER DELETE ON code_chunks BEGIN
|
|
316
|
+
INSERT INTO code_chunks_fts(code_chunks_fts, rowid, path, symbol_name, seg_content)
|
|
317
|
+
VALUES ('delete', old.rowid, old.path, old.symbol_name, old.seg_content);
|
|
318
|
+
END;
|
|
319
|
+
CREATE TRIGGER code_chunks_au AFTER UPDATE ON code_chunks BEGIN
|
|
320
|
+
INSERT INTO code_chunks_fts(code_chunks_fts, rowid, path, symbol_name, seg_content)
|
|
321
|
+
VALUES ('delete', old.rowid, old.path, old.symbol_name, old.seg_content);
|
|
322
|
+
INSERT INTO code_chunks_fts(rowid, path, symbol_name, seg_content)
|
|
323
|
+
VALUES (new.rowid, new.path, new.symbol_name, new.seg_content);
|
|
324
|
+
END;
|
|
325
|
+
`)
|
|
326
|
+
db.exec(`
|
|
327
|
+
CREATE TABLE doc_chunks (
|
|
328
|
+
origin TEXT NOT NULL DEFAULT '',
|
|
329
|
+
path TEXT NOT NULL,
|
|
330
|
+
language TEXT NOT NULL DEFAULT 'markdown',
|
|
331
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
332
|
+
content TEXT NOT NULL,
|
|
333
|
+
line_start INTEGER NOT NULL DEFAULT 0,
|
|
334
|
+
line_end INTEGER NOT NULL DEFAULT 0,
|
|
335
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
336
|
+
embedding BLOB,
|
|
337
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
338
|
+
PRIMARY KEY (origin, path, line_start)
|
|
339
|
+
)
|
|
340
|
+
`)
|
|
341
|
+
db.exec(`
|
|
342
|
+
CREATE VIRTUAL TABLE doc_chunks_fts USING fts5(
|
|
343
|
+
path, heading, seg_content,
|
|
344
|
+
content='doc_chunks', content_rowid='rowid',
|
|
345
|
+
tokenize='unicode61'
|
|
346
|
+
)
|
|
347
|
+
`)
|
|
348
|
+
db.exec(`
|
|
349
|
+
CREATE TRIGGER doc_chunks_ai AFTER INSERT ON doc_chunks BEGIN
|
|
350
|
+
INSERT INTO doc_chunks_fts(rowid, path, heading, seg_content)
|
|
351
|
+
VALUES (new.rowid, new.path, new.heading, new.seg_content);
|
|
352
|
+
END;
|
|
353
|
+
CREATE TRIGGER doc_chunks_ad AFTER DELETE ON doc_chunks BEGIN
|
|
354
|
+
INSERT INTO doc_chunks_fts(doc_chunks_fts, rowid, path, heading, seg_content)
|
|
355
|
+
VALUES ('delete', old.rowid, old.path, old.heading, old.seg_content);
|
|
356
|
+
END;
|
|
357
|
+
CREATE TRIGGER doc_chunks_au AFTER UPDATE ON doc_chunks BEGIN
|
|
358
|
+
INSERT INTO doc_chunks_fts(doc_chunks_fts, rowid, path, heading, seg_content)
|
|
359
|
+
VALUES ('delete', old.rowid, old.path, old.heading, old.seg_content);
|
|
360
|
+
INSERT INTO doc_chunks_fts(rowid, path, heading, seg_content)
|
|
361
|
+
VALUES (new.rowid, new.path, new.heading, new.seg_content);
|
|
362
|
+
END;
|
|
363
|
+
`)
|
|
364
|
+
db.exec(`PRAGMA user_version = 8`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (version < 9) {
|
|
368
|
+
// v9: files 表 PK 加 origin,防跨项目 project 层记忆覆盖
|
|
369
|
+
// SQLite 不能 ALTER 主键 → 删表重建(下次 syncDir 自动重索引)
|
|
370
|
+
db.exec(`
|
|
371
|
+
DROP TRIGGER IF EXISTS files_ai;
|
|
372
|
+
DROP TRIGGER IF EXISTS files_ad;
|
|
373
|
+
DROP TRIGGER IF EXISTS files_au;
|
|
374
|
+
DROP TABLE IF EXISTS files_fts;
|
|
375
|
+
DROP TABLE IF EXISTS files;
|
|
376
|
+
`)
|
|
377
|
+
db.exec(`
|
|
378
|
+
CREATE TABLE files (
|
|
379
|
+
layer TEXT NOT NULL CHECK(layer IN ('project','team')),
|
|
380
|
+
origin TEXT NOT NULL DEFAULT '',
|
|
381
|
+
path TEXT NOT NULL,
|
|
382
|
+
type TEXT NOT NULL CHECK(type IN ('rule','knowledge','decision','pattern')),
|
|
383
|
+
title TEXT NOT NULL,
|
|
384
|
+
content TEXT NOT NULL,
|
|
385
|
+
tags TEXT NOT NULL DEFAULT '',
|
|
386
|
+
author TEXT NOT NULL DEFAULT '',
|
|
387
|
+
embedding BLOB,
|
|
388
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
389
|
+
seg_title TEXT NOT NULL DEFAULT '',
|
|
390
|
+
seg_content TEXT NOT NULL DEFAULT '',
|
|
391
|
+
seg_tags TEXT NOT NULL DEFAULT '',
|
|
392
|
+
updated_at INTEGER NOT NULL,
|
|
393
|
+
PRIMARY KEY (layer, origin, path)
|
|
394
|
+
)
|
|
395
|
+
`)
|
|
396
|
+
db.exec(`
|
|
397
|
+
CREATE VIRTUAL TABLE files_fts USING fts5(
|
|
398
|
+
seg_title, seg_content, seg_tags,
|
|
399
|
+
content='files', content_rowid='rowid',
|
|
400
|
+
tokenize='unicode61'
|
|
401
|
+
)
|
|
402
|
+
`)
|
|
403
|
+
db.exec(`
|
|
404
|
+
CREATE TRIGGER files_ai AFTER INSERT ON files BEGIN
|
|
405
|
+
INSERT INTO files_fts(rowid, seg_title, seg_content, seg_tags)
|
|
406
|
+
VALUES (new.rowid, new.seg_title, new.seg_content, new.seg_tags);
|
|
407
|
+
END;
|
|
408
|
+
CREATE TRIGGER files_ad AFTER DELETE ON files BEGIN
|
|
409
|
+
INSERT INTO files_fts(files_fts, rowid, seg_title, seg_content, seg_tags)
|
|
410
|
+
VALUES ('delete', old.rowid, old.seg_title, old.seg_content, old.seg_tags);
|
|
411
|
+
END;
|
|
412
|
+
CREATE TRIGGER files_au AFTER UPDATE ON files BEGIN
|
|
413
|
+
INSERT INTO files_fts(files_fts, rowid, seg_title, seg_content, seg_tags)
|
|
414
|
+
VALUES ('delete', old.rowid, old.seg_title, old.seg_content, old.seg_tags);
|
|
415
|
+
INSERT INTO files_fts(rowid, seg_title, seg_content, seg_tags)
|
|
416
|
+
VALUES (new.rowid, new.seg_title, new.seg_content, new.seg_tags);
|
|
417
|
+
END;
|
|
418
|
+
`)
|
|
419
|
+
db.exec(`PRAGMA user_version = 9`)
|
|
420
|
+
}
|
|
421
|
+
db.exec("COMMIT")
|
|
422
|
+
} catch (err) {
|
|
423
|
+
db.exec("ROLLBACK")
|
|
424
|
+
throw err
|
|
425
|
+
}
|
|
426
|
+
}
|