akm-cli 0.9.16-alpha.1 → 0.9.16-alpha.2

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 (144) hide show
  1. package/CHANGELOG.md +40 -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/shapes/passthrough.js +2 -1
  86. package/dist/output/text/command-format.js +13 -19
  87. package/dist/output/text/helpers.js +1 -1
  88. package/dist/output/text/index.js +2 -5
  89. package/dist/registry/resolve.js +37 -10
  90. package/dist/scripts/akm-migrate-node.js +15197 -11351
  91. package/dist/scripts/akm-migrate.js +15514 -11668
  92. package/dist/setup/semantic-assets.js +2 -2
  93. package/dist/setup/setup.js +3 -3
  94. package/dist/setup/steps/connection.js +2 -3
  95. package/dist/setup/steps/tasks.js +29 -36
  96. package/dist/sources/providers/git-install.js +17 -11
  97. package/dist/sources/providers/git-provider.js +12 -5
  98. package/dist/sources/providers/git-stash.js +38 -16
  99. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  100. package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
  101. package/dist/storage/repositories/index-connection.js +3 -1
  102. package/dist/storage/repositories/index-entries-repository.js +68 -77
  103. package/dist/storage/repositories/index-entry-schema.js +25 -16
  104. package/dist/storage/repositories/index-fts-repository.js +263 -29
  105. package/dist/storage/repositories/index-meta-repository.js +29 -0
  106. package/dist/storage/repositories/index-schema.js +122 -115
  107. package/dist/storage/repositories/index-utility-repository.js +1 -1
  108. package/dist/storage/repositories/index-vec-repository.js +435 -22
  109. package/dist/tasks/activation-config.js +90 -0
  110. package/dist/tasks/backends/cron.js +9 -0
  111. package/dist/tasks/backends/launchd.js +1 -0
  112. package/dist/tasks/backends/schtasks.js +2 -0
  113. package/dist/tasks/embedded.js +4 -5
  114. package/dist/tasks/scheduler-binding.js +2 -2
  115. package/dist/tasks/scheduler-sync-preview.js +8 -1
  116. package/dist/tasks/scheduler-sync.js +19 -10
  117. package/dist/tasks/source/parse-task-source.js +10 -113
  118. package/dist/tasks/source/project-v4.js +2 -2
  119. package/dist/tasks/source/task-source-v4.js +4 -12
  120. package/dist/tasks/source/task-to-v3.js +4 -12
  121. package/dist/tasks/source/task-to-v4.js +40 -7
  122. package/docs/migration/README.md +1 -0
  123. package/docs/migration/release-notes/0.9.15.md +36 -34
  124. package/docs/migration/release-notes/0.9.16.md +60 -98
  125. package/docs/migration/release-notes/README.md +0 -5
  126. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  127. package/docs/reference/cli.md +124 -122
  128. package/docs/reference/configuration.md +137 -133
  129. package/docs/reference/data-and-telemetry.md +1 -2
  130. package/docs/reference/tasks.md +34 -29
  131. package/package.json +1 -1
  132. package/schemas/akm-config.json +170 -6
  133. package/schemas/akm-task.json +1 -2
  134. package/dist/commands/sources/index-status.js +0 -99
  135. package/dist/core/hash.js +0 -18
  136. package/dist/indexer/drain.js +0 -306
  137. package/dist/indexer/embedding-identity.js +0 -20
  138. package/dist/indexer/enrich.js +0 -260
  139. package/dist/indexer/reconcile.js +0 -890
  140. package/dist/indexer/scan/parse-file.js +0 -66
  141. package/dist/indexer/units/unit.js +0 -159
  142. package/dist/llm/embedders/provider-limits.js +0 -288
  143. package/dist/storage/repositories/files-repository.js +0 -181
  144. package/dist/storage/repositories/units-repository.js +0 -510
@@ -2,130 +2,90 @@
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: 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.
5
+ * Write-path indexing: targeted single-file index updates for asset writers.
10
6
  *
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.
7
+ * The index is maintained eagerly by every first-class mutation command
8
+ * (`source add`, `wiki`, `workflow`, `setup` all run `akmIndex()` after
9
+ * writing). The memory write paths — `akm remember` / `writeMarkdownAsset`
10
+ * and extract's session assets — historically did not, which is why reads
11
+ * used to compensate with stale-triggered background reindexes (the
12
+ * lock-contention footgun removed alongside this module's introduction, per
13
+ * the 2026-07 read-path reindex-contention findings §7).
18
14
  *
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.
15
+ * This is NOT a general reindex. It upserts exactly the files the caller just
16
+ * wrote: frontmatter/metadata via the shared matcher pipeline, the canonical
17
+ * row with its transactionally owned FTS projection, and vectors for changed
18
+ * entry IDs when semantic search is enabled. Index-time LLM passes, graph
19
+ * extraction, `builtAt`, and the per-dir walk cache remain full-index
20
+ * responsibilities.
24
21
  */
22
+ import fs from "node:fs";
25
23
  import path from "node:path";
24
+ import { akmAdapter } from "../core/adapter/adapters/akm-adapter.js";
26
25
  import { loadConfig } from "../core/config/config.js";
27
26
  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 } from "../core/paths.js";
29
+ import { getDbPath, getIndexRebuildLockPath } from "../core/paths.js";
30
+ import { formatLockHolderPid } from "../core/run-lock.js";
30
31
  import { warn, warnVerbose } from "../core/warn.js";
31
32
  import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.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";
33
+ import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
34
+ import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
35
+ import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
36
+ import { getMarkdownFragmentContent, hasMarkdownFragmentContent, setMarkdownFragmentContent, } from "./passes/metadata.js";
37
+ import { drainDirDocuments } from "./scan/drain-dir.js";
38
+ import { buildSearchText } from "./search/search-fields.js";
39
+ import { buildFileContext } from "./walk/file-context.js";
38
40
  /**
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.
41
+ * Busy-timeout (ms) for write-path index upserts. A real write — unlike the
42
+ * 250ms telemetry inserts — but it must not hang `akm remember` for the full
43
+ * default 30s behind a running full reindex. When it times out, the upsert is
44
+ * skipped and the asset becomes searchable after that reindex instead.
44
45
  */
45
46
  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
- }
95
47
  /**
96
48
  * Index the given just-written asset files into the existing local index.
97
49
  *
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`".
50
+ * FAIL-OPEN at every step: any error (index.db absent, empty, locked past the
51
+ * busy timeout, unparseable file) is reduced to a verbose-only warning and the
52
+ * write command succeeds untouched. The degraded outcome is exactly the
53
+ * pre-write-path-indexing behavior: the asset appears after the next full
54
+ * `akm index` / improve-cron run.
103
55
  *
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.
56
+ * An absent or empty index is skipped on purpose — bootstrap belongs to the
57
+ * first read (`ensureIndex`) or an explicit `akm index`, which also cover
58
+ * embeddings and the other passes this fast path skips.
111
59
  *
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.
60
+ * A live rebuild holding the rebuild lock is the SAME kind of skip, not a
61
+ * failure (#956 fix): a live `akm index` run owns bringing the index to the
62
+ * expected state on its own, so this call returns `true` on that skip
63
+ * exactly like the absent-index and empty-index cases above. Callers that
64
+ * gate their own success on this boolean (`acceptProposal`, `source clone`)
65
+ * must never fail or warn just because a concurrent rebuild is in progress.
125
66
  */
126
67
  export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
127
68
  try {
128
69
  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
+ }
129
89
  const dbPath = getDbPath();
130
90
  // `true` here means "the index is in the state the caller expects" — and
131
91
  // `acceptProposal` advances its journal to `index-finalized` on the
@@ -134,54 +94,132 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
134
94
  // `openExistingDatabase` and surfaces as the honest `false` (#791).
135
95
  if (isPathAbsent(dbPath))
136
96
  return true;
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.
97
+ // The full walk never descends into dot-directories (for example `.meta/`)
98
+ // — mirror that dot-segment skip here so this fast path indexes exactly
99
+ // what a full run would. Sensitive/
100
+ // infra abstention is the adapter's job now (see the `akmAdapter` note
101
+ // below), not a path pre-filter.
140
102
  const files = filePaths.filter((f) => {
141
103
  const rel = path.relative(stashDir, f);
142
104
  return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
143
105
  });
144
106
  if (files.length === 0)
145
107
  return true;
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)
108
+ // Generate metadata BEFORE opening the DB so the write window stays
109
+ // short. One drain call per file keeps the entry↔path pairing exact and
110
+ // reuses the full-index recognize engine (F4a M-core-2 item 5): broken
111
+ // workflows drop, valid workflow docs are cached for the side-table upsert.
112
+ const component = deriveInstallations([
113
+ { path: stashDir, writable: true, ...(options.bundleId ? { registryId: options.bundleId } : {}) },
114
+ ])[0]?.components[0];
115
+ if (!component)
151
116
  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
+ }
152
154
  const db = openExistingDatabase(dbPath);
153
155
  try {
154
156
  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
- // 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 {
159
+ const targetEntryIds = new Set();
160
+ let mutated = false;
161
+ db.transaction(() => {
162
+ const unindexableEntryIds = new Set();
163
+ for (const file of unindexable) {
164
+ const rows = db
165
+ .prepare(`SELECT id FROM entries
166
+ WHERE file_path = ?
167
+ AND bundle_id = ?
168
+ AND adapter_id = ?`)
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) {
169
214
  const config = loadConfig();
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));
215
+ await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
216
+ publishTargetedEmbeddingMeta(db, config);
179
217
  }
180
- return true;
181
218
  }
182
219
  finally {
183
220
  closeDatabase(db);
184
221
  }
222
+ return true;
185
223
  })();
186
224
  }
187
225
  catch (error) {