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.
- package/CHANGELOG.md +56 -132
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -4
- package/dist/cli/unknown-flags.js +3 -36
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +4 -6
- package/dist/commands/improve/improve-cli.js +20 -15
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/repository.js +3 -12
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/curate.js +44 -34
- package/dist/commands/read/search.js +35 -54
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +55 -61
- package/dist/commands/sources/source-add.js +39 -38
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-cli.js +111 -119
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +8 -35
- package/dist/core/adapter/adapters/akm-metadata.js +1 -11
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/asset/asset-placement.js +0 -35
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/embedding.js +30 -7
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +10 -12
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/errors.js +2 -5
- package/dist/core/maintenance-barrier.js +11 -13
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +2 -5
- package/dist/core/state/migrations.js +1 -26
- package/dist/core/state-db.js +27 -63
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/ensure-index.js +0 -5
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-written-assets.js +171 -133
- package/dist/indexer/indexer.js +1621 -458
- package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
- package/dist/indexer/materialize-embeddings.js +785 -0
- package/dist/indexer/passes/dir-staleness.js +161 -0
- package/dist/indexer/passes/metadata.js +1 -18
- package/dist/indexer/scan/drain-dir.js +70 -27
- package/dist/indexer/search/db-search.js +89 -373
- package/dist/indexer/search/ranking-contributors.js +16 -21
- package/dist/indexer/search/ranking.js +57 -135
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +3 -11
- package/dist/llm/embedder.js +3 -10
- package/dist/llm/embedders/remote.js +104 -133
- package/dist/llm/feature-gate.js +2 -4
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/html-render.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/stdout.js +24 -0
- package/dist/output/text/command-format.js +13 -19
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +2 -5
- package/dist/output/text.js +4 -3
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +15197 -11351
- package/dist/scripts/akm-migrate.js +15514 -11668
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/connection.js +2 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-connection.js +3 -1
- package/dist/storage/repositories/index-entries-repository.js +68 -77
- package/dist/storage/repositories/index-entry-schema.js +25 -16
- package/dist/storage/repositories/index-fts-repository.js +263 -29
- package/dist/storage/repositories/index-meta-repository.js +29 -0
- package/dist/storage/repositories/index-schema.js +122 -115
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +435 -22
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.15.md +36 -34
- package/docs/migration/release-notes/0.9.16.md +60 -98
- package/docs/migration/release-notes/README.md +0 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +124 -122
- package/docs/reference/configuration.md +137 -133
- package/docs/reference/data-and-telemetry.md +1 -2
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +170 -6
- package/schemas/akm-task.json +1 -2
- package/dist/commands/sources/index-status.js +0 -99
- package/dist/core/hash.js +0 -18
- package/dist/indexer/drain.js +0 -306
- package/dist/indexer/embedding-identity.js +0 -20
- package/dist/indexer/enrich.js +0 -260
- package/dist/indexer/reconcile.js +0 -890
- package/dist/indexer/scan/parse-file.js +0 -66
- package/dist/indexer/units/unit.js +0 -159
- package/dist/llm/embedders/provider-limits.js +0 -288
- package/dist/storage/repositories/files-repository.js +0 -181
- 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,
|
|
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 {
|
|
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
|
|
30
|
-
* transaction. Callers therefore cannot publish an entry and forget
|
|
31
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
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
|
|
186
|
-
* `utility_scores_scoped` are keyed by `entry_id`, so an
|
|
187
|
-
* a delete + insert under the new `item_ref`) is what
|
|
188
|
-
* accumulated usage-ranking history attached across a
|
|
189
|
-
* (`asset_salience` / `asset_outcome` live in state.db keyed by
|
|
190
|
-
* TEXT and are re-keyed separately by `akm mv` — see
|
|
191
|
-
* helper.) `document_json.name` (and `filename`, when
|
|
192
|
-
* and `search_text` rebuilt so search reflects the new
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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
|
|
211
|
-
*
|
|
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
|
-
|
|
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 (
|
|
257
|
-
//
|
|
258
|
-
//
|
|
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
|
-
|
|
253
|
+
replaceFtsEntry(db, row.id, document, hasMarkdownFragmentContent(document) ? (getMarkdownFragmentContent(document) ?? null) : undefined);
|
|
268
254
|
else
|
|
269
|
-
|
|
270
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
379
|
+
db.transaction(() => {
|
|
394
380
|
const rows = db.prepare("SELECT id FROM entries WHERE bundle_id = ?").all(bundleId);
|
|
395
381
|
deleteEntryRows(db, rows);
|
|
396
|
-
}
|
|
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
|
|
390
|
+
return db.transaction(() => {
|
|
405
391
|
const rows = db.prepare("SELECT id FROM entries").all();
|
|
406
392
|
return deleteEntryRows(db, rows, options);
|
|
407
|
-
}
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
433
|
-
// boundary, not a caller-maintained
|
|
434
|
-
// parent row inside this transaction
|
|
435
|
-
|
|
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
|
-
* (
|
|
484
|
+
* (embeddings, entries_vec, entries_fts, utility scores, usage_events).
|
|
497
485
|
*
|
|
498
|
-
* Used by `
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
49
|
-
--
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|