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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../llm/embedders/deterministic.js";
|
|
5
|
+
import { DEFAULT_LOCAL_MODEL } from "../llm/embedders/local.js";
|
|
6
|
+
/**
|
|
7
|
+
* Returns `undefined` when nothing was actually observed this call (no
|
|
8
|
+
* vector to measure yet) — there is nothing to key an identity on.
|
|
9
|
+
*/
|
|
10
|
+
export function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVectorLen) {
|
|
11
|
+
if (isDeterministicEmbedEnabled()) {
|
|
12
|
+
return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
|
|
13
|
+
}
|
|
14
|
+
if (observedVectorLen === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (embedding?.endpoint) {
|
|
17
|
+
return `remote:${observedModel ?? embedding.model ?? "unknown"}|${observedVectorLen}`;
|
|
18
|
+
}
|
|
19
|
+
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* LLM metadata-enrichment pass, restored on the reconcile path
|
|
6
|
+
* (docs/plans/index-redesign-contract.md, B5e).
|
|
7
|
+
*
|
|
8
|
+
* Before the index redesign, `akm index` ran a config-driven metadata
|
|
9
|
+
* enhancement pass over every "generated"-quality entry, keyed by a
|
|
10
|
+
* `(assetRef, cacheVariant)` cache row whose `body_hash` column happened to
|
|
11
|
+
* gate freshness. The reconcile rewrite (`reconcile.ts`, B1) dropped the
|
|
12
|
+
* call site along with the rest of the old phase pipeline. This module
|
|
13
|
+
* restores the feature on the NEW path, content-addressed throughout:
|
|
14
|
+
* `reconcileRoots` collects one {@link MetadataEnrichmentCandidate} per file
|
|
15
|
+
* it just upserted (added or changed) and hands the batch to
|
|
16
|
+
* {@link enrichReconciledEntries} once, AFTER every per-file transaction in
|
|
17
|
+
* this run has already committed — a provider call must never run inside
|
|
18
|
+
* `applyChange`'s `BEGIN IMMEDIATE` transaction (docs/plans/index-redesign.md
|
|
19
|
+
* rule 5: every index write stays a short, idempotent transaction; an LLM
|
|
20
|
+
* call can take seconds to minutes and must not hold one open).
|
|
21
|
+
*
|
|
22
|
+
* **Content-addressed cache** — `llm_enrichment_cache` (still shared with
|
|
23
|
+
* graph-extraction and memory-inference, which key it by absolute file path)
|
|
24
|
+
* is used here with `asset_ref = body_hash = candidate.blobHash`: the cache
|
|
25
|
+
* row IS the content address, so a cache hit means "this exact byte content
|
|
26
|
+
* has already been enriched" regardless of which entry or how many entries
|
|
27
|
+
* currently carry it, and a rename or an unrelated field edit elsewhere in
|
|
28
|
+
* the same file never invalidates it. `withLlmCache` (`./db/llm-cache.ts`)
|
|
29
|
+
* already implements exactly this hash-gated lookup/call/write shape, so this
|
|
30
|
+
* module reuses it rather than duplicating the pattern a third time.
|
|
31
|
+
*
|
|
32
|
+
* **Fail-soft** — `enhanceMetadata`'s `EnhanceMetadataOutcome` distinguishes
|
|
33
|
+
* `enriched` (real success — cache it) from `skipped` (feature gate closed)
|
|
34
|
+
* and `failed` (provider/network error): `withLlmCache`'s "only cache a
|
|
35
|
+
* defined result" contract means a `skipped`/`failed` outcome (mapped to
|
|
36
|
+
* `undefined` below) writes no cache row and leaves the entry's `quality`
|
|
37
|
+
* untouched, so a transient provider outage can never poison an entry into a
|
|
38
|
+
* permanent enrichment skip.
|
|
39
|
+
*
|
|
40
|
+
* **`--full` re-applies without a new provider call** — `reconcileRoots`'s
|
|
41
|
+
* `forceReparse` re-parses every file, so an unchanged file's fresh
|
|
42
|
+
* `IndexDocument` is `quality: "generated"` again (enrichment only ever
|
|
43
|
+
* updated the DB row, never the source file) and becomes an enrichment
|
|
44
|
+
* candidate again on every `--full` run. Its `blobHash` is unchanged, so the
|
|
45
|
+
* content-addressed cache lookup above hits and re-applies the SAME cached
|
|
46
|
+
* fields with no new provider call — this falls out of content-addressing
|
|
47
|
+
* for free and needs no `--full`-specific branch here.
|
|
48
|
+
*/
|
|
49
|
+
import fs from "node:fs";
|
|
50
|
+
import { concurrentMap } from "../core/concurrent.js";
|
|
51
|
+
import { ConfigError } from "../core/errors.js";
|
|
52
|
+
import { defaultConcurrencyForEndpoint } from "../core/loopback.js";
|
|
53
|
+
import { withImmediateTransaction } from "../core/state-db.js";
|
|
54
|
+
import { warn } from "../core/warn.js";
|
|
55
|
+
import { resolveIndexPassExecution } from "../llm/index-passes.js";
|
|
56
|
+
import { enhanceMetadata } from "../llm/metadata-enhance.js";
|
|
57
|
+
import { insertNewUnitTexts } from "../storage/repositories/files-repository.js";
|
|
58
|
+
import { upsertEntry } from "../storage/repositories/index-entries-repository.js";
|
|
59
|
+
import { replaceEntryUnits } from "../storage/repositories/units-repository.js";
|
|
60
|
+
import { withLlmCache } from "./db/llm-cache.js";
|
|
61
|
+
import { getMarkdownFragmentContent, hasMarkdownFragmentContent, isEnrichmentComplete, setMarkdownFragmentContent, } from "./passes/metadata.js";
|
|
62
|
+
import { buildSearchText } from "./search/search-fields.js";
|
|
63
|
+
import { deriveUnits, toUnitSource } from "./units/unit.js";
|
|
64
|
+
/**
|
|
65
|
+
* Namespaces this pass's `llm_enrichment_cache` rows away from
|
|
66
|
+
* graph-extraction's and memory-inference's own `cacheVariant` values, which
|
|
67
|
+
* key the SAME shared table by absolute file path rather than content hash.
|
|
68
|
+
*/
|
|
69
|
+
const METADATA_ENRICHMENT_CACHE_VARIANT = "metadata-enhance-v1";
|
|
70
|
+
function emptyCounts() {
|
|
71
|
+
return { attempted: 0, cacheHits: 0, enriched: 0, failed: 0, skipped: 0 };
|
|
72
|
+
}
|
|
73
|
+
/** Only "generated"-quality entries missing description/tags/searchHints are worth an LLM call — see `isEnrichmentComplete`. */
|
|
74
|
+
function isEligibleForEnrichment(entry) {
|
|
75
|
+
return entry.quality === "generated" && !isEnrichmentComplete(entry);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Bounded-pool width for this pass — kept as a direct call to the shared
|
|
79
|
+
* classifier (not `indexer.ts`'s `getDefaultLlmConcurrency` wrapper) to avoid
|
|
80
|
+
* an indexer.ts → enrich.ts → indexer.ts import cycle, exactly like
|
|
81
|
+
* `src/llm/embedders/remote.ts`'s `resolveEmbeddingConcurrency` — see that
|
|
82
|
+
* function's neighboring comment. `tests/indexer/llm-concurrency-default.test.ts`
|
|
83
|
+
* pins `getDefaultLlmConcurrency`'s behavior; this mirrors it exactly.
|
|
84
|
+
*/
|
|
85
|
+
function resolveEnrichmentConcurrency(connection) {
|
|
86
|
+
if (typeof connection?.concurrency === "number")
|
|
87
|
+
return connection.concurrency;
|
|
88
|
+
return defaultConcurrencyForEndpoint(connection?.endpoint);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Run the metadata-enrichment pass over every eligible candidate
|
|
92
|
+
* `reconcileRoots` collected this run, with a bounded concurrency pool
|
|
93
|
+
* (`resolveEnrichmentConcurrency`). Only called when
|
|
94
|
+
* `resolveIndexPassExecution("enrichment", config)` resolves a runner — an
|
|
95
|
+
* unconfigured engine, or `index.enrichment.enabled: false`, is a no-op with
|
|
96
|
+
* zero cache reads and zero provider calls. The separate `metadata_enhance`
|
|
97
|
+
* feature gate (`index.metadataEnhance.enabled`, default `false`) is checked
|
|
98
|
+
* per-call inside `enhanceMetadata` itself, so a closed gate still shows up
|
|
99
|
+
* here as a cheap `skipped` outcome rather than being special-cased twice.
|
|
100
|
+
*
|
|
101
|
+
* A `ConfigError` (a required symbolic credential that resolved to nothing)
|
|
102
|
+
* is not fail-soft like a provider error — `enhanceMetadata` lets it escape
|
|
103
|
+
* `tryLlmFeature`'s normal fallback (`llm/structured-call.ts`'s
|
|
104
|
+
* `callStructured`) precisely so a genuinely broken config surfaces loudly
|
|
105
|
+
* instead of reading as an ordinary per-entry failure. `concurrentMap`
|
|
106
|
+
* itself swallows a thrown callback into an `undefined` slot, so this
|
|
107
|
+
* catches it per-candidate and rethrows the first occurrence once every
|
|
108
|
+
* in-flight candidate has settled.
|
|
109
|
+
*/
|
|
110
|
+
export async function enrichReconciledEntries(db, config, candidates, maxChars, opts) {
|
|
111
|
+
const counts = emptyCounts();
|
|
112
|
+
const eligible = candidates.filter((candidate) => isEligibleForEnrichment(candidate.entry));
|
|
113
|
+
if (eligible.length === 0)
|
|
114
|
+
return counts;
|
|
115
|
+
const runner = resolveIndexPassExecution("enrichment", config).runner;
|
|
116
|
+
if (!runner)
|
|
117
|
+
return counts;
|
|
118
|
+
const concurrency = resolveEnrichmentConcurrency(runner.connection);
|
|
119
|
+
opts?.onProgress?.(`Metadata enrichment starting for ${eligible.length} entr${eligible.length === 1 ? "y" : "ies"} (concurrency ${concurrency}).`);
|
|
120
|
+
let configFailure;
|
|
121
|
+
await concurrentMap(eligible, async (candidate) => {
|
|
122
|
+
if (opts?.signal?.aborted)
|
|
123
|
+
return;
|
|
124
|
+
counts.attempted++;
|
|
125
|
+
try {
|
|
126
|
+
await enrichOneCandidate(db, runner, config, candidate, maxChars, counts, opts?.signal);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof ConfigError) {
|
|
130
|
+
configFailure ??= err;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}, concurrency);
|
|
136
|
+
if (configFailure)
|
|
137
|
+
throw configFailure;
|
|
138
|
+
opts?.onProgress?.(`Metadata enrichment finished: ${counts.enriched} enriched (${counts.cacheHits} from cache), ` +
|
|
139
|
+
`${counts.failed} failed, ${counts.skipped} skipped.`);
|
|
140
|
+
if (counts.failed > 0 && counts.enriched === 0 && counts.skipped === 0) {
|
|
141
|
+
warn(`LLM metadata enrichment failed for all ${counts.failed} attempted entr${counts.failed === 1 ? "y" : "ies"} — ` +
|
|
142
|
+
"index built without enrichment. Check the engine selected by index.enrichment.engine (or defaults.llmEngine).");
|
|
143
|
+
}
|
|
144
|
+
return counts;
|
|
145
|
+
}
|
|
146
|
+
async function enrichOneCandidate(db, runner, config, candidate, maxChars, counts, signal) {
|
|
147
|
+
let sawOutcome;
|
|
148
|
+
let cacheHit = false;
|
|
149
|
+
const metadata = await withLlmCache(db, candidate.blobHash, "", false, async () => {
|
|
150
|
+
let fileContent;
|
|
151
|
+
try {
|
|
152
|
+
fileContent = fs.readFileSync(candidate.filePath, "utf8");
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Best-effort context for the prompt only — enhanceMetadata still
|
|
156
|
+
// runs (with less context) when the file cannot be re-read.
|
|
157
|
+
}
|
|
158
|
+
const outcome = await enhanceMetadata(runner, candidate.entry, fileContent, signal, config);
|
|
159
|
+
if (outcome.status !== "enriched") {
|
|
160
|
+
sawOutcome = outcome.status;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return outcome.metadata;
|
|
164
|
+
}, (raw) => (raw !== null && typeof raw === "object" ? raw : undefined), candidate.blobHash, METADATA_ENRICHMENT_CACHE_VARIANT, { onCacheHit: () => (cacheHit = true) });
|
|
165
|
+
if (metadata === undefined) {
|
|
166
|
+
if (sawOutcome === "failed")
|
|
167
|
+
counts.failed++;
|
|
168
|
+
else
|
|
169
|
+
counts.skipped++;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let applied;
|
|
173
|
+
try {
|
|
174
|
+
applied = applyEnrichmentToEntry(db, candidate, maxChars, metadata);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// A real write failure (not the stale-identity no-op below, which never
|
|
178
|
+
// throws): `concurrentMap` would otherwise swallow this into a silent
|
|
179
|
+
// undefined slot with `counts.enriched` never incremented but no record
|
|
180
|
+
// of the failure either. Surface it the same way a provider failure is
|
|
181
|
+
// already surfaced.
|
|
182
|
+
counts.failed++;
|
|
183
|
+
warn(`[index] Metadata enrichment write failed for ${candidate.filePath}: ` +
|
|
184
|
+
(err instanceof Error ? err.message : String(err)));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!applied) {
|
|
188
|
+
// The live `entries` row no longer matches the identity this candidate
|
|
189
|
+
// was queued under (a concurrent rename or delete) — see
|
|
190
|
+
// `applyEnrichmentToEntry`. The metadata is real and already cached
|
|
191
|
+
// above, so this is reported as skipped rather than lost, and the next
|
|
192
|
+
// ordinary run re-applies it from cache with no new provider call.
|
|
193
|
+
counts.skipped++;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (cacheHit)
|
|
197
|
+
counts.cacheHits++;
|
|
198
|
+
counts.enriched++;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Merge enrichment fields onto the candidate's entry, re-derive its units,
|
|
202
|
+
* and write both through the SAME canonical entry/FTS mutation and
|
|
203
|
+
* `unit_texts`/`entry_units` maintenance `applyChange` (`reconcile.ts`) uses
|
|
204
|
+
* — one short `BEGIN IMMEDIATE` transaction, no `content_hash` argument so
|
|
205
|
+
* `upsertEntry`'s `COALESCE` preserves the scan-derived blob hash untouched.
|
|
206
|
+
*
|
|
207
|
+
* Fragment units are unaffected: only `description`/`tags`/`searchHints`
|
|
208
|
+
* change, which feeds solely unit ordinal 0 (`structuredFieldsText`,
|
|
209
|
+
* `units/unit.ts`); `replaceEntryUnits` is still a full delete-then-insert
|
|
210
|
+
* for the entry, so `getMarkdownFragmentContent`/`setMarkdownFragmentContent`
|
|
211
|
+
* re-tag the merged copy — otherwise `deriveUnits` would see no markdown
|
|
212
|
+
* body at all and silently drop every fragment unit `applyChange` already
|
|
213
|
+
* derived for this entry.
|
|
214
|
+
*
|
|
215
|
+
* **Stale-identity guard** — `candidate` was captured before the LLM round
|
|
216
|
+
* trip above, which can take seconds to minutes. A concurrent reconcile can
|
|
217
|
+
* rename (`repointEntry` updates the SAME `entries.id` in place with a new
|
|
218
|
+
* `item_ref`/`content_hash`/`file_path`) or delete this row while that call
|
|
219
|
+
* was in flight. Writing the captured values regardless would either (a)
|
|
220
|
+
* `upsertEntry` under the stale `item_ref`, which no longer conflicts with
|
|
221
|
+
* anything and INSERTs a ghost row pointing at an identity that no longer
|
|
222
|
+
* exists, or (b) `replaceEntryUnits(candidate.entryId)` overwriting a live
|
|
223
|
+
* renamed row's units with the old identity's hashes — or, if the row was
|
|
224
|
+
* deleted outright, throw a foreign-key error. So this re-reads the live row
|
|
225
|
+
* BY ID inside the same transaction and no-ops (returns `false`, counted as
|
|
226
|
+
* skipped by the caller) unless its `content_hash` and `item_ref` still
|
|
227
|
+
* match what this candidate was queued under; only then is `candidate.entryId`
|
|
228
|
+
* — now confirmed live, never a captured id that may no longer exist — used
|
|
229
|
+
* to write.
|
|
230
|
+
*/
|
|
231
|
+
function applyEnrichmentToEntry(db, candidate, maxChars, metadata) {
|
|
232
|
+
const merged = { ...candidate.entry, quality: "enriched" };
|
|
233
|
+
if (metadata.description)
|
|
234
|
+
merged.description = metadata.description;
|
|
235
|
+
if (metadata.tags?.length)
|
|
236
|
+
merged.tags = metadata.tags;
|
|
237
|
+
if (metadata.searchHints?.length)
|
|
238
|
+
merged.searchHints = metadata.searchHints;
|
|
239
|
+
if (hasMarkdownFragmentContent(candidate.entry)) {
|
|
240
|
+
setMarkdownFragmentContent(merged, getMarkdownFragmentContent(candidate.entry));
|
|
241
|
+
}
|
|
242
|
+
const searchText = buildSearchText(merged);
|
|
243
|
+
return withImmediateTransaction(db, () => {
|
|
244
|
+
const live = db
|
|
245
|
+
.prepare("SELECT content_hash AS contentHash, item_ref AS itemRef FROM entries WHERE id = ?")
|
|
246
|
+
.get(candidate.entryId);
|
|
247
|
+
if (!live || live.contentHash !== candidate.blobHash || live.itemRef !== candidate.provenance.itemRef) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
upsertEntry(db, candidate.filePath, merged, searchText, candidate.provenance);
|
|
251
|
+
const units = deriveUnits(toUnitSource(candidate.entryId, merged), maxChars);
|
|
252
|
+
insertNewUnitTexts(db, units.map((unit) => ({
|
|
253
|
+
hash: unit.hash,
|
|
254
|
+
kind: unit.fragmentId === null ? "card" : "fragment",
|
|
255
|
+
text: unit.text,
|
|
256
|
+
})));
|
|
257
|
+
replaceEntryUnits(db, candidate.entryId, units.map((unit) => ({ ordinal: unit.ordinal, fragmentId: unit.fragmentId, hash: unit.hash })));
|
|
258
|
+
return true;
|
|
259
|
+
}, "index");
|
|
260
|
+
}
|
|
@@ -193,6 +193,11 @@ function indexCanServeStash(stashDir) {
|
|
|
193
193
|
}
|
|
194
194
|
async function runInlineReindex(stashDir, options = {}) {
|
|
195
195
|
const { akmIndex } = await import("./indexer.js");
|
|
196
|
+
// The embedding drain on this implicit path is bounded (see
|
|
197
|
+
// `IndexOptions.implicit`, src/indexer/indexer.ts): a read command's
|
|
198
|
+
// inline bootstrap embeds one provider request's worth of units and
|
|
199
|
+
// leaves the rest of the durable queue to a later drain, so a first
|
|
200
|
+
// `search`/`show` against a fresh index never blocks on the whole corpus.
|
|
196
201
|
await akmIndex({
|
|
197
202
|
stashDir,
|
|
198
203
|
implicit: true,
|