akm-cli 0.9.16-alpha.1 → 0.9.16

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 (147) hide show
  1. package/CHANGELOG.md +56 -132
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  4. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  5. package/dist/cli/retired-commands.js +0 -4
  6. package/dist/cli/unknown-flags.js +3 -36
  7. package/dist/commands/env/env-binding.js +4 -4
  8. package/dist/commands/env/env-cli.js +3 -3
  9. package/dist/commands/improve/collapse-detector.js +2 -2
  10. package/dist/commands/improve/consolidate.js +4 -6
  11. package/dist/commands/improve/improve-cli.js +20 -15
  12. package/dist/commands/improve/reflect.js +23 -2
  13. package/dist/commands/lint/base-linter.js +9 -0
  14. package/dist/commands/lint/env-key-rules.js +2 -2
  15. package/dist/commands/proposal/propose.js +15 -1
  16. package/dist/commands/proposal/repository.js +3 -12
  17. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  18. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  19. package/dist/commands/read/curate.js +44 -34
  20. package/dist/commands/read/search.js +35 -54
  21. package/dist/commands/read/show.js +21 -2
  22. package/dist/commands/registry-cli.js +5 -5
  23. package/dist/commands/sources/add-cli.js +59 -16
  24. package/dist/commands/sources/bundle-cli.js +35 -11
  25. package/dist/commands/sources/bundle-config-ops.js +30 -0
  26. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  27. package/dist/commands/sources/info.js +8 -8
  28. package/dist/commands/sources/installed-stashes.js +55 -61
  29. package/dist/commands/sources/source-add.js +39 -38
  30. package/dist/commands/sources/source-manage.js +34 -12
  31. package/dist/commands/sources/stash-cli.js +111 -119
  32. package/dist/commands/sources/stash-skeleton.js +6 -3
  33. package/dist/commands/tasks/explain.js +4 -1
  34. package/dist/commands/tasks/tasks-cli.js +31 -9
  35. package/dist/commands/tasks/tasks.js +239 -194
  36. package/dist/commands/tasks/validate.js +20 -32
  37. package/dist/core/activation-policy.js +4 -4
  38. package/dist/core/adapter/adapters/akm-adapter.js +8 -35
  39. package/dist/core/adapter/adapters/akm-metadata.js +1 -11
  40. package/dist/core/adapter/execution-source.js +10 -29
  41. package/dist/core/asset/asset-placement.js +0 -35
  42. package/dist/core/config/config-schema.js +64 -8
  43. package/dist/core/config/config-sources.js +96 -2
  44. package/dist/core/config/config.js +190 -24
  45. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  46. package/dist/core/config/schema/embedding.js +30 -7
  47. package/dist/core/config/schema/execution.js +23 -0
  48. package/dist/core/config/schema/experimental.js +1 -1
  49. package/dist/core/config/schema/scheduler.js +20 -0
  50. package/dist/core/config/schema/search.js +10 -12
  51. package/dist/core/config/schema/sources-bundles.js +32 -1
  52. package/dist/core/content-safety.js +52 -0
  53. package/dist/core/errors.js +2 -5
  54. package/dist/core/maintenance-barrier.js +11 -13
  55. package/dist/core/paths.js +11 -0
  56. package/dist/core/run-lock.js +2 -5
  57. package/dist/core/state/migrations.js +1 -26
  58. package/dist/core/state-db.js +27 -63
  59. package/dist/core/type-presentation.js +1 -1
  60. package/dist/core/write-source.js +13 -8
  61. package/dist/indexer/bundle-identity-guard.js +45 -8
  62. package/dist/indexer/ensure-index.js +0 -5
  63. package/dist/indexer/index-db-contention.js +56 -0
  64. package/dist/indexer/index-rebuild-lock.js +73 -0
  65. package/dist/indexer/index-written-assets.js +171 -133
  66. package/dist/indexer/indexer.js +1621 -458
  67. package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
  68. package/dist/indexer/materialize-embeddings.js +785 -0
  69. package/dist/indexer/passes/dir-staleness.js +161 -0
  70. package/dist/indexer/passes/metadata.js +1 -18
  71. package/dist/indexer/scan/drain-dir.js +70 -27
  72. package/dist/indexer/search/db-search.js +89 -373
  73. package/dist/indexer/search/ranking-contributors.js +16 -21
  74. package/dist/indexer/search/ranking.js +57 -135
  75. package/dist/indexer/search/search-source.js +29 -11
  76. package/dist/integrations/agent/execution-lowering.js +3 -2
  77. package/dist/integrations/agent/execution-preparation.js +32 -1
  78. package/dist/integrations/agent/prompts.js +1 -1
  79. package/dist/integrations/agent/request-lowering.js +3 -2
  80. package/dist/llm/client.js +3 -11
  81. package/dist/llm/embedder.js +3 -10
  82. package/dist/llm/embedders/remote.js +104 -133
  83. package/dist/llm/feature-gate.js +2 -4
  84. package/dist/llm/rerank-client.js +3 -3
  85. package/dist/output/html-render.js +2 -1
  86. package/dist/output/shapes/passthrough.js +2 -1
  87. package/dist/output/stdout.js +24 -0
  88. package/dist/output/text/command-format.js +13 -19
  89. package/dist/output/text/helpers.js +1 -1
  90. package/dist/output/text/index.js +2 -5
  91. package/dist/output/text.js +4 -3
  92. package/dist/registry/resolve.js +37 -10
  93. package/dist/scripts/akm-migrate-node.js +15197 -11351
  94. package/dist/scripts/akm-migrate.js +15514 -11668
  95. package/dist/setup/semantic-assets.js +2 -2
  96. package/dist/setup/setup.js +3 -3
  97. package/dist/setup/steps/connection.js +2 -3
  98. package/dist/setup/steps/tasks.js +29 -36
  99. package/dist/sources/providers/git-install.js +17 -11
  100. package/dist/sources/providers/git-provider.js +12 -5
  101. package/dist/sources/providers/git-stash.js +38 -16
  102. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  103. package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
  104. package/dist/storage/repositories/index-connection.js +3 -1
  105. package/dist/storage/repositories/index-entries-repository.js +68 -77
  106. package/dist/storage/repositories/index-entry-schema.js +25 -16
  107. package/dist/storage/repositories/index-fts-repository.js +263 -29
  108. package/dist/storage/repositories/index-meta-repository.js +29 -0
  109. package/dist/storage/repositories/index-schema.js +122 -115
  110. package/dist/storage/repositories/index-utility-repository.js +1 -1
  111. package/dist/storage/repositories/index-vec-repository.js +435 -22
  112. package/dist/tasks/activation-config.js +90 -0
  113. package/dist/tasks/backends/cron.js +9 -0
  114. package/dist/tasks/backends/launchd.js +1 -0
  115. package/dist/tasks/backends/schtasks.js +2 -0
  116. package/dist/tasks/embedded.js +4 -5
  117. package/dist/tasks/scheduler-binding.js +2 -2
  118. package/dist/tasks/scheduler-sync-preview.js +8 -1
  119. package/dist/tasks/scheduler-sync.js +19 -10
  120. package/dist/tasks/source/parse-task-source.js +10 -113
  121. package/dist/tasks/source/project-v4.js +2 -2
  122. package/dist/tasks/source/task-source-v4.js +4 -12
  123. package/dist/tasks/source/task-to-v3.js +4 -12
  124. package/dist/tasks/source/task-to-v4.js +40 -7
  125. package/docs/migration/README.md +1 -0
  126. package/docs/migration/release-notes/0.9.15.md +36 -34
  127. package/docs/migration/release-notes/0.9.16.md +60 -98
  128. package/docs/migration/release-notes/README.md +0 -5
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  130. package/docs/reference/cli.md +124 -122
  131. package/docs/reference/configuration.md +137 -133
  132. package/docs/reference/data-and-telemetry.md +1 -2
  133. package/docs/reference/tasks.md +34 -29
  134. package/package.json +1 -1
  135. package/schemas/akm-config.json +170 -6
  136. package/schemas/akm-task.json +1 -2
  137. package/dist/commands/sources/index-status.js +0 -99
  138. package/dist/core/hash.js +0 -18
  139. package/dist/indexer/drain.js +0 -306
  140. package/dist/indexer/embedding-identity.js +0 -20
  141. package/dist/indexer/enrich.js +0 -260
  142. package/dist/indexer/reconcile.js +0 -890
  143. package/dist/indexer/scan/parse-file.js +0 -66
  144. package/dist/indexer/units/unit.js +0 -159
  145. package/dist/llm/embedders/provider-limits.js +0 -288
  146. package/dist/storage/repositories/files-repository.js +0 -181
  147. package/dist/storage/repositories/units-repository.js +0 -510
@@ -14,24 +14,22 @@ import { parseBundleRef } from "../../core/asset/asset-ref.js";
14
14
  import { conceptIdFromTypeName } from "../../core/asset/resolve-ref.js";
15
15
  import { bestEffort } from "../../core/best-effort.js";
16
16
  import { isPathAbsent } from "../../core/path-access.js";
17
- import { getStateDbPath, withImmediateTransaction, withStateDb } from "../../core/state-db.js";
17
+ import { getStateDbPath, withStateDb } from "../../core/state-db.js";
18
18
  import { warn } from "../../core/warn.js";
19
19
  import { getMarkdownFragmentContent, hasMarkdownFragmentContent, } from "../../indexer/passes/metadata.js";
20
20
  import { buildSearchText } from "../../indexer/search/search-fields.js";
21
21
  import { ENTRY_COLUMNS, rowToIndexedEntry } from "./index-entry-mapper.js";
22
- import { deleteFragmentSource, replaceFragmentSource } from "./index-fts-repository.js";
22
+ import { deleteFtsEntries, replaceFtsEntry } from "./index-fts-repository.js";
23
23
  import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
24
+ import { deleteEntryVectors, isVecAvailable } from "./index-vec-repository.js";
24
25
  // ── Entry operations ────────────────────────────────────────────────────────
25
26
  /**
26
27
  * Insert or update one canonical entry and all synchronously derived search
27
28
  * state. Returns the stable row id.
28
29
  *
29
- * The entries row and its safe-Markdown fragment source commit as one SQLite
30
- * transaction. Callers therefore cannot publish an entry and forget the
31
- * fragment-source write. (Unit derivation — `unit_texts`/`units_fts`/
32
- * `entry_units` — is the caller's job, driven by reconcile: a changed
33
- * `search_text` needs no explicit vector invalidation here, since a new
34
- * document simply derives new content-addressed unit hashes.)
30
+ * The entries row, FTS projection, and stale-vector invalidation commit as one
31
+ * SQLite transaction. Callers therefore cannot publish an entry and forget a
32
+ * second FTS maintenance step.
35
33
  */
36
34
  export function upsertEntry(db, filePath, entry, searchText, provenance, contentHash) {
37
35
  // Hot path during indexing — cache prepared statements per database
@@ -44,37 +42,20 @@ export function upsertEntry(db, filePath, entry, searchText, provenance, content
44
42
  // `content_hash` is optional on the LLM-enrichment re-upsert; a missing hash
45
43
  // preserves the scan writer's current value.
46
44
  const apply = () => {
45
+ const previous = stmts.findByItemRef.get(provenance.itemRef);
47
46
  const result = stmts.upsert.get(provenance.itemRef, provenance.bundleId, provenance.componentId, provenance.conceptId, provenance.adapterId, entry.type, filePath, contentHash ?? null, JSON.stringify(entry), searchText, derivedFrom);
48
47
  if (!result)
49
48
  throw new Error("upsertEntry: item_ref not found after upsert");
50
- replaceFragmentSource(db, result.id, hasMarkdownFragmentContent(entry) ? (getMarkdownFragmentContent(entry) ?? null) : undefined);
49
+ if (previous?.id === result.id && previous.search_text !== searchText)
50
+ deleteEntryVectors(db, result.id);
51
+ replaceFtsEntry(db, result.id, entry, hasMarkdownFragmentContent(entry) ? (getMarkdownFragmentContent(entry) ?? null) : undefined);
51
52
  return result.id;
52
53
  };
53
- // Both production callers (reconcile.ts's `applyChange`, enrich.ts's
54
- // `applyEnrichmentToEntry`) always invoke this from inside their OWN outer
55
- // `withImmediateTransaction`, so `db.inTransaction` is already true and a
56
- // standalone top-level call never happens today — but a caller that DOES
57
- // call this standalone (no outer transaction) must still get a proper
58
- // `BEGIN IMMEDIATE`, not a bare deferred `db.transaction()` that fails
59
- // instantly with SQLITE_BUSY under a competing writer instead of honouring
60
- // `busy_timeout` (docs/plans/index-redesign.md rule 5). Route ONLY that
61
- // standalone case through `withImmediateTransaction`.
62
- //
63
- // The nested case deliberately keeps the driver's own `db.transaction()`
64
- // instead of `withImmediateTransaction`: the driver lowers a transaction
65
- // opened inside an already-open one to a SAVEPOINT, so a caller that
66
- // catches this mutation's error and continues its OWN outer (bare)
67
- // transaction still cannot commit a partial entries row — the exact
68
- // contract tests/integration/indexer/index-mutation-boundary.test.ts pins
69
- // (#820). `withImmediateTransaction`'s join-if-open guard runs `fn`
70
- // directly with NO isolation boundary of its own (by design, #686 — see
71
- // its doc comment), which would let such a caught, partial write escape
72
- // through the outer transaction's COMMIT instead of rolling back to a
73
- // savepoint. Both real production callers already hold their OWN
74
- // immediate transaction before calling this, so this nested branch is a
75
- // savepoint under an already-immediate lock, not a fresh deferred BEGIN —
76
- // no busy_timeout race to lose here either way.
77
- return db.inTransaction ? db.transaction(apply)() : withImmediateTransaction(db, apply, "index");
54
+ // Always enter the driver's transaction wrapper. Both supported SQLite
55
+ // drivers lower a transaction opened inside another transaction to a
56
+ // savepoint, so a caller that catches this mutation's error cannot commit a
57
+ // partial entries row through its outer transaction.
58
+ return db.transaction(apply)();
78
59
  }
79
60
  const upsertStmtsByDb = new WeakMap();
80
61
  // item_ref is the sole durable conflict target. `content_hash` COALESCEs so a
@@ -107,6 +88,7 @@ function getUpsertStmts(db) {
107
88
  ON CONFLICT(item_ref) DO UPDATE ${UPSERT_SET_CLAUSE}
108
89
  RETURNING id
109
90
  `),
91
+ findByItemRef: db.prepare("SELECT id, search_text FROM entries WHERE item_ref = ?"),
110
92
  };
111
93
  upsertStmtsByDb.set(db, stmts);
112
94
  return stmts;
@@ -182,18 +164,16 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
182
164
  /**
183
165
  * Re-key an entries row in place for the opt-in source-maintenance script.
184
166
  *
185
- * The row id is preserved on purpose — `utility_scores` and
186
- * `utility_scores_scoped` are keyed by `entry_id`, so an UPDATE (rather than
187
- * a delete + insert under the new `item_ref`) is what keeps the asset's
188
- * accumulated usage-ranking history attached across a rename.
189
- * (`asset_salience` / `asset_outcome` live in state.db keyed by `asset_ref`
190
- * TEXT and are re-keyed separately by `akm mv` — see the state rekey
191
- * helper.) `document_json.name` (and `filename`, when present) is patched
192
- * and `search_text` rebuilt so search reflects the new name. Its
193
- * safe-Markdown fragment source is updated in the same transaction as the
194
- * canonical identity; the new `search_text` needs no explicit vector
195
- * invalidation — content-addressed units simply derive new hashes on the
196
- * next reconcile.
167
+ * The row id is preserved on purpose — `utility_scores`,
168
+ * `utility_scores_scoped`, and `embeddings` are keyed by `entry_id`, so an
169
+ * UPDATE (rather than a delete + insert under the new `item_ref`) is what
170
+ * keeps the asset's accumulated usage-ranking history attached across a
171
+ * rename. (`asset_salience` / `asset_outcome` live in state.db keyed by
172
+ * `asset_ref` TEXT and are re-keyed separately by `akm mv` — see
173
+ * the state rekey helper.) `document_json.name` (and `filename`, when
174
+ * present) is patched and `search_text` rebuilt so search reflects the new
175
+ * name. Its FTS projection and stale vector are updated in the same
176
+ * transaction as the canonical identity.
197
177
  *
198
178
  * Bundle-qualified `usage_events.entry_ref` rows for the old conceptId are
199
179
  * rewritten to the new item ref. Without this, events keep the old
@@ -207,8 +187,10 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
207
187
  * A stale row already occupying the new item ref (the caller has verified no
208
188
  * FILE exists at the target, so such a row can only be a leftover for a
209
189
  * deleted file) is evicted first — through {@link deleteRelatedRows}, so its
210
- * child rows (utility scores, usage events) go with it. The moved row keeps
211
- * its id.
190
+ * child rows (embeddings, entries_vec, utility scores, usage events) go with
191
+ * it. A bare `DELETE FROM entries` would trip the non-CASCADE `embeddings`
192
+ * FK under `PRAGMA foreign_keys = ON` and roll back the whole re-key.
193
+ * The moved row keeps its id.
212
194
  *
213
195
  * Returns the surviving row id, or `null` when no row matches the old item ref
214
196
  * (nothing indexed under the old name — the caller falls open and the next
@@ -250,12 +232,14 @@ export function rekeyEntryInPlace(db, opts) {
250
232
  throw new Error(`Refusing to re-key entry ${oldItemRef}: target ref does not match the entry type and name.`);
251
233
  }
252
234
  const newItemRef = `${opts.sourceName}//${opts.newRef}`;
253
- withImmediateTransaction(db, () => {
235
+ db.transaction(() => {
254
236
  const stale = db.prepare("SELECT id FROM entries WHERE item_ref = ?").get(newItemRef);
255
237
  if (stale && stale.id !== row.id) {
256
- // Full child-row cleanup (utility scores, usage events, fragment
257
- // source) BEFORE the entries delete — the FK-less child rows would
258
- // otherwise orphan permanently.
238
+ // Full child-row cleanup (embeddings, entries_vec, utility scores,
239
+ // usage events, FTS + dirty marks) BEFORE the entries delete: the
240
+ // `embeddings` FK is non-CASCADE and `foreign_keys = ON`, so a bare
241
+ // entries delete would throw and roll back the entire re-key; and
242
+ // without it the FK-less child rows would orphan permanently.
259
243
  deleteRelatedRows(db, [{ id: stale.id }]);
260
244
  db.prepare("DELETE FROM entries WHERE id = ?").run(stale.id);
261
245
  }
@@ -263,11 +247,13 @@ export function rekeyEntryInPlace(db, opts) {
263
247
  if (opts.newDerivedFrom !== undefined) {
264
248
  db.prepare("UPDATE entries SET derived_from = ? WHERE id = ?").run(opts.newDerivedFrom, row.id);
265
249
  }
250
+ if (row.search_text !== searchText)
251
+ deleteEntryVectors(db, row.id);
266
252
  if (document)
267
- replaceFragmentSource(db, row.id, hasMarkdownFragmentContent(document) ? (getMarkdownFragmentContent(document) ?? null) : undefined);
253
+ replaceFtsEntry(db, row.id, document, hasMarkdownFragmentContent(document) ? (getMarkdownFragmentContent(document) ?? null) : undefined);
268
254
  else
269
- deleteFragmentSource(db, [row.id]);
270
- }, "index");
255
+ deleteFtsEntries(db, [row.id]);
256
+ })();
271
257
  // Re-point usage history at the new ref. Chunk-8 WI-8.3: usage_events lives in
272
258
  // state.db now, so this is a SEPARATE cross-DB transaction (best-effort — the
273
259
  // rename itself already committed above; on failure the next full index's
@@ -297,9 +283,9 @@ function rewriteUsageEventRefForMove(opts) {
297
283
  };
298
284
  try {
299
285
  withStateDb((stateDb) => {
300
- withImmediateTransaction(stateDb, () => {
286
+ stateDb.transaction(() => {
301
287
  rename(stateDb, `${opts.sourceName}//${opts.oldRef}`, `${opts.sourceName}//${opts.newRef}`);
302
- });
288
+ })();
303
289
  });
304
290
  }
305
291
  catch (error) {
@@ -386,14 +372,14 @@ function deleteEntryRows(db, rows, options = {}) {
386
372
  return rows.map((row) => row.id);
387
373
  }
388
374
  export function deleteEntriesByDirAndBundle(db, dirPath, bundleId, options = {}) {
389
- return withImmediateTransaction(db, () => deleteEntryRows(db, rowsInDirectory(db, dirPath, bundleId), options), "index");
375
+ return db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath, bundleId), options))();
390
376
  }
391
377
  /** Delete every entry and child row belonging to one canonical bundle. */
392
378
  export function deleteEntriesByBundle(db, bundleId) {
393
- withImmediateTransaction(db, () => {
379
+ db.transaction(() => {
394
380
  const rows = db.prepare("SELECT id FROM entries WHERE bundle_id = ?").all(bundleId);
395
381
  deleteEntryRows(db, rows);
396
- }, "index");
382
+ })();
397
383
  }
398
384
  /**
399
385
  * Delete the complete regenerable entry generation through the same child-row
@@ -401,10 +387,10 @@ export function deleteEntriesByBundle(db, bundleId) {
401
387
  * usage events so the finalize pass can relink them to the new row ids.
402
388
  */
403
389
  export function deleteAllEntries(db, options = {}) {
404
- return withImmediateTransaction(db, () => {
390
+ return db.transaction(() => {
405
391
  const rows = db.prepare("SELECT id FROM entries").all();
406
392
  return deleteEntryRows(db, rows, options);
407
- }, "index");
393
+ })();
408
394
  }
409
395
  /**
410
396
  * Diff-persist orphan delete: remove every entry under `dirPath` whose durable
@@ -412,7 +398,7 @@ export function deleteAllEntries(db, options = {}) {
412
398
  *
413
399
  * Replaces the old per-dir `deleteEntriesByDir` + full re-insert: the caller
414
400
  * upserts the current file set first (ON CONFLICT preserving `entries.id`, so
415
- * utility / usage stay attached to unchanged rows), then calls this
401
+ * embeddings / utility / usage stay attached to unchanged rows), then calls this
416
402
  * to prune only the departed rows. The net row-state for the directory is identical
417
403
  * to delete-then-reinsert; the win is that unchanged rows keep their id.
418
404
  *
@@ -420,25 +406,27 @@ export function deleteAllEntries(db, options = {}) {
420
406
  * prune one another's rows.
421
407
  */
422
408
  export function deleteEntriesByDirExceptRefs(db, dirPath, bundleId, keepRefs, options = {}) {
423
- return withImmediateTransaction(db, () => {
409
+ return db.transaction(() => {
424
410
  const doomed = rowsInDirectory(db, dirPath, bundleId).filter((row) => !keepRefs.has(row.item_ref));
425
411
  return deleteEntryRows(db, doomed, options);
426
- }, "index");
412
+ })();
427
413
  }
428
414
  function deleteRelatedRows(db, ids, options = {}) {
429
415
  if (ids.length === 0)
430
416
  return;
431
417
  const numericIds = ids.map((r) => r.id);
432
- // The safe-Markdown fragment source is part of the canonical mutation
433
- // boundary, not a caller-maintained dirty queue. Delete it before the
434
- // parent row inside this transaction (redundant with entry_fragments' own
435
- // ON DELETE CASCADE, but explicit here alongside the other child-row
436
- // cleanup this function owns).
437
- deleteFragmentSource(db, numericIds);
418
+ const vecAvail = isVecAvailable(db);
419
+ // FTS is part of the canonical mutation boundary, not a caller-maintained
420
+ // dirty queue. Delete it before the parent row inside this transaction.
421
+ deleteFtsEntries(db, numericIds);
438
422
  // Process in chunks to stay within SQLITE_MAX_VARIABLE_NUMBER
439
423
  for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
440
424
  const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
441
425
  const placeholders = chunk.map(() => "?").join(",");
426
+ bestEffort(() => db.prepare(`DELETE FROM embeddings WHERE id IN (${placeholders})`).run(...chunk), "delete embeddings for entries");
427
+ if (vecAvail) {
428
+ bestEffort(() => db.prepare(`DELETE FROM entries_vec WHERE id IN (${placeholders})`).run(...chunk), "delete entries_vec for entries");
429
+ }
442
430
  // Clean up utility scores before deleting entries
443
431
  bestEffort(() => db.prepare(`DELETE FROM utility_scores WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores for entries");
444
432
  bestEffort(() => db.prepare(`DELETE FROM utility_scores_scoped WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores_scoped for entries");
@@ -493,14 +481,15 @@ export function deleteUsageEventsByEntryIds(entryIds) {
493
481
  }
494
482
  /**
495
483
  * Delete entries by their primary key IDs, along with all related rows
496
- * (entry_fragments, entry_units, utility scores, usage_events).
484
+ * (embeddings, entries_vec, entries_fts, utility scores, usage_events).
497
485
  *
498
- * Used by `reconcile.ts` to remove entries whose source files are gone.
486
+ * Used by explicit `--clean` reconciliation before embeddings and final
487
+ * verification to remove stale entries whose source files no longer exist.
499
488
  */
500
489
  export function deleteEntriesByIds(db, ids) {
501
490
  if (ids.length === 0)
502
491
  return;
503
- withImmediateTransaction(db, () => {
492
+ db.transaction(() => {
504
493
  const idObjs = ids.map((id) => ({ id }));
505
494
  deleteRelatedRows(db, idObjs);
506
495
  for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
@@ -508,7 +497,7 @@ export function deleteEntriesByIds(db, ids) {
508
497
  const placeholders = chunk.map(() => "?").join(",");
509
498
  db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk);
510
499
  }
511
- }, "index");
500
+ })();
512
501
  }
513
502
  // ── All entries ─────────────────────────────────────────────────────────────
514
503
  function parseEntryRows(rows, context) {
@@ -803,10 +792,11 @@ export function relinkUsageEvents(indexDb, stateDb, options = {}) {
803
792
  });
804
793
  if (staleLinks.length > 0) {
805
794
  const nullOut = stateDb.prepare(`UPDATE ${usageEvents} SET entry_id = NULL WHERE entry_id = ? AND entry_ref IS ?`);
806
- withImmediateTransaction(stateDb, () => {
795
+ const nullTx = stateDb.transaction(() => {
807
796
  for (const { id, ref } of staleLinks)
808
797
  nullOut.run(id, ref);
809
798
  });
799
+ nullTx();
810
800
  }
811
801
  // Step 2: re-resolve each fully-qualified ref. Bare rows are not current
812
802
  // durable identities and remain detached.
@@ -814,7 +804,7 @@ export function relinkUsageEvents(indexDb, stateDb, options = {}) {
814
804
  .prepare(`SELECT DISTINCT entry_ref AS ref FROM ${usageEvents} WHERE entry_id IS NULL AND entry_ref IS NOT NULL`)
815
805
  .all();
816
806
  const update = stateDb.prepare(`UPDATE ${usageEvents} SET entry_id = ? WHERE entry_ref = ? AND entry_id IS NULL`);
817
- withImmediateTransaction(stateDb, () => {
807
+ const relinkTx = stateDb.transaction(() => {
818
808
  for (const { ref } of refs) {
819
809
  let id;
820
810
  try {
@@ -829,5 +819,6 @@ export function relinkUsageEvents(indexDb, stateDb, options = {}) {
829
819
  update.run(id, ref);
830
820
  }
831
821
  });
822
+ relinkTx();
832
823
  }, "usage_events table may not exist yet during entry_id re-resolution");
833
824
  }
@@ -12,18 +12,7 @@
12
12
  */
13
13
  // v23 adds an isolated fragment FTS population. v22 is the last shipped
14
14
  // generation and is intentionally rebuilt rather than migrated in place.
15
- //
16
- // v23→v24 (index-redesign B5c): `entries_fts` and `entry_fragments_fts` are
17
- // dropped from the canonical shape. Lexical search runs entirely over
18
- // `units_fts` now (index-redesign-contract.md B1/B3) — the card unit already
19
- // carries name/description/tags/hints, so an entry-level lexical query is a
20
- // units query grouped by entry, and a fragment-level lexical query is the
21
- // same `units_fts` table filtered to fragment-kind units. `entry_fragments`
22
- // (the safe-rendered Markdown source, NOT an FTS index) stays: it is what a
23
- // matched fragment hit's display metadata is projected from, and what `akm
24
- // show <ref>#<fragmentId>` resolves an opaque fragment selector through —
25
- // both are consumers independent of which table search itself queries.
26
- export const CANONICAL_INDEX_DB_VERSION = 24;
15
+ export const CANONICAL_INDEX_DB_VERSION = 23;
27
16
  export const CANONICAL_ENTRY_SCHEMA_SQL = `
28
17
  CREATE TABLE IF NOT EXISTS entries (
29
18
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -45,14 +34,30 @@ export const CANONICAL_ENTRY_SCHEMA_SQL = `
45
34
  CREATE INDEX IF NOT EXISTS idx_entries_file_path ON entries(file_path);
46
35
  CREATE INDEX IF NOT EXISTS idx_entries_derived_from ON entries(derived_from);
47
36
 
48
- -- The safe-rendered Markdown source a matched fragment hit's display
49
- -- metadata is projected from (index-fts-repository.ts's
50
- -- getIndexedMarkdownFragment(s)) and that akm show's opaque fragment
51
- -- selectors resolve through. Not a search index — units_fts is.
37
+ -- Keep parent metadata and body fragments in separate FTS populations.
38
+ -- Combining them changes parent-document IDF and conjunction semantics.
39
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
40
+ entry_id UNINDEXED,
41
+ name,
42
+ description,
43
+ tags,
44
+ hints,
45
+ content,
46
+ tokenize='porter unicode61'
47
+ );
48
+
52
49
  CREATE TABLE IF NOT EXISTS entry_fragments (
53
50
  entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE,
54
51
  safe_markdown TEXT NOT NULL
55
52
  );
53
+
54
+ CREATE VIRTUAL TABLE IF NOT EXISTS entry_fragments_fts USING fts5(
55
+ entry_id UNINDEXED,
56
+ fragment_id UNINDEXED,
57
+ fragment_ordinal UNINDEXED,
58
+ content,
59
+ tokenize='porter unicode61'
60
+ );
56
61
  `;
57
62
  const CANONICAL_ENTRY_SCHEMA_FINGERPRINT = {
58
63
  tableSql: "CREATE TABLE entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_ref TEXT NOT NULL UNIQUE, bundle_id TEXT NOT NULL, component_id TEXT NOT NULL, concept_id TEXT NOT NULL, adapter_id TEXT NOT NULL, type TEXT NOT NULL, file_path TEXT NOT NULL, content_hash TEXT, document_json TEXT NOT NULL, search_text TEXT NOT NULL, derived_from TEXT )",
@@ -205,7 +210,9 @@ const CANONICAL_ENTRY_SCHEMA_FINGERPRINT = {
205
210
  },
206
211
  ],
207
212
  searchSurfaces: {
213
+ entriesFtsSql: "CREATE VIRTUAL TABLE entries_fts USING fts5( entry_id UNINDEXED, name, description, tags, hints, content, tokenize='porter unicode61' )",
208
214
  fragmentSourceSql: "CREATE TABLE entry_fragments ( entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE, safe_markdown TEXT NOT NULL )",
215
+ fragmentsFtsSql: "CREATE VIRTUAL TABLE entry_fragments_fts USING fts5( entry_id UNINDEXED, fragment_id UNINDEXED, fragment_ordinal UNINDEXED, content, tokenize='porter unicode61' )",
209
216
  },
210
217
  };
211
218
  function sqlString(value) {
@@ -263,7 +270,9 @@ export function readEntrySchemaFingerprint(db) {
263
270
  columns,
264
271
  indexes,
265
272
  searchSurfaces: {
273
+ entriesFtsSql: readNamedTableSql(db, "entries_fts"),
266
274
  fragmentSourceSql: readNamedTableSql(db, "entry_fragments"),
275
+ fragmentsFtsSql: readNamedTableSql(db, "entry_fragments_fts"),
267
276
  },
268
277
  };
269
278
  }