akm-cli 0.9.15 → 0.9.16-alpha.1
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/CHANGELOG.md +144 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/cli/retired-commands.js +2 -0
- package/dist/cli/unknown-flags.js +36 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +6 -4
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/proposal/repository.js +12 -3
- package/dist/commands/read/curate.js +34 -44
- package/dist/commands/read/search.js +50 -2
- package/dist/commands/sources/index-status.js +99 -0
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +33 -12
- package/dist/commands/sources/source-add.js +21 -6
- package/dist/commands/sources/stash-cli.js +119 -111
- package/dist/core/adapter/adapters/akm-adapter.js +35 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -1
- package/dist/core/asset/asset-placement.js +35 -0
- package/dist/core/config/schema/embedding.js +7 -30
- package/dist/core/config/schema/search.js +11 -9
- package/dist/core/errors.js +5 -2
- package/dist/core/hash.js +18 -0
- package/dist/core/maintenance-barrier.js +8 -6
- package/dist/core/paths.js +0 -11
- package/dist/core/run-lock.js +5 -2
- package/dist/core/state/migrations.js +26 -1
- package/dist/core/state-db.js +63 -27
- package/dist/indexer/drain.js +306 -0
- package/dist/indexer/embedding-identity.js +20 -0
- package/dist/indexer/enrich.js +260 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/index-written-assets.js +133 -171
- package/dist/indexer/indexer.js +458 -1621
- package/dist/indexer/lookup/adapter-concept-owner.js +19 -5
- package/dist/indexer/passes/metadata.js +18 -1
- package/dist/indexer/reconcile.js +890 -0
- package/dist/indexer/scan/drain-dir.js +27 -70
- package/dist/indexer/scan/parse-file.js +66 -0
- package/dist/indexer/search/db-search.js +373 -89
- package/dist/indexer/search/ranking-contributors.js +21 -16
- package/dist/indexer/search/ranking.js +135 -57
- package/dist/indexer/units/unit.js +159 -0
- package/dist/llm/client.js +10 -1
- package/dist/llm/embedder.js +10 -3
- package/dist/llm/embedders/provider-limits.js +288 -0
- package/dist/llm/embedders/remote.js +133 -104
- package/dist/llm/feature-gate.js +4 -2
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +19 -13
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +5 -2
- package/dist/scripts/akm-migrate-node.js +1141 -1237
- package/dist/scripts/akm-migrate.js +1141 -1237
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/steps/connection.js +3 -2
- package/dist/storage/repositories/files-repository.js +181 -0
- package/dist/storage/repositories/index-connection.js +1 -3
- package/dist/storage/repositories/index-entries-repository.js +77 -68
- package/dist/storage/repositories/index-entry-schema.js +16 -25
- package/dist/storage/repositories/index-fts-repository.js +29 -263
- package/dist/storage/repositories/index-meta-repository.js +0 -29
- package/dist/storage/repositories/index-schema.js +115 -122
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +21 -334
- package/dist/storage/repositories/units-repository.js +510 -0
- package/docs/migration/release-notes/0.9.15.md +34 -36
- package/docs/migration/release-notes/0.9.16.md +110 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +93 -87
- package/docs/reference/configuration.md +128 -89
- package/docs/reference/data-and-telemetry.md +2 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +2 -58
- package/dist/indexer/index-db-contention.js +0 -56
- package/dist/indexer/index-rebuild-lock.js +0 -73
- package/dist/indexer/materialize-embeddings.js +0 -771
- package/dist/indexer/passes/dir-staleness.js +0 -161
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
|
@@ -2,85 +2,56 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* `index.db`
|
|
5
|
+
* `index.db` safe-Markdown-fragment repository.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* index-redesign B5c: `entries_fts`, `entry_fragments_fts` and the lexical
|
|
8
|
+
* query paths that fed them (`searchFts`, `rebuildFts`) are deleted — lexical
|
|
9
|
+
* search runs entirely over `units_fts` now (`db-search.ts`'s
|
|
10
|
+
* `searchUnitsLexical`, seeded by `deriveUnits`/`reconcile.ts` from the same
|
|
11
|
+
* entry). What remains here is `entry_fragments`: the safe-rendered Markdown
|
|
12
|
+
* source a matched fragment hit's display metadata (content, ordinal, count,
|
|
13
|
+
* line range, prev/next) is projected from, and that `akm show <ref>#<id>`
|
|
14
|
+
* resolves an opaque fragment selector through. Both search paths (today,
|
|
15
|
+
* only the units path) and `show` (`src/commands/read/show.ts`) read it via
|
|
16
|
+
* `getIndexedMarkdownFragment(s)`.
|
|
9
17
|
*/
|
|
10
18
|
import { splitMarkdownFragments } from "../../core/asset/markdown-fragments.js";
|
|
11
|
-
import { stableFtsScore } from "../../core/lexical-score.js";
|
|
12
|
-
import { warn } from "../../core/warn.js";
|
|
13
|
-
import { buildLexicalQueryPlan } from "../../indexer/search/fts-query.js";
|
|
14
|
-
import { buildSearchFields } from "../../indexer/search/search-fields.js";
|
|
15
19
|
import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
function getFtsMutationStatements(db) {
|
|
20
|
-
const existing = ftsMutationStatementsByDb.get(db);
|
|
20
|
+
const fragmentSourceStatementsByDb = new WeakMap();
|
|
21
|
+
function getFragmentSourceStatements(db) {
|
|
22
|
+
const existing = fragmentSourceStatementsByDb.get(db);
|
|
21
23
|
if (existing)
|
|
22
24
|
return existing;
|
|
23
25
|
const statements = {
|
|
24
|
-
deleteOne: db.prepare("DELETE FROM entries_fts WHERE entry_id = ?"),
|
|
25
|
-
insert: db.prepare(INSERT_FTS_SQL),
|
|
26
|
-
deleteFragments: db.prepare("DELETE FROM entry_fragments_fts WHERE entry_id = ?"),
|
|
27
26
|
upsertFragmentSource: db.prepare("INSERT INTO entry_fragments (entry_id, safe_markdown) VALUES (?, ?) ON CONFLICT(entry_id) DO UPDATE SET safe_markdown = excluded.safe_markdown"),
|
|
28
27
|
deleteFragmentSource: db.prepare("DELETE FROM entry_fragments WHERE entry_id = ?"),
|
|
29
|
-
insertFragment: db.prepare(INSERT_FRAGMENT_SQL),
|
|
30
28
|
};
|
|
31
|
-
|
|
29
|
+
fragmentSourceStatementsByDb.set(db, statements);
|
|
32
30
|
return statements;
|
|
33
31
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// A scan that did read Markdown always supplies a value below.
|
|
32
|
+
/**
|
|
33
|
+
* Replace one entry's safe-Markdown fragment source inside the caller's
|
|
34
|
+
* transaction. `fragmentContent === undefined` means a metadata-only
|
|
35
|
+
* re-upsert or re-key that did not re-scan Markdown — the persisted source
|
|
36
|
+
* is left untouched. An empty/null value is an explicit clear (the entry no
|
|
37
|
+
* longer has Markdown content to project fragments from).
|
|
38
|
+
*/
|
|
39
|
+
export function replaceFragmentSource(db, entryId, fragmentContent) {
|
|
40
|
+
if (fragmentContent === undefined)
|
|
44
41
|
return;
|
|
45
|
-
|
|
46
|
-
statements.deleteFragments.run(entryId);
|
|
42
|
+
const statements = getFragmentSourceStatements(db);
|
|
47
43
|
statements.deleteFragmentSource.run(entryId);
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
statements.upsertFragmentSource.run(entryId, fragmentContent);
|
|
51
|
-
for (const fragment of splitMarkdownFragments(fragmentContent)) {
|
|
52
|
-
statements.insertFragment.run(entryId, fragment.fragmentId, fragment.ordinal, fragment.text.toLowerCase());
|
|
53
|
-
}
|
|
44
|
+
if (fragmentContent)
|
|
45
|
+
statements.upsertFragmentSource.run(entryId, fragmentContent);
|
|
54
46
|
}
|
|
55
|
-
/** Delete
|
|
56
|
-
export function
|
|
47
|
+
/** Delete the safe-Markdown fragment source for entries being removed. */
|
|
48
|
+
export function deleteFragmentSource(db, entryIds) {
|
|
57
49
|
for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
58
50
|
const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
59
51
|
const placeholders = chunk.map(() => "?").join(",");
|
|
60
|
-
db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk);
|
|
61
|
-
db.prepare(`DELETE FROM entry_fragments_fts WHERE entry_id IN (${placeholders})`).run(...chunk);
|
|
62
52
|
db.prepare(`DELETE FROM entry_fragments WHERE entry_id IN (${placeholders})`).run(...chunk);
|
|
63
53
|
}
|
|
64
54
|
}
|
|
65
|
-
export function searchFts(db, query, limit, entryType, excludeTypes) {
|
|
66
|
-
const plan = buildLexicalQueryPlan(query);
|
|
67
|
-
if (!plan.exact)
|
|
68
|
-
return [];
|
|
69
|
-
// Try the exact AND query first
|
|
70
|
-
const exactResults = runFtsQuery(db, plan.exact, "exact", limit, entryType, excludeTypes);
|
|
71
|
-
if (exactResults.length > 0)
|
|
72
|
-
return exactResults;
|
|
73
|
-
if (plan.exactPrefix) {
|
|
74
|
-
const prefixResults = runFtsQuery(db, plan.exactPrefix, "prefix", limit, entryType, excludeTypes);
|
|
75
|
-
if (prefixResults.length > 0)
|
|
76
|
-
return prefixResults;
|
|
77
|
-
}
|
|
78
|
-
// One measured relaxation only after both conjunctive forms miss. This is
|
|
79
|
-
// still the same FTS table, BM25 weights, candidate collection, and
|
|
80
|
-
// downstream ranker — merely an OR candidate query for sentence-shaped
|
|
81
|
-
// input whose filler terms prevented a strict hit.
|
|
82
|
-
return plan.relaxed ? runFtsQuery(db, plan.relaxed, "relaxed", limit, entryType, excludeTypes) : [];
|
|
83
|
-
}
|
|
84
55
|
/**
|
|
85
56
|
* Resolve an opaque fragment selector from the indexed safe projection, not
|
|
86
57
|
* from current disk. A search result remains self-consistent across a later
|
|
@@ -136,208 +107,3 @@ function materializeIndexedMarkdownFragment(fragment, fragments, parentChars) {
|
|
|
136
107
|
fragments,
|
|
137
108
|
};
|
|
138
109
|
}
|
|
139
|
-
function runFtsQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludeTypes) {
|
|
140
|
-
// Preserve the repository's ordinary limit contract for direct callers.
|
|
141
|
-
// The boundary-expansion rule applies only to a positive candidate pool.
|
|
142
|
-
if (limit <= 0)
|
|
143
|
-
return [];
|
|
144
|
-
// #627 — exclude-type clause. Only applies on the untyped ('any') path; an
|
|
145
|
-
// explicit include filter (entryType) already narrows to a single type, so
|
|
146
|
-
// exclusion is redundant there. An empty list skips the clause entirely
|
|
147
|
-
// (never emit `NOT IN ()`, which is a SQL error / always-false).
|
|
148
|
-
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
149
|
-
const candidateBoundaryOffset = Math.max(0, limit - 1);
|
|
150
|
-
// The typed and untyped paths differ only by one `type` WHERE clause
|
|
151
|
-
// equality vs. an optional NOT IN exclusion) and their parameter order.
|
|
152
|
-
// Join on integer entry_id directly (no CAST; we store integer). bm25()
|
|
153
|
-
// per-column weights:
|
|
154
|
-
// entry_id(0), name(10), description(5), tags(3), hints(2), content(1).
|
|
155
|
-
let filterClause;
|
|
156
|
-
let params;
|
|
157
|
-
if (entryType && entryType !== "any") {
|
|
158
|
-
filterClause = "AND e.type = ?";
|
|
159
|
-
params = [ftsQuery, entryType, candidateBoundaryOffset];
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
filterClause = excludes.length > 0 ? `AND e.type NOT IN (${excludes.map(() => "?").join(", ")})` : "";
|
|
163
|
-
// Param order: MATCH, then the NOT IN values, then the zero-based
|
|
164
|
-
// candidate-boundary offset.
|
|
165
|
-
params = [ftsQuery, ...excludes, candidateBoundaryOffset];
|
|
166
|
-
}
|
|
167
|
-
const sql = `
|
|
168
|
-
-- Do not make a SQL-only relevance decision inside a tied BM25 boundary:
|
|
169
|
-
-- the TypeScript ranker adds exact-name, type, and other contributors
|
|
170
|
-
-- afterwards. Materialize BM25 once, locate the Nth score, and admit
|
|
171
|
-
-- every row tied with it. This deliberately makes the result set
|
|
172
|
-
-- data-bound for a pathological all-tied query; that is the only way to
|
|
173
|
-
-- avoid silently dropping a legitimate later ranking winner.
|
|
174
|
-
WITH scored AS MATERIALIZED (
|
|
175
|
-
-- Keep this materialized set deliberately narrow. document_json can be
|
|
176
|
-
-- large, and only rows admitted through the BM25 boundary need it.
|
|
177
|
-
SELECT e.id, bm25(entries_fts, 0, 10.0, 5.0, 3.0, 2.0, 1.0) AS bm25Score
|
|
178
|
-
FROM entries_fts f
|
|
179
|
-
JOIN entries e ON e.id = f.entry_id
|
|
180
|
-
WHERE entries_fts MATCH ?
|
|
181
|
-
${filterClause}
|
|
182
|
-
), boundary AS (
|
|
183
|
-
SELECT bm25Score
|
|
184
|
-
FROM scored
|
|
185
|
-
ORDER BY bm25Score
|
|
186
|
-
LIMIT 1 OFFSET ?
|
|
187
|
-
)
|
|
188
|
-
SELECT e.id, e.file_path AS filePath, e.document_json AS documentJson, e.search_text AS searchText,
|
|
189
|
-
e.item_ref AS itemRef, e.bundle_id AS bundleId, e.concept_id AS conceptId, e.adapter_id AS adapterId,
|
|
190
|
-
scored.bm25Score
|
|
191
|
-
FROM scored
|
|
192
|
-
JOIN entries e ON e.id = scored.id
|
|
193
|
-
WHERE NOT EXISTS (SELECT 1 FROM boundary)
|
|
194
|
-
OR scored.bm25Score <= (SELECT bm25Score FROM boundary)
|
|
195
|
-
ORDER BY scored.bm25Score, e.id ASC
|
|
196
|
-
`;
|
|
197
|
-
const rows = db.prepare(sql).all(...params);
|
|
198
|
-
const results = materializeRows(rows, lexicalMatch);
|
|
199
|
-
// Fragments are a separate, intentionally calibrated evidence population:
|
|
200
|
-
// parent FTS remains the sole implementation of metadata/body conjunction.
|
|
201
|
-
// A selector is emitted only for one fragment that independently satisfies
|
|
202
|
-
// this query. Raw BM25 values are never claimed comparable across tables;
|
|
203
|
-
// each is passed through #933's stable mapping before merge.
|
|
204
|
-
const fragmentResults = hasFragmentFts(db)
|
|
205
|
-
? runFragmentQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludes)
|
|
206
|
-
: [];
|
|
207
|
-
return mergeParentAndFragmentResults(results, fragmentResults);
|
|
208
|
-
}
|
|
209
|
-
function hasFragmentFts(db) {
|
|
210
|
-
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'entry_fragments_fts'").get());
|
|
211
|
-
}
|
|
212
|
-
function materializeRows(rows, lexicalMatch) {
|
|
213
|
-
const results = [];
|
|
214
|
-
for (const row of rows) {
|
|
215
|
-
let entry;
|
|
216
|
-
try {
|
|
217
|
-
entry = JSON.parse(row.documentJson);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
warn(`[db] searchFts: skipping entry id=${row.id} — corrupt document_json`);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
results.push({
|
|
224
|
-
id: row.id,
|
|
225
|
-
filePath: row.filePath,
|
|
226
|
-
entry,
|
|
227
|
-
searchText: row.searchText,
|
|
228
|
-
bm25Score: row.bm25Score,
|
|
229
|
-
itemRef: row.itemRef,
|
|
230
|
-
bundleId: row.bundleId,
|
|
231
|
-
conceptId: row.conceptId,
|
|
232
|
-
adapterId: row.adapterId,
|
|
233
|
-
lexicalMatch,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return results;
|
|
237
|
-
}
|
|
238
|
-
function runFragmentQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludes) {
|
|
239
|
-
const filter = entryType && entryType !== "any"
|
|
240
|
-
? "AND e.type = ?"
|
|
241
|
-
: excludes.length
|
|
242
|
-
? `AND e.type NOT IN (${excludes.map(() => "?").join(",")})`
|
|
243
|
-
: "";
|
|
244
|
-
const filterParams = entryType && entryType !== "any" ? [entryType] : excludes;
|
|
245
|
-
const candidateBoundaryOffset = Math.max(0, limit - 1);
|
|
246
|
-
// Select the winning child per parent inside SQLite before finding the
|
|
247
|
-
// candidate boundary. A document with many matching fragments therefore
|
|
248
|
-
// occupies one parent slot, while a boundary tie retains every parent for
|
|
249
|
-
// the TypeScript ranker to decide with its non-BM25 contributors. This has
|
|
250
|
-
// one FTS query and no OFFSET walk; the returned boundary is intentionally
|
|
251
|
-
// data-bound for a pathological all-tied query, just like parent FTS.
|
|
252
|
-
const sql = `
|
|
253
|
-
WITH matches AS MATERIALIZED (
|
|
254
|
-
-- Keep repeated child rows as narrow as parent FTS's scored CTE. The
|
|
255
|
-
-- document projection can be large; hydrate it only after the one-child
|
|
256
|
-
-- per-parent collapse and BM25 boundary filtering below.
|
|
257
|
-
SELECT e.id, f.fragment_id AS fragmentId, f.fragment_ordinal AS fragmentOrdinal,
|
|
258
|
-
bm25(entry_fragments_fts) AS bm25Score
|
|
259
|
-
FROM entry_fragments_fts f JOIN entries e ON e.id = f.entry_id
|
|
260
|
-
WHERE entry_fragments_fts MATCH ? ${filter}
|
|
261
|
-
), ranked AS MATERIALIZED (
|
|
262
|
-
SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY bm25Score ASC, fragmentOrdinal ASC, fragmentId ASC) AS parentRank
|
|
263
|
-
FROM matches
|
|
264
|
-
), parents AS MATERIALIZED (
|
|
265
|
-
SELECT * FROM ranked WHERE parentRank = 1
|
|
266
|
-
), boundary AS (
|
|
267
|
-
SELECT bm25Score FROM parents ORDER BY bm25Score ASC LIMIT 1 OFFSET ?
|
|
268
|
-
)
|
|
269
|
-
SELECT e.id, e.file_path AS filePath, e.document_json AS documentJson, e.search_text AS searchText,
|
|
270
|
-
e.item_ref AS itemRef, e.bundle_id AS bundleId, e.concept_id AS conceptId, e.adapter_id AS adapterId,
|
|
271
|
-
parents.fragmentId, parents.bm25Score
|
|
272
|
-
FROM parents JOIN entries e ON e.id = parents.id
|
|
273
|
-
WHERE NOT EXISTS (SELECT 1 FROM boundary)
|
|
274
|
-
OR parents.bm25Score <= (SELECT bm25Score FROM boundary)
|
|
275
|
-
ORDER BY parents.bm25Score ASC, parents.id ASC`;
|
|
276
|
-
const rows = db.prepare(sql).all(ftsQuery, ...filterParams, candidateBoundaryOffset);
|
|
277
|
-
const results = [];
|
|
278
|
-
for (const row of rows) {
|
|
279
|
-
const [result] = materializeRows([row], lexicalMatch);
|
|
280
|
-
if (result) {
|
|
281
|
-
results.push({
|
|
282
|
-
...result,
|
|
283
|
-
fragmentId: row.fragmentId,
|
|
284
|
-
lexicalScore: stableFtsScore(result.bm25Score, "fragment"),
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return results;
|
|
289
|
-
}
|
|
290
|
-
function mergeParentAndFragmentResults(parents, fragments) {
|
|
291
|
-
const winners = new Map();
|
|
292
|
-
for (const parent of parents)
|
|
293
|
-
winners.set(parent.id, { ...parent, lexicalScore: stableFtsScore(parent.bm25Score) });
|
|
294
|
-
for (const fragment of fragments) {
|
|
295
|
-
const existing = winners.get(fragment.id);
|
|
296
|
-
if (!existing || (fragment.lexicalScore ?? 0) > (existing.lexicalScore ?? 0))
|
|
297
|
-
winners.set(fragment.id, fragment);
|
|
298
|
-
}
|
|
299
|
-
return [...winners.values()].sort((left, right) => (right.lexicalScore ?? 0) - (left.lexicalScore ?? 0) || left.id - right.id);
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Explicitly rebuild the complete FTS5 projection from canonical entries.
|
|
303
|
-
* Ordinary entry mutations do not call this: `upsertEntry` and the delete
|
|
304
|
-
* operations publish their FTS state in the same transaction as `entries`.
|
|
305
|
-
* This remains a recovery/schema-verification primitive for regenerable
|
|
306
|
-
* `index.db` state.
|
|
307
|
-
*
|
|
308
|
-
* Skipped corrupt-JSON rows are aggregated into one warning instead of
|
|
309
|
-
* spamming stderr per-entry.
|
|
310
|
-
*/
|
|
311
|
-
export function rebuildFts(db) {
|
|
312
|
-
db.transaction(() => {
|
|
313
|
-
db.exec("DELETE FROM entries_fts");
|
|
314
|
-
db.exec("DELETE FROM entry_fragments_fts");
|
|
315
|
-
const rows = db
|
|
316
|
-
.prepare("SELECT e.id, e.document_json, f.safe_markdown FROM entries e LEFT JOIN entry_fragments f ON f.entry_id = e.id")
|
|
317
|
-
.all();
|
|
318
|
-
const insertStmt = db.prepare(INSERT_FTS_SQL);
|
|
319
|
-
const fragmentStmt = db.prepare(INSERT_FRAGMENT_SQL);
|
|
320
|
-
let skipped = 0;
|
|
321
|
-
for (const row of rows) {
|
|
322
|
-
let entry;
|
|
323
|
-
let fields;
|
|
324
|
-
try {
|
|
325
|
-
entry = JSON.parse(row.document_json);
|
|
326
|
-
fields = buildSearchFields(entry);
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
skipped++;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
insertStmt.run(row.id, fields.name, fields.description, fields.tags, fields.hints, fields.content);
|
|
333
|
-
if (row.safe_markdown) {
|
|
334
|
-
for (const fragment of splitMarkdownFragments(row.safe_markdown)) {
|
|
335
|
-
fragmentStmt.run(row.id, fragment.fragmentId, fragment.ordinal, fragment.text.toLowerCase());
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (skipped > 0) {
|
|
340
|
-
warn(`[db] rebuildFts: skipped ${skipped} entr${skipped === 1 ? "y" : "ies"} with invalid document_json`);
|
|
341
|
-
}
|
|
342
|
-
})();
|
|
343
|
-
}
|
|
@@ -20,32 +20,3 @@ export function setMeta(db, key, value) {
|
|
|
20
20
|
export function deleteMeta(db, key) {
|
|
21
21
|
db.prepare("DELETE FROM index_meta WHERE key = ?").run(key);
|
|
22
22
|
}
|
|
23
|
-
// ── Per-directory index state ───────────────────────────────────────────────
|
|
24
|
-
export function getIndexDirState(db, dirPath) {
|
|
25
|
-
const row = db
|
|
26
|
-
.prepare("SELECT dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at, row_count FROM index_dir_state WHERE dir_path = ?")
|
|
27
|
-
.get(dirPath);
|
|
28
|
-
if (!row)
|
|
29
|
-
return undefined;
|
|
30
|
-
return {
|
|
31
|
-
dirPath: row.dir_path,
|
|
32
|
-
fileSetHash: row.file_set_hash,
|
|
33
|
-
fileMtimeMaxMs: row.file_mtime_max_ms,
|
|
34
|
-
reason: row.reason,
|
|
35
|
-
updatedAt: row.updated_at,
|
|
36
|
-
rowCount: row.row_count ?? undefined,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export function upsertIndexDirState(db, state) {
|
|
40
|
-
db.prepare(`INSERT INTO index_dir_state (dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at, row_count)
|
|
41
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
42
|
-
ON CONFLICT(dir_path) DO UPDATE SET
|
|
43
|
-
file_set_hash = excluded.file_set_hash,
|
|
44
|
-
file_mtime_max_ms = excluded.file_mtime_max_ms,
|
|
45
|
-
reason = excluded.reason,
|
|
46
|
-
updated_at = excluded.updated_at,
|
|
47
|
-
row_count = excluded.row_count`).run(state.dirPath, state.fileSetHash, state.fileMtimeMaxMs, state.reason, new Date().toISOString(), state.rowCount ?? null);
|
|
48
|
-
}
|
|
49
|
-
export function deleteIndexDirState(db, dirPath) {
|
|
50
|
-
db.prepare("DELETE FROM index_dir_state WHERE dir_path = ?").run(dirPath);
|
|
51
|
-
}
|