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.
Files changed (79) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  3. package/dist/cli/retired-commands.js +2 -0
  4. package/dist/cli/unknown-flags.js +36 -3
  5. package/dist/commands/improve/collapse-detector.js +2 -2
  6. package/dist/commands/improve/consolidate.js +6 -4
  7. package/dist/commands/improve/improve-cli.js +1 -1
  8. package/dist/commands/proposal/repository.js +12 -3
  9. package/dist/commands/read/curate.js +34 -44
  10. package/dist/commands/read/search.js +50 -2
  11. package/dist/commands/sources/index-status.js +99 -0
  12. package/dist/commands/sources/info.js +8 -8
  13. package/dist/commands/sources/installed-stashes.js +33 -12
  14. package/dist/commands/sources/source-add.js +21 -6
  15. package/dist/commands/sources/stash-cli.js +119 -111
  16. package/dist/core/adapter/adapters/akm-adapter.js +35 -3
  17. package/dist/core/adapter/adapters/akm-metadata.js +11 -1
  18. package/dist/core/asset/asset-placement.js +35 -0
  19. package/dist/core/config/schema/embedding.js +7 -30
  20. package/dist/core/config/schema/search.js +11 -9
  21. package/dist/core/errors.js +5 -2
  22. package/dist/core/hash.js +18 -0
  23. package/dist/core/maintenance-barrier.js +8 -6
  24. package/dist/core/paths.js +0 -11
  25. package/dist/core/run-lock.js +5 -2
  26. package/dist/core/state/migrations.js +26 -1
  27. package/dist/core/state-db.js +63 -27
  28. package/dist/indexer/drain.js +306 -0
  29. package/dist/indexer/embedding-identity.js +20 -0
  30. package/dist/indexer/enrich.js +260 -0
  31. package/dist/indexer/ensure-index.js +5 -0
  32. package/dist/indexer/index-written-assets.js +133 -171
  33. package/dist/indexer/indexer.js +458 -1621
  34. package/dist/indexer/lookup/adapter-concept-owner.js +19 -5
  35. package/dist/indexer/passes/metadata.js +18 -1
  36. package/dist/indexer/reconcile.js +890 -0
  37. package/dist/indexer/scan/drain-dir.js +27 -70
  38. package/dist/indexer/scan/parse-file.js +66 -0
  39. package/dist/indexer/search/db-search.js +373 -89
  40. package/dist/indexer/search/ranking-contributors.js +21 -16
  41. package/dist/indexer/search/ranking.js +135 -57
  42. package/dist/indexer/units/unit.js +159 -0
  43. package/dist/llm/client.js +10 -1
  44. package/dist/llm/embedder.js +10 -3
  45. package/dist/llm/embedders/provider-limits.js +288 -0
  46. package/dist/llm/embedders/remote.js +133 -104
  47. package/dist/llm/feature-gate.js +4 -2
  48. package/dist/llm/rerank-client.js +3 -3
  49. package/dist/output/shapes/passthrough.js +1 -0
  50. package/dist/output/text/command-format.js +19 -13
  51. package/dist/output/text/helpers.js +1 -1
  52. package/dist/output/text/index.js +5 -2
  53. package/dist/scripts/akm-migrate-node.js +1141 -1237
  54. package/dist/scripts/akm-migrate.js +1141 -1237
  55. package/dist/setup/semantic-assets.js +2 -2
  56. package/dist/setup/steps/connection.js +3 -2
  57. package/dist/storage/repositories/files-repository.js +181 -0
  58. package/dist/storage/repositories/index-connection.js +1 -3
  59. package/dist/storage/repositories/index-entries-repository.js +77 -68
  60. package/dist/storage/repositories/index-entry-schema.js +16 -25
  61. package/dist/storage/repositories/index-fts-repository.js +29 -263
  62. package/dist/storage/repositories/index-meta-repository.js +0 -29
  63. package/dist/storage/repositories/index-schema.js +115 -122
  64. package/dist/storage/repositories/index-utility-repository.js +1 -1
  65. package/dist/storage/repositories/index-vec-repository.js +21 -334
  66. package/dist/storage/repositories/units-repository.js +510 -0
  67. package/docs/migration/release-notes/0.9.15.md +34 -36
  68. package/docs/migration/release-notes/0.9.16.md +110 -0
  69. package/docs/migration/release-notes/README.md +5 -0
  70. package/docs/reference/cli.md +93 -87
  71. package/docs/reference/configuration.md +128 -89
  72. package/docs/reference/data-and-telemetry.md +2 -1
  73. package/package.json +1 -1
  74. package/schemas/akm-config.json +2 -58
  75. package/dist/indexer/index-db-contention.js +0 -56
  76. package/dist/indexer/index-rebuild-lock.js +0 -73
  77. package/dist/indexer/materialize-embeddings.js +0 -771
  78. package/dist/indexer/passes/dir-staleness.js +0 -161
  79. 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` FTS5 search + materialization repository.
5
+ * `index.db` safe-Markdown-fragment repository.
6
6
  *
7
- * Owns the `entries_fts` full-text query path, per-entry projections, and the
8
- * explicit full recovery rebuild.
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 INSERT_FTS_SQL = "INSERT INTO entries_fts (entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?)";
17
- const INSERT_FRAGMENT_SQL = "INSERT INTO entry_fragments_fts (entry_id, fragment_id, fragment_ordinal, content) VALUES (?, ?, ?, ?)";
18
- const ftsMutationStatementsByDb = new WeakMap();
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
- ftsMutationStatementsByDb.set(db, statements);
29
+ fragmentSourceStatementsByDb.set(db, statements);
32
30
  return statements;
33
31
  }
34
- /** Replace one entry's derived FTS projection inside the caller's transaction. */
35
- export function replaceFtsEntry(db, entryId, entry, fragmentContent) {
36
- const fields = buildSearchFields(entry);
37
- const statements = getFtsMutationStatements(db);
38
- statements.deleteOne.run(entryId);
39
- statements.insert.run(entryId, fields.name, fields.description, fields.tags, fields.hints, fields.content);
40
- if (fragmentContent === undefined) {
41
- // Metadata-only re-upserts and re-keys deserialize the public document
42
- // without the internal substrate. Leave the persisted source untouched.
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 (!fragmentContent)
49
- return;
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 derived FTS projections for canonical entries that are being removed. */
56
- export function deleteFtsEntries(db, entryIds) {
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
- }