akm-cli 0.9.15 → 0.9.16-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/cli/retired-commands.js +2 -0
- package/dist/cli/unknown-flags.js +36 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +6 -4
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/proposal/repository.js +12 -3
- package/dist/commands/read/curate.js +34 -44
- package/dist/commands/read/search.js +50 -2
- package/dist/commands/sources/index-status.js +99 -0
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +33 -12
- package/dist/commands/sources/source-add.js +21 -6
- package/dist/commands/sources/stash-cli.js +119 -111
- package/dist/core/adapter/adapters/akm-adapter.js +35 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -1
- package/dist/core/asset/asset-placement.js +35 -0
- package/dist/core/config/schema/embedding.js +7 -30
- package/dist/core/config/schema/search.js +11 -9
- package/dist/core/errors.js +5 -2
- package/dist/core/hash.js +18 -0
- package/dist/core/maintenance-barrier.js +8 -6
- package/dist/core/paths.js +0 -11
- package/dist/core/run-lock.js +5 -2
- package/dist/core/state/migrations.js +26 -1
- package/dist/core/state-db.js +63 -27
- package/dist/indexer/drain.js +306 -0
- package/dist/indexer/embedding-identity.js +20 -0
- package/dist/indexer/enrich.js +260 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/index-written-assets.js +133 -171
- package/dist/indexer/indexer.js +458 -1621
- package/dist/indexer/lookup/adapter-concept-owner.js +19 -5
- package/dist/indexer/passes/metadata.js +18 -1
- package/dist/indexer/reconcile.js +890 -0
- package/dist/indexer/scan/drain-dir.js +27 -70
- package/dist/indexer/scan/parse-file.js +66 -0
- package/dist/indexer/search/db-search.js +373 -89
- package/dist/indexer/search/ranking-contributors.js +21 -16
- package/dist/indexer/search/ranking.js +135 -57
- package/dist/indexer/units/unit.js +159 -0
- package/dist/llm/client.js +10 -1
- package/dist/llm/embedder.js +10 -3
- package/dist/llm/embedders/provider-limits.js +288 -0
- package/dist/llm/embedders/remote.js +133 -104
- package/dist/llm/feature-gate.js +4 -2
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +19 -13
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +5 -2
- package/dist/scripts/akm-migrate-node.js +1141 -1237
- package/dist/scripts/akm-migrate.js +1141 -1237
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/steps/connection.js +3 -2
- package/dist/storage/repositories/files-repository.js +181 -0
- package/dist/storage/repositories/index-connection.js +1 -3
- package/dist/storage/repositories/index-entries-repository.js +77 -68
- package/dist/storage/repositories/index-entry-schema.js +16 -25
- package/dist/storage/repositories/index-fts-repository.js +29 -263
- package/dist/storage/repositories/index-meta-repository.js +0 -29
- package/dist/storage/repositories/index-schema.js +115 -122
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +21 -334
- package/dist/storage/repositories/units-repository.js +510 -0
- package/docs/migration/release-notes/0.9.15.md +34 -36
- package/docs/migration/release-notes/0.9.16.md +110 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +93 -87
- package/docs/reference/configuration.md +128 -89
- package/docs/reference/data-and-telemetry.md +2 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +2 -58
- package/dist/indexer/index-db-contention.js +0 -56
- package/dist/indexer/index-rebuild-lock.js +0 -73
- package/dist/indexer/materialize-embeddings.js +0 -771
- package/dist/indexer/passes/dir-staleness.js +0 -161
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
|
@@ -2,90 +2,130 @@
|
|
|
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
|
-
* Write-path indexing:
|
|
5
|
+
* Write-path indexing: index exactly what a write just produced, inline, in
|
|
6
|
+
* the same call as the write. `remember` / `writeMarkdownAsset`, extract's
|
|
7
|
+
* session assets, `source clone`, and proposal acceptance all call this
|
|
8
|
+
* right after committing their file write so the asset is searchable
|
|
9
|
+
* immediately, without a background reindex.
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
11
|
+
* docs/plans/index-redesign-contract.md, module B2: a thin call to B1's
|
|
12
|
+
* `reconcilePaths` (the per-file version of the stat-walk reconcile — parses,
|
|
13
|
+
* upserts `entries`, derives units) followed by B4's `drainEmbeddingQueue`
|
|
14
|
+
* scoped to exactly the units that reconcile just produced. No lock probe, no
|
|
15
|
+
* rebuild detection, no background spawn — the redesign has no full-rebuild
|
|
16
|
+
* pipeline for a write path to defer to; two writers reconciling the same
|
|
17
|
+
* path converge on the same idempotent rows instead of contending for a lock.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* responsibilities.
|
|
19
|
+
* FAIL-OPEN at every step: any error (index.db absent, empty, unreadable,
|
|
20
|
+
* unparseable file) is reduced to a verbose-only warning and the write
|
|
21
|
+
* command succeeds untouched — the degraded outcome is the asset appearing
|
|
22
|
+
* after the next reconcile (an explicit `akm index`, or the schedule) rather
|
|
23
|
+
* than immediately.
|
|
21
24
|
*/
|
|
22
|
-
import fs from "node:fs";
|
|
23
25
|
import path from "node:path";
|
|
24
|
-
import { akmAdapter } from "../core/adapter/adapters/akm-adapter.js";
|
|
25
26
|
import { loadConfig } from "../core/config/config.js";
|
|
26
27
|
import { isDataDirUnreadableError } from "../core/errors.js";
|
|
27
|
-
import { probeLock } from "../core/file-lock.js";
|
|
28
28
|
import { isPathAbsent } from "../core/path-access.js";
|
|
29
|
-
import { getDbPath
|
|
30
|
-
import { formatLockHolderPid } from "../core/run-lock.js";
|
|
29
|
+
import { getDbPath } from "../core/paths.js";
|
|
31
30
|
import { warn, warnVerbose } from "../core/warn.js";
|
|
32
31
|
import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import { buildFileContext } from "./walk/file-context.js";
|
|
32
|
+
import { getEntryCount } from "../storage/repositories/index-entries-repository.js";
|
|
33
|
+
import { SQLITE_CHUNK_SIZE } from "../storage/repositories/index-sql.js";
|
|
34
|
+
import { drainEmbeddingQueue } from "./drain.js";
|
|
35
|
+
import { deriveInstallations } from "./installations.js";
|
|
36
|
+
import { reconcilePaths } from "./reconcile.js";
|
|
37
|
+
import { resolveSourceEntries } from "./search/search-source.js";
|
|
40
38
|
/**
|
|
41
|
-
* Busy-timeout (ms) for write-path index upserts.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Busy-timeout (ms) for write-path index upserts. Every index write in the
|
|
40
|
+
* redesign is a short immediate transaction relying on SQLite's own busy
|
|
41
|
+
* timeout to serialize concurrent writers (rule 5, docs/plans/index-redesign.md)
|
|
42
|
+
* rather than an application-level lock — this bounds how long one interactive
|
|
43
|
+
* write waits behind another process's short reconcile/drain transaction.
|
|
45
44
|
*/
|
|
46
45
|
export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
|
|
46
|
+
/**
|
|
47
|
+
* The distinct unit hashes reachable from an entry at one of `files`, via the
|
|
48
|
+
* `entry_units` mapping `reconcilePaths` just wrote — i.e. exactly the units
|
|
49
|
+
* this write's reconcile pass added or touched, the scope B4's drain should
|
|
50
|
+
* be asked to embed (module B2, docs/plans/index-redesign-contract.md:
|
|
51
|
+
* "Compute 'the units this write added' from the mapping:
|
|
52
|
+
* entries.file_path IN (paths) → entry_units → unit_hash"). A gone/unindexable
|
|
53
|
+
* path has no `entries` row any more and so contributes nothing here, which
|
|
54
|
+
* is correct — there is nothing left to embed for it.
|
|
55
|
+
*/
|
|
56
|
+
function unitHashesForFiles(db, files) {
|
|
57
|
+
const hashes = new Set();
|
|
58
|
+
for (let offset = 0; offset < files.length; offset += SQLITE_CHUNK_SIZE) {
|
|
59
|
+
const chunk = files.slice(offset, offset + SQLITE_CHUNK_SIZE);
|
|
60
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
61
|
+
const rows = db
|
|
62
|
+
.prepare(`SELECT DISTINCT eu.unit_hash AS unitHash
|
|
63
|
+
FROM entries e
|
|
64
|
+
JOIN entry_units eu ON eu.entry_id = e.id
|
|
65
|
+
WHERE e.file_path IN (${placeholders})`)
|
|
66
|
+
.all(...chunk);
|
|
67
|
+
for (const row of rows)
|
|
68
|
+
hashes.add(row.unitHash);
|
|
69
|
+
}
|
|
70
|
+
return [...hashes];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The bundle id a fresh write into `stashDir` should be recorded under, when
|
|
74
|
+
* the caller did not already know it (`options.bundleId`).
|
|
75
|
+
*
|
|
76
|
+
* `stashDir` is matched against the CONFIGURED sources first, the way every
|
|
77
|
+
* other write-path caller resolves its bundle id (`knowledge.ts`'s demoted-file
|
|
78
|
+
* loop: `resolveSourceEntries` + `deriveInstallations`, matched by resolved
|
|
79
|
+
* root). A stash that is ALSO a configured bundle (`akm bundle add <dir>
|
|
80
|
+
* --name x`) must produce that bundle's id here, not a fresh slug of the raw
|
|
81
|
+
* path — a mismatch leaves an undeletable duplicate row once a full `akm
|
|
82
|
+
* index` reconciles the same path under its real, configured id (issue found
|
|
83
|
+
* integrating write-path W1). Only a stash that is genuinely unconfigured (an
|
|
84
|
+
* ad hoc write target) falls back to deriving an id from the raw path alone,
|
|
85
|
+
* the way a fresh install would.
|
|
86
|
+
*/
|
|
87
|
+
function resolveWrittenAssetBundleId(stashDir) {
|
|
88
|
+
const resolvedStashDir = path.resolve(stashDir);
|
|
89
|
+
const sourceEntries = resolveSourceEntries();
|
|
90
|
+
const index = sourceEntries.findIndex((entry) => path.resolve(entry.path) === resolvedStashDir);
|
|
91
|
+
if (index !== -1)
|
|
92
|
+
return deriveInstallations(sourceEntries)[index]?.id;
|
|
93
|
+
return deriveInstallations([{ path: stashDir, writable: true }])[0]?.id;
|
|
94
|
+
}
|
|
47
95
|
/**
|
|
48
96
|
* Index the given just-written asset files into the existing local index.
|
|
49
97
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `akm index`
|
|
98
|
+
* Returns `true` when the index is left in the state the caller expects —
|
|
99
|
+
* either genuinely updated, or one of the deliberate skips below — and
|
|
100
|
+
* `false` only for a real failure, which callers that gate their own success
|
|
101
|
+
* on this boolean (`acceptProposal`, `source clone`) must treat as "the
|
|
102
|
+
* write stands, but tell the operator the index needs a manual `akm index`".
|
|
55
103
|
*
|
|
56
|
-
*
|
|
57
|
-
* first read (`ensureIndex`) or
|
|
58
|
-
*
|
|
104
|
+
* Deliberate `true` skips:
|
|
105
|
+
* - An absent index: bootstrap belongs to the first read (`ensureIndex`) or
|
|
106
|
+
* an explicit `akm index`, not this fast path.
|
|
107
|
+
* - An index with zero entries: same reasoning — nothing has bootstrapped
|
|
108
|
+
* it yet.
|
|
109
|
+
* - Every given path living under a dot-segment directory (e.g. `.meta/`):
|
|
110
|
+
* the full reconcile never descends into those either.
|
|
59
111
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
112
|
+
* `false` on failure:
|
|
113
|
+
* - An index directory/file that exists but cannot be READ (not merely
|
|
114
|
+
* absent) is the one failure the next reconcile will not heal on its own
|
|
115
|
+
* (#791) — this warns audibly, not verbose-only.
|
|
116
|
+
* - Any other error during `reconcilePaths` (unparseable file, a locked
|
|
117
|
+
* database past the busy timeout, …) warns verbose-only; the write itself
|
|
118
|
+
* already succeeded and the asset appears after the next reconcile.
|
|
119
|
+
*
|
|
120
|
+
* The embedding drain that follows a successful reconcile is best-effort: a
|
|
121
|
+
* failure there (a provider outage, a tripped circuit breaker) does NOT turn
|
|
122
|
+
* this call into a `false` — the asset is already lexically searchable, and
|
|
123
|
+
* the embedding queue is durable (module B4): any later drain, including the
|
|
124
|
+
* next write, computes the same "no vector yet" query and picks it up.
|
|
66
125
|
*/
|
|
67
126
|
export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
68
127
|
try {
|
|
69
128
|
return await (async () => {
|
|
70
|
-
// #956: a live `akm index` rebuild holds index.db under one long
|
|
71
|
-
// transaction (persistDirRecords), which this fast path's own 5s
|
|
72
|
-
// busy_timeout would just contend with pointlessly. Interactive
|
|
73
|
-
// commands (remember, import, extract session assets) must never wait
|
|
74
|
-
// on it — skip the inline upsert/embedding entirely; the write itself
|
|
75
|
-
// (file + commit) has already succeeded by the time this runs, and the
|
|
76
|
-
// rebuild in progress will pick up the change on its own.
|
|
77
|
-
const rebuildProbe = probeLock(getIndexRebuildLockPath());
|
|
78
|
-
if (rebuildProbe.state === "held") {
|
|
79
|
-
// #956: name the launcher pid alongside the holder pid when known —
|
|
80
|
-
// every process listing and task log shows the launcher's pid, not
|
|
81
|
-
// the bun/node child's.
|
|
82
|
-
const holderLabel = formatLockHolderPid({
|
|
83
|
-
pid: rebuildProbe.holderPid,
|
|
84
|
-
launcherPid: rebuildProbe.launcherPid ?? null,
|
|
85
|
-
});
|
|
86
|
-
warn(`index rebuild in progress (pid ${holderLabel}); the next index pass will index ${filePaths.join(", ")}`);
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
129
|
const dbPath = getDbPath();
|
|
90
130
|
// `true` here means "the index is in the state the caller expects" — and
|
|
91
131
|
// `acceptProposal` advances its journal to `index-finalized` on the
|
|
@@ -94,132 +134,54 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
|
94
134
|
// `openExistingDatabase` and surfaces as the honest `false` (#791).
|
|
95
135
|
if (isPathAbsent(dbPath))
|
|
96
136
|
return true;
|
|
97
|
-
// The full
|
|
98
|
-
// — mirror that dot-segment skip here so this fast path
|
|
99
|
-
// what a full
|
|
100
|
-
// infra abstention is the adapter's job now (see the `akmAdapter` note
|
|
101
|
-
// below), not a path pre-filter.
|
|
137
|
+
// The full reconcile never descends into dot-directories (for example
|
|
138
|
+
// `.meta/`) — mirror that dot-segment skip here so this fast path
|
|
139
|
+
// indexes exactly what a full reconcile would.
|
|
102
140
|
const files = filePaths.filter((f) => {
|
|
103
141
|
const rel = path.relative(stashDir, f);
|
|
104
142
|
return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
|
|
105
143
|
});
|
|
106
144
|
if (files.length === 0)
|
|
107
145
|
return true;
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{ path: stashDir, writable: true, ...(options.bundleId ? { registryId: options.bundleId } : {}) },
|
|
114
|
-
])[0]?.components[0];
|
|
115
|
-
if (!component)
|
|
146
|
+
// An explicit bundleId wins outright (it IS the resulting installation
|
|
147
|
+
// id); otherwise resolve it config-aware, matching a configured source
|
|
148
|
+
// before ever falling back to deriving one from the raw path.
|
|
149
|
+
const bundleId = options.bundleId ?? resolveWrittenAssetBundleId(stashDir);
|
|
150
|
+
if (!bundleId)
|
|
116
151
|
throw new Error(`Could not derive bundle provenance for ${stashDir}`);
|
|
117
|
-
const pairs = [];
|
|
118
|
-
const unindexable = new Set();
|
|
119
|
-
const rejectedConceptIds = new Set();
|
|
120
|
-
for (const file of files) {
|
|
121
|
-
if (!fs.existsSync(file)) {
|
|
122
|
-
let authoredDanglingSymlink = false;
|
|
123
|
-
try {
|
|
124
|
-
authoredDanglingSymlink = fs.lstatSync(file).isSymbolicLink();
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// A genuinely absent path has no source identity to preflight.
|
|
128
|
-
}
|
|
129
|
-
if (!authoredDanglingSymlink) {
|
|
130
|
-
unindexable.add(file);
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const ctx = buildFileContext(stashDir, file);
|
|
135
|
-
// Hardcoded `akmAdapter` on purpose (owner ruling 2026-07-21): this
|
|
136
|
-
// write-path fast path only ever runs for assets a first-class akm
|
|
137
|
-
// mutation command just wrote into a managed akm source, so the akm
|
|
138
|
-
// adapter is always the right recognizer here — no per-component
|
|
139
|
-
// dispatch needed.
|
|
140
|
-
const drained = drainDirDocuments(akmAdapter, component, [ctx]);
|
|
141
|
-
for (const rejectedPath of drained.rejectedPaths)
|
|
142
|
-
unindexable.add(rejectedPath);
|
|
143
|
-
for (const conceptId of drained.rejectedConceptIds)
|
|
144
|
-
rejectedConceptIds.add(conceptId);
|
|
145
|
-
const entry = drained.entries[0];
|
|
146
|
-
// A broken workflow drains to zero entries and is treated as
|
|
147
|
-
// unindexable; valid peer sources have already compiled to source IR.
|
|
148
|
-
const conceptId = drained.conceptIdByFile.get(ctx.absPath);
|
|
149
|
-
if (entry && conceptId)
|
|
150
|
-
pairs.push({ file, entry, conceptId, contentHash: drained.hashByFile.get(ctx.absPath) });
|
|
151
|
-
else
|
|
152
|
-
unindexable.add(file);
|
|
153
|
-
}
|
|
154
152
|
const db = openExistingDatabase(dbPath);
|
|
155
153
|
try {
|
|
156
154
|
db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
|
|
155
|
+
// An absent or empty index is skipped on purpose — bootstrap belongs
|
|
156
|
+
// to the first read (`ensureIndex`) or an explicit `akm index`.
|
|
157
157
|
if (getEntryCount(db) === 0)
|
|
158
158
|
return true;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.all(file, component.id, component.adapter);
|
|
170
|
-
for (const row of rows)
|
|
171
|
-
unindexableEntryIds.add(row.id);
|
|
172
|
-
}
|
|
173
|
-
for (const conceptId of rejectedConceptIds) {
|
|
174
|
-
const itemRef = `${component.id}//${conceptId}`;
|
|
175
|
-
const rows = db
|
|
176
|
-
.prepare(`SELECT id FROM entries
|
|
177
|
-
WHERE bundle_id = ? AND adapter_id = ?
|
|
178
|
-
AND type = 'workflow'
|
|
179
|
-
AND (concept_id = ? OR item_ref = ?)`)
|
|
180
|
-
.all(component.id, component.adapter, conceptId, itemRef);
|
|
181
|
-
for (const row of rows)
|
|
182
|
-
unindexableEntryIds.add(row.id);
|
|
183
|
-
}
|
|
184
|
-
deleteEntriesByIds(db, [...unindexableEntryIds]);
|
|
185
|
-
mutated ||= unindexableEntryIds.size > 0;
|
|
186
|
-
for (const { file, entry, conceptId, contentHash } of pairs) {
|
|
187
|
-
let entryWithSize = entry;
|
|
188
|
-
try {
|
|
189
|
-
entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
|
|
190
|
-
if (hasMarkdownFragmentContent(entry)) {
|
|
191
|
-
setMarkdownFragmentContent(entryWithSize, getMarkdownFragmentContent(entry));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// stat raced a delete — index without the size, like the full walk does.
|
|
196
|
-
}
|
|
197
|
-
// Real provenance (F4a M-core-2 item 5): populate item_ref/content_hash
|
|
198
|
-
// via the SAME derivation the full-index writer uses, so a write-path
|
|
199
|
-
// row is never a NULL-item_ref straggler.
|
|
200
|
-
const provenance = deriveEntryProvenance({ bundleId: component.id, componentId: component.id, adapterId: component.adapter }, entry.type, entry.name, conceptId);
|
|
201
|
-
// A materialized file has one current owner. If a targeted write is
|
|
202
|
-
// the ownership handoff (for example an explicitly named bundle
|
|
203
|
-
// replacing an earlier path-derived bootstrap identity), remove the
|
|
204
|
-
// superseded physical-path row before publishing the canonical ref.
|
|
205
|
-
const supersededIds = db
|
|
206
|
-
.prepare("SELECT id FROM entries WHERE file_path = ? AND item_ref <> ?")
|
|
207
|
-
.all(file, provenance.itemRef);
|
|
208
|
-
deleteEntriesByIds(db, supersededIds.map((row) => row.id));
|
|
209
|
-
targetEntryIds.add(upsertEntry(db, file, entryWithSize, buildSearchText(entry), provenance, contentHash));
|
|
210
|
-
mutated = true;
|
|
211
|
-
}
|
|
212
|
-
})();
|
|
213
|
-
if (mutated) {
|
|
159
|
+
// Pass `stashDir` through as `reconcilePaths`'s `opts.root` rather
|
|
160
|
+
// than relying on its config-based bundleId->root lookup: this
|
|
161
|
+
// caller already knows the exact root its write happened under, and
|
|
162
|
+
// a bundleId that is not (yet, or ever) a `bundles.<key>` config
|
|
163
|
+
// entry — an ad hoc write target, e.g. a proposal's named target —
|
|
164
|
+
// can resolve to the WRONG root through that lookup, producing a
|
|
165
|
+
// corrupt conceptId and a duplicate `entries` row instead of a clean
|
|
166
|
+
// no-op (caught integrating this against tests/integration/proposal/proposals.test.ts).
|
|
167
|
+
await reconcilePaths(db, files, bundleId, { root: stashDir });
|
|
168
|
+
try {
|
|
214
169
|
const config = loadConfig();
|
|
215
|
-
|
|
216
|
-
|
|
170
|
+
const onlyHashes = unitHashesForFiles(db, files);
|
|
171
|
+
await drainEmbeddingQueue(db, config, { onlyHashes });
|
|
172
|
+
}
|
|
173
|
+
catch (drainError) {
|
|
174
|
+
// Best-effort: the write is already lexically searchable via
|
|
175
|
+
// reconcilePaths above. The embedding queue is durable, so a
|
|
176
|
+
// failed drain here is not a failure of this call — see the
|
|
177
|
+
// return-contract note above.
|
|
178
|
+
warnVerbose("Write-path embedding drain skipped (vectors appear after the next drain):", drainError instanceof Error ? drainError.message : String(drainError));
|
|
217
179
|
}
|
|
180
|
+
return true;
|
|
218
181
|
}
|
|
219
182
|
finally {
|
|
220
183
|
closeDatabase(db);
|
|
221
184
|
}
|
|
222
|
-
return true;
|
|
223
185
|
})();
|
|
224
186
|
}
|
|
225
187
|
catch (error) {
|