akm-cli 0.9.1 → 0.9.2-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.
- package/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
package/dist/indexer/indexer.js
CHANGED
|
@@ -7,32 +7,47 @@ import { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
|
7
7
|
import { adapterForId } from "../core/adapter/registry.js";
|
|
8
8
|
import { isHttpUrl, toErrorMessage } from "../core/common.js";
|
|
9
9
|
import { concurrentMap } from "../core/concurrent.js";
|
|
10
|
+
import { ConfigError } from "../core/errors.js";
|
|
10
11
|
import { isLoopbackEndpoint } from "../core/loopback.js";
|
|
11
12
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
12
13
|
import { getDbPath } from "../core/paths.js";
|
|
13
14
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
14
15
|
import { withStateDb } from "../core/state-db.js";
|
|
15
16
|
import { isVerbose, warn, warnVerbose } from "../core/warn.js";
|
|
16
|
-
import {
|
|
17
|
+
import { disposeLoweredExecutionDispatchLease, } from "../integrations/agent/execution-lowering.js";
|
|
18
|
+
import { isLlmFeatureEnabled } from "../llm/feature-gate.js";
|
|
19
|
+
import { resolveIndexPassExecution } from "../llm/index-passes.js";
|
|
20
|
+
import { preflightStructuredLlmRunner } from "../llm/structured-call.js";
|
|
17
21
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
18
|
-
import { closeDatabase, openExistingDatabase, openIndexDatabase } from "../storage/repositories/index-connection.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { deleteIndexDirState, deleteIndexDirStatesByStashDir, deleteMeta, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
22
|
+
import { closeDatabase, openExistingDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
|
|
23
|
+
import { deleteAllEntries, deleteEntriesByBundle, deleteEntriesByDirAndBundle, deleteEntriesByDirExceptRefs, deleteEntriesByIds, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedBundleIdsByDir, getIndexedDirPathsByBundleId, relinkUsageEvents, upsertEntry, } from "../storage/repositories/index-entries-repository.js";
|
|
24
|
+
import { clearStaleCacheEntries, computeBodyHash, getLlmCacheEntry, } from "../storage/repositories/index-llm-cache-repository.js";
|
|
25
|
+
import { deleteIndexDirState, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
23
26
|
import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
27
|
+
import { getEmbeddingCount, isVecAvailable, isVecFastPathReady, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
28
|
+
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
26
29
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
27
30
|
import { withIndexWriterLease } from "./index-writer-lock.js";
|
|
28
31
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
32
|
+
import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
|
|
33
|
+
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
29
34
|
import { canUseIncrementalSkip, computeDirFingerprint, getCachedZeroRowDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
|
|
30
35
|
import { isEnrichmentComplete, isWorkflowSkipWarning } from "./passes/metadata.js";
|
|
31
36
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
32
37
|
import { buildSearchText } from "./search/search-fields.js";
|
|
33
|
-
import {
|
|
38
|
+
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./search/semantic-status.js";
|
|
34
39
|
import { purgeOldUsageEvents } from "./usage/usage-events.js";
|
|
35
40
|
import { walkStashFlatWithStatus } from "./walk/walker.js";
|
|
41
|
+
function collectLoweringNotices(target, notices) {
|
|
42
|
+
const keys = new Set(target.map((notice) => JSON.stringify(notice)));
|
|
43
|
+
for (const notice of notices) {
|
|
44
|
+
const key = JSON.stringify(notice);
|
|
45
|
+
if (keys.has(key))
|
|
46
|
+
continue;
|
|
47
|
+
keys.add(key);
|
|
48
|
+
target.push(notice);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
36
51
|
function throwIfAborted(signal) {
|
|
37
52
|
if (signal?.aborted) {
|
|
38
53
|
throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
|
|
@@ -56,51 +71,67 @@ export function getDefaultLlmConcurrency(llmConfig) {
|
|
|
56
71
|
// auto-derived 1/2 is what runs (see docs/architecture/internals/indexing.md).
|
|
57
72
|
return 2;
|
|
58
73
|
}
|
|
59
|
-
|
|
74
|
+
function sourceOwners(sources) {
|
|
75
|
+
const installations = deriveInstallations([...sources]);
|
|
76
|
+
return sources.flatMap((source, index) => {
|
|
77
|
+
const installation = installations[index];
|
|
78
|
+
return installation ? [{ bundleId: installation.id, sourceRoot: path.resolve(source.path) }] : [];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function parseStoredSourceOwners(raw) {
|
|
82
|
+
if (!raw)
|
|
83
|
+
return [];
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
if (!Array.isArray(parsed) ||
|
|
87
|
+
parsed.some((owner) => typeof owner !== "object" ||
|
|
88
|
+
owner === null ||
|
|
89
|
+
typeof owner.bundleId !== "string" ||
|
|
90
|
+
typeof owner.sourceRoot !== "string")) {
|
|
91
|
+
warn("index_meta sourceOwners value is invalid — treating as empty");
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return parsed.map((owner) => {
|
|
95
|
+
const stored = owner;
|
|
96
|
+
return { bundleId: stored.bundleId, sourceRoot: path.resolve(stored.sourceRoot) };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
warn("index_meta sourceOwners value is corrupt JSON — treating as empty");
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
60
104
|
/**
|
|
61
105
|
* Source cache phase: ensure git stash caches are up to date and purge orphaned
|
|
62
106
|
* entries from removed sources (incremental only).
|
|
63
107
|
*/
|
|
64
108
|
async function runSourceCachePhase(ctx) {
|
|
65
|
-
const { db,
|
|
109
|
+
const { db, isIncremental, full, sources } = ctx;
|
|
66
110
|
if (isIncremental && !full) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
warn("index_meta stashDirs value is not an array — treating as empty");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
warn("index_meta stashDirs value is corrupt JSON — treating as empty");
|
|
83
|
-
}
|
|
84
|
-
const currentSet = new Set(sourceDirs);
|
|
85
|
-
for (const dir of prevStashDirs) {
|
|
86
|
-
if (!currentSet.has(dir)) {
|
|
87
|
-
ctx.hadRemovedSources = true;
|
|
88
|
-
ctx.removedSourceDirs.push(dir);
|
|
89
|
-
}
|
|
111
|
+
const currentByBundle = new Map(sourceOwners(sources).map((owner) => [owner.bundleId, owner]));
|
|
112
|
+
for (const previous of parseStoredSourceOwners(getMeta(db, "sourceOwners"))) {
|
|
113
|
+
const current = currentByBundle.get(previous.bundleId);
|
|
114
|
+
if (!current || current.sourceRoot !== previous.sourceRoot) {
|
|
115
|
+
ctx.hadRemovedSources = true;
|
|
116
|
+
ctx.removedSources.push({
|
|
117
|
+
...previous,
|
|
118
|
+
removeBundleEntries: current === undefined,
|
|
119
|
+
});
|
|
90
120
|
}
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
// Source caches are hydrated before akmIndex() calls this phase; nothing
|
|
94
124
|
// further to do here. The flag is exposed on ctx for runWalkPhase().
|
|
95
|
-
void config;
|
|
96
125
|
}
|
|
97
126
|
function applyRemovedSources(ctx) {
|
|
98
127
|
if (!ctx.scanComplete)
|
|
99
128
|
return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
129
|
+
const currentRoots = new Set(sourceOwners(ctx.sources).map((owner) => owner.sourceRoot));
|
|
130
|
+
for (const removed of ctx.removedSources) {
|
|
131
|
+
if (removed.removeBundleEntries)
|
|
132
|
+
deleteEntriesByBundle(ctx.db, removed.bundleId);
|
|
133
|
+
if (!currentRoots.has(removed.sourceRoot))
|
|
134
|
+
deleteStoredGraph(ctx.db, removed.sourceRoot);
|
|
104
135
|
}
|
|
105
136
|
}
|
|
106
137
|
/**
|
|
@@ -115,7 +146,14 @@ async function runWalkPhase(ctx) {
|
|
|
115
146
|
throwIfAborted(signal);
|
|
116
147
|
ctx.timing.tWalkStart = Date.now();
|
|
117
148
|
const doFullDelete = full || !isIncremental;
|
|
118
|
-
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings, complete } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress, !clean)
|
|
149
|
+
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings, complete } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress, !clean, async (dirRecords, ownersByRoot) => {
|
|
150
|
+
const runner = ctx.enrichmentExecution.runner;
|
|
151
|
+
if (runner &&
|
|
152
|
+
isLlmFeatureEnabled(config, "metadata_enhance") &&
|
|
153
|
+
dirRecordsNeedMetadataDispatch(db, dirRecords, ownersByRoot)) {
|
|
154
|
+
ctx.enrichmentLease = await preflightStructuredLlmRunner(runner);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
119
157
|
ctx.scannedDirs = scannedDirs;
|
|
120
158
|
ctx.skippedDirs = skippedDirs;
|
|
121
159
|
ctx.generatedCount = generatedCount;
|
|
@@ -142,10 +180,10 @@ async function runWalkPhase(ctx) {
|
|
|
142
180
|
ctx.timing.tWalkEnd = Date.now();
|
|
143
181
|
throwIfAborted(signal);
|
|
144
182
|
// LLM enrichment for directories that need it
|
|
145
|
-
await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal);
|
|
183
|
+
await enhanceDirsWithLlm(db, config, ctx.enrichmentExecution, dirsNeedingLlm, onProgress, signal, (notices) => collectLoweringNotices(ctx.loweringNotices, notices), ctx.enrichmentLease);
|
|
146
184
|
onProgress({
|
|
147
185
|
phase: "llm",
|
|
148
|
-
message:
|
|
186
|
+
message: ctx.enrichmentExecution.runner
|
|
149
187
|
? `LLM enhancement reviewed ${dirsNeedingLlm.length} ${dirsNeedingLlm.length === 1 ? "directory" : "directories"}.`
|
|
150
188
|
: "LLM enhancement disabled.",
|
|
151
189
|
});
|
|
@@ -167,35 +205,41 @@ async function runEmbeddingPhase(ctx) {
|
|
|
167
205
|
ctx.timing.tEmbedEnd = Date.now();
|
|
168
206
|
}
|
|
169
207
|
/**
|
|
170
|
-
* Finalize phase:
|
|
171
|
-
*
|
|
208
|
+
* Finalize phase: confirm transactionally materialized FTS state, re-link
|
|
209
|
+
* usage events, recompute utility scores, update index metadata, and emit the
|
|
210
|
+
* verify event.
|
|
172
211
|
*/
|
|
173
|
-
async function runFinalizePhase(ctx) {
|
|
212
|
+
async function runFinalizePhase(ctx, deferredUpdateTransaction) {
|
|
174
213
|
const { db, config, sources, sourceDirs, isIncremental, stashDir, signal, onProgress } = ctx;
|
|
175
214
|
ctx.timing.tFinalizeStart = Date.now();
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// since the last rebuild are re-indexed.
|
|
179
|
-
rebuildFts(db, { incremental: isIncremental });
|
|
215
|
+
// `upsertEntry` and every canonical delete own their FTS projection. This is
|
|
216
|
+
// an observation point, not a second materialization pass.
|
|
180
217
|
onProgress({
|
|
181
218
|
phase: "fts",
|
|
182
|
-
message:
|
|
219
|
+
message: "Full-text search index is current.",
|
|
183
220
|
});
|
|
184
221
|
ctx.timing.tFtsEnd = Date.now();
|
|
185
|
-
// Re-link
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
// Chunk-8 WI-8.3: usage_events lives in state.db now (index.db no longer holds
|
|
191
|
-
// it), so these cross-DB passes take both handles — entries in `db` (index.db),
|
|
192
|
-
// usage_events in the loaned state.db.
|
|
193
|
-
withStateDb((stateDb) => {
|
|
222
|
+
// Re-link state.db usage events to the regenerated index and recompute the
|
|
223
|
+
// derived utility cache. Stored refs already use the current item-ref grammar,
|
|
224
|
+
// so this idempotent pass only restores derived entry ids.
|
|
225
|
+
const mutateState = (stateDb, stateSchema) => {
|
|
194
226
|
onProgress({ phase: "finalize", message: "Relinking usage events." });
|
|
195
|
-
relinkUsageEvents(db, stateDb, { sources, defaultStashDir: stashDir });
|
|
227
|
+
relinkUsageEvents(db, stateDb, { sources, defaultStashDir: stashDir, stateSchema });
|
|
196
228
|
onProgress({ phase: "finalize", message: "Recomputing utility scores." });
|
|
197
|
-
recomputeUtilityScores(db, stateDb);
|
|
198
|
-
}
|
|
229
|
+
recomputeUtilityScores(db, stateDb, { stateSchema });
|
|
230
|
+
};
|
|
231
|
+
if (deferredUpdateTransaction) {
|
|
232
|
+
if (deferredUpdateTransaction.db !== db || !db.inTransaction) {
|
|
233
|
+
throw new Error("Source update index finalization requires its borrowed unified transaction.");
|
|
234
|
+
}
|
|
235
|
+
// state.db is ATTACHed to this same index connection before the outer
|
|
236
|
+
// BEGIN IMMEDIATE. Index and state mutations therefore share one SQLite
|
|
237
|
+
// commit/rollback decision rather than an unsafe two-connection ordering.
|
|
238
|
+
mutateState(db, deferredUpdateTransaction.stateSchema);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
withStateDb(mutateState);
|
|
242
|
+
}
|
|
199
243
|
// Purge LLM cache entries for assets that no longer exist in the index.
|
|
200
244
|
try {
|
|
201
245
|
onProgress({ phase: "finalize", message: "Clearing stale LLM cache entries." });
|
|
@@ -213,22 +257,19 @@ async function runFinalizePhase(ctx) {
|
|
|
213
257
|
setMeta(db, "builtAt", new Date().toISOString());
|
|
214
258
|
setMeta(db, "stashDir", stashDir);
|
|
215
259
|
setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
|
|
260
|
+
setMeta(db, "sourceOwners", JSON.stringify(sourceOwners(sources)));
|
|
216
261
|
}
|
|
217
262
|
setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
|
|
218
|
-
// Stash-organization conventions (SPEC-8): track which `index.indexBodyOpening`
|
|
219
|
-
// state the index was built with, and warn while the flag diverges from it.
|
|
220
|
-
const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, (ctx.full || !isIncremental) && ctx.scanComplete);
|
|
221
|
-
if (bodyOpeningWarning)
|
|
222
|
-
warn(bodyOpeningWarning);
|
|
223
263
|
warnIfVecMissing(db);
|
|
224
264
|
const totalEntries = getEntryCount(db);
|
|
225
265
|
const semanticEntryCount = getEmbeddableEntryCount(db);
|
|
226
266
|
onProgress({ phase: "finalize", message: "Verifying semantic search state." });
|
|
227
267
|
const verification = verifyIndexState(db, config, semanticEntryCount, embeddingResult);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
268
|
+
const persistSemanticStatus = () => {
|
|
269
|
+
if (config.semanticSearchMode === "off") {
|
|
270
|
+
clearSemanticStatus();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
232
273
|
writeSemanticStatus({
|
|
233
274
|
status: verification.semanticStatus === "disabled" ? "pending" : verification.semanticStatus,
|
|
234
275
|
...(embeddingResult.reason ? { reason: embeddingResult.reason } : {}),
|
|
@@ -238,6 +279,12 @@ async function runFinalizePhase(ctx) {
|
|
|
238
279
|
entryCount: verification.entryCount,
|
|
239
280
|
embeddingCount: verification.embeddingCount,
|
|
240
281
|
});
|
|
282
|
+
};
|
|
283
|
+
if (deferredUpdateTransaction) {
|
|
284
|
+
deferredUpdateTransaction.afterCommit = persistSemanticStatus;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
persistSemanticStatus();
|
|
241
288
|
}
|
|
242
289
|
onProgress({ phase: "verify", message: verification.message });
|
|
243
290
|
// Store verification result and totalEntries on ctx for the caller to use
|
|
@@ -247,45 +294,10 @@ async function runFinalizePhase(ctx) {
|
|
|
247
294
|
// suppress unused warning — sources was previously used inline
|
|
248
295
|
void sources;
|
|
249
296
|
}
|
|
250
|
-
/**
|
|
251
|
-
* Stash-organization conventions (SPEC-8): reconcile the `index.indexBodyOpening`
|
|
252
|
-
* flag with the state the index was last FULLY built with (index_meta key
|
|
253
|
-
* `indexBodyOpening`), returning a warning message while they diverge.
|
|
254
|
-
*
|
|
255
|
-
* Incremental runs re-extract only changed files (and embeddings are only
|
|
256
|
-
* generated for rows lacking one), so a flag toggle leaves the index MIXED
|
|
257
|
-
* until a full rebuild — `akm index --full` re-extracts every entry and wipes
|
|
258
|
-
* embeddings so they regenerate from the new text. The warning therefore
|
|
259
|
-
* repeats on every incremental run until a full walk records the flag state
|
|
260
|
-
* as applied.
|
|
261
|
-
*
|
|
262
|
-
* A missing meta key on an incremental run means the index predates this
|
|
263
|
-
* feature, i.e. it was necessarily built with the flag OFF — so the absent
|
|
264
|
-
* key reads (and is seeded) as "0", never as the current flag value. This
|
|
265
|
-
* keeps the most likely real toggle scenario — upgrade, enable the flag, run
|
|
266
|
-
* a plain `akm index` — warning until `--full` runs (review finding).
|
|
267
|
-
*
|
|
268
|
-
* Exported for tests; production's only caller is the finalize phase above.
|
|
269
|
-
*/
|
|
270
|
-
export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
|
|
271
|
-
const bodyOpeningFlag = flagEnabled ? "1" : "0";
|
|
272
|
-
const prevBodyOpeningFlag = getMeta(db, "indexBodyOpening") ?? "0";
|
|
273
|
-
// Only a full walk (which includes the first build ever) may record the
|
|
274
|
-
// current flag as the applied state; incremental runs preserve — or, for a
|
|
275
|
-
// pre-feature index, seed — the state of the last full build.
|
|
276
|
-
setMeta(db, "indexBodyOpening", isFullWalk ? bodyOpeningFlag : prevBodyOpeningFlag);
|
|
277
|
-
if (isFullWalk || prevBodyOpeningFlag === bodyOpeningFlag)
|
|
278
|
-
return undefined;
|
|
279
|
-
return (`index.indexBodyOpening is ${flagEnabled ? "enabled" : "disabled"} but the index was built with it ` +
|
|
280
|
-
`${flagEnabled ? "disabled" : "enabled"}. Incremental runs only re-extract changed files, so ` +
|
|
281
|
-
"indexed text and embeddings are stale for unchanged entries. Run `akm index --full` to apply the new " +
|
|
282
|
-
"setting everywhere (embeddings regenerate), and re-mint collapse-detector canary baselines via " +
|
|
283
|
-
"`bun scripts/refresh-canary-set.ts --refresh` if you use them.");
|
|
284
|
-
}
|
|
285
297
|
// ── Clean pass ───────────────────────────────────────────────────────────────
|
|
286
298
|
/**
|
|
287
|
-
*
|
|
288
|
-
* no longer exists on disk and remove them (unless `dryRun` is true).
|
|
299
|
+
* Missing-file reconciliation: scan the `entries` table for rows whose source
|
|
300
|
+
* file no longer exists on disk and remove them (unless `dryRun` is true).
|
|
289
301
|
*
|
|
290
302
|
* Only rows with a non-empty `file_path` are checked — remote/virtual entries
|
|
291
303
|
* that have no local path are always skipped.
|
|
@@ -299,7 +311,7 @@ export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
|
|
|
299
311
|
* reported instead.
|
|
300
312
|
*/
|
|
301
313
|
function runCleanPass(db, dryRun) {
|
|
302
|
-
const allEntries = db.prepare("SELECT id,
|
|
314
|
+
const allEntries = db.prepare("SELECT id, item_ref AS ref, file_path AS path FROM entries").all();
|
|
303
315
|
// Only check entries that have a non-empty local path (skip remote/virtual).
|
|
304
316
|
const localEntries = allEntries.filter((e) => typeof e.path === "string" && e.path.trim() !== "");
|
|
305
317
|
const missing = [];
|
|
@@ -335,9 +347,23 @@ export function _setAkmIndexForTests(fake) {
|
|
|
335
347
|
akmIndexOverride = fake;
|
|
336
348
|
}
|
|
337
349
|
export async function akmIndex(options) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
350
|
+
try {
|
|
351
|
+
const override = akmIndexOverride;
|
|
352
|
+
return override ? await override(options) : await akmIndexReal(options);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const updateDb = options.deferredUpdateTransaction?.db;
|
|
356
|
+
if (updateDb?.inTransaction) {
|
|
357
|
+
try {
|
|
358
|
+
updateDb.exec("ROLLBACK");
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Preserve the indexing error. The update coordinator will retry
|
|
362
|
+
// rollback before closing its borrowed unified handle.
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
341
367
|
}
|
|
342
368
|
let indexTransactionHookForTests;
|
|
343
369
|
/**
|
|
@@ -385,7 +411,7 @@ function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig,
|
|
|
385
411
|
}
|
|
386
412
|
}
|
|
387
413
|
const persistedAdapters = {};
|
|
388
|
-
if (detectedByBundle.size === 0)
|
|
414
|
+
if (detectedByBundle.size === 0 || !opts.persist)
|
|
389
415
|
return { config, persistedAdapters };
|
|
390
416
|
const nextConfig = mutateConfig((current) => {
|
|
391
417
|
if (!current.bundles)
|
|
@@ -418,6 +444,38 @@ function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig,
|
|
|
418
444
|
}
|
|
419
445
|
return { config: nextConfig, persistedAdapters };
|
|
420
446
|
}
|
|
447
|
+
function createIndexRunContext(options) {
|
|
448
|
+
const prevStashDir = getMeta(options.db, "stashDir");
|
|
449
|
+
const prevBuiltAt = getMeta(options.db, "builtAt");
|
|
450
|
+
const isIncremental = !options.full && prevStashDir === options.stashDir && !!prevBuiltAt;
|
|
451
|
+
const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
|
|
452
|
+
const { t0, ...context } = options;
|
|
453
|
+
return {
|
|
454
|
+
...context,
|
|
455
|
+
loweringNotices: [...options.enrichmentExecution.notices],
|
|
456
|
+
timing: {
|
|
457
|
+
t0,
|
|
458
|
+
tWalkStart: t0,
|
|
459
|
+
tWalkEnd: t0,
|
|
460
|
+
tLlmEnd: t0,
|
|
461
|
+
tFtsEnd: t0,
|
|
462
|
+
tEmbedEnd: t0,
|
|
463
|
+
tFinalizeStart: t0,
|
|
464
|
+
tFinalizeEnd: t0,
|
|
465
|
+
},
|
|
466
|
+
isIncremental,
|
|
467
|
+
builtAtMs,
|
|
468
|
+
hadRemovedSources: false,
|
|
469
|
+
removedSources: [],
|
|
470
|
+
scanComplete: true,
|
|
471
|
+
scannedDirs: 0,
|
|
472
|
+
skippedDirs: 0,
|
|
473
|
+
generatedCount: 0,
|
|
474
|
+
walkWarnings: [],
|
|
475
|
+
dirsNeedingLlm: [],
|
|
476
|
+
embeddingResult: null,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
421
479
|
async function akmIndexReal(options) {
|
|
422
480
|
// R-022: `dryRun` only ever gated the `--clean` stale-entry removal pass
|
|
423
481
|
// (see `runCleanPass` below) — every other phase (walk, LLM enrichment,
|
|
@@ -460,7 +518,8 @@ async function akmIndexReal(options) {
|
|
|
460
518
|
// Durable state must be runtime-compatible before source hydration,
|
|
461
519
|
// adapter persistence, or index.db creation can mutate the installation.
|
|
462
520
|
onProgress({ phase: "preflight", message: "Validating durable state." });
|
|
463
|
-
|
|
521
|
+
if (!options.deferredUpdateTransaction)
|
|
522
|
+
withStateDb(() => undefined);
|
|
464
523
|
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
465
524
|
// so their content directories exist on disk for the walker to discover.
|
|
466
525
|
const sourceCacheStart = Date.now();
|
|
@@ -482,6 +541,7 @@ async function akmIndexReal(options) {
|
|
|
482
541
|
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
483
542
|
const detected = detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, {
|
|
484
543
|
announce: options.implicit !== true,
|
|
544
|
+
persist: options.persistDetectedAdapters !== false,
|
|
485
545
|
});
|
|
486
546
|
config = detected.config;
|
|
487
547
|
const persistedAdapters = detected.persistedAdapters;
|
|
@@ -491,20 +551,22 @@ async function akmIndexReal(options) {
|
|
|
491
551
|
message: `Resolved ${allSourceDirs.length} stash source${allSourceDirs.length === 1 ? "" : "s"}.`,
|
|
492
552
|
});
|
|
493
553
|
const t0 = Date.now();
|
|
554
|
+
const enrichmentExecution = resolveIndexPassExecution("enrichment", config);
|
|
494
555
|
// Open database — pass embedding dimension from config if available
|
|
495
556
|
const dbPath = getDbPath();
|
|
496
557
|
const embeddingDim = config.embedding?.dimension;
|
|
497
|
-
const
|
|
558
|
+
const borrowedUpdateDb = options.deferredUpdateTransaction?.db;
|
|
559
|
+
const db = borrowedUpdateDb ?? openIndexDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
|
|
560
|
+
if (borrowedUpdateDb && !borrowedUpdateDb.inTransaction) {
|
|
561
|
+
throw new Error("Source update index requires an active borrowed index transaction.");
|
|
562
|
+
}
|
|
563
|
+
let indexRunContext;
|
|
498
564
|
try {
|
|
499
|
-
// Determine incremental vs full mode
|
|
500
|
-
const prevStashDir = getMeta(db, "stashDir");
|
|
501
|
-
const prevBuiltAt = getMeta(db, "builtAt");
|
|
502
|
-
const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
|
|
503
|
-
const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
|
|
504
565
|
// Assemble the run context
|
|
505
|
-
const ctx = {
|
|
566
|
+
const ctx = createIndexRunContext({
|
|
506
567
|
db,
|
|
507
568
|
config,
|
|
569
|
+
enrichmentExecution,
|
|
508
570
|
sources: allSourceEntries,
|
|
509
571
|
sourceDirs: allSourceDirs,
|
|
510
572
|
full,
|
|
@@ -512,55 +574,31 @@ async function akmIndexReal(options) {
|
|
|
512
574
|
stashDir,
|
|
513
575
|
onProgress,
|
|
514
576
|
signal,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
tWalkEnd: t0,
|
|
519
|
-
tLlmEnd: t0,
|
|
520
|
-
tFtsEnd: t0,
|
|
521
|
-
tEmbedEnd: t0,
|
|
522
|
-
tFinalizeStart: t0,
|
|
523
|
-
tFinalizeEnd: t0,
|
|
524
|
-
},
|
|
525
|
-
isIncremental,
|
|
526
|
-
builtAtMs,
|
|
527
|
-
hadRemovedSources: false,
|
|
528
|
-
removedSourceDirs: [],
|
|
529
|
-
scanComplete: true,
|
|
530
|
-
scannedDirs: 0,
|
|
531
|
-
skippedDirs: 0,
|
|
532
|
-
generatedCount: 0,
|
|
533
|
-
walkWarnings: [],
|
|
534
|
-
dirsNeedingLlm: [],
|
|
535
|
-
embeddingResult: null,
|
|
536
|
-
};
|
|
577
|
+
t0,
|
|
578
|
+
});
|
|
579
|
+
indexRunContext = ctx;
|
|
537
580
|
onProgress({
|
|
538
581
|
phase: "summary",
|
|
539
582
|
message: buildIndexSummaryMessage({
|
|
540
|
-
mode: isIncremental ? "incremental" : "full",
|
|
583
|
+
mode: ctx.isIncremental ? "incremental" : "full",
|
|
541
584
|
sourcesCount: allSourceDirs.length,
|
|
542
585
|
semanticSearchMode: config.semanticSearchMode,
|
|
543
586
|
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
544
|
-
llmEnabled: !!
|
|
587
|
+
llmEnabled: !!enrichmentExecution.runner,
|
|
545
588
|
vecAvailable: isVecAvailable(db),
|
|
546
589
|
}),
|
|
547
590
|
});
|
|
591
|
+
let cleanResult;
|
|
592
|
+
let cleanStart = Date.now();
|
|
593
|
+
let cleanEnd = cleanStart;
|
|
548
594
|
// ── Phase sequence ───────────────────────────────────────────────────────
|
|
549
595
|
await runSourceCachePhase(ctx);
|
|
550
596
|
await runWalkPhase(ctx);
|
|
551
597
|
applyRemovedSources(ctx);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
const verification = ctx.verification;
|
|
557
|
-
const totalEntries = ctx.totalEntries;
|
|
558
|
-
const { timing } = ctx;
|
|
559
|
-
// ── Clean pass ───────────────────────────────────────────────────────────
|
|
560
|
-
// After the normal index completes, remove entries whose source files no
|
|
561
|
-
// longer exist on disk. Remote entries (empty file_path) are skipped.
|
|
562
|
-
let cleanResult;
|
|
563
|
-
const cleanStart = Date.now();
|
|
598
|
+
// Reconcile explicit missing-file cleanup before embeddings, totals, or
|
|
599
|
+
// verification describe this generation. Dry-run intentionally leaves
|
|
600
|
+
// the generation unchanged while still returning the previewed refs.
|
|
601
|
+
cleanStart = Date.now();
|
|
564
602
|
if (clean) {
|
|
565
603
|
onProgress({
|
|
566
604
|
phase: "finalize",
|
|
@@ -574,17 +612,25 @@ async function akmIndexReal(options) {
|
|
|
574
612
|
cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
|
|
575
613
|
}
|
|
576
614
|
}
|
|
577
|
-
|
|
615
|
+
cleanEnd = Date.now();
|
|
616
|
+
await runEmbeddingPhase(ctx);
|
|
617
|
+
await runFinalizePhase(ctx, options.deferredUpdateTransaction);
|
|
578
618
|
// ────────────────────────────────────────────────────────────────────────
|
|
619
|
+
// runFinalizePhase always populates these before returning.
|
|
620
|
+
const verification = ctx.verification;
|
|
621
|
+
const totalEntries = ctx.totalEntries;
|
|
622
|
+
const { timing } = ctx;
|
|
579
623
|
return {
|
|
580
624
|
stashDir,
|
|
581
625
|
totalEntries,
|
|
582
626
|
generatedMetadata: ctx.generatedCount,
|
|
583
627
|
indexPath: dbPath,
|
|
584
|
-
mode: isIncremental ? "incremental" : "full",
|
|
628
|
+
mode: ctx.isIncremental ? "incremental" : "full",
|
|
585
629
|
directoriesScanned: ctx.scannedDirs,
|
|
586
630
|
directoriesSkipped: ctx.skippedDirs,
|
|
631
|
+
scanComplete: ctx.scanComplete,
|
|
587
632
|
...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
|
|
633
|
+
...(ctx.loweringNotices.length > 0 ? { notices: Object.freeze([...ctx.loweringNotices]) } : {}),
|
|
588
634
|
...(Object.keys(persistedAdapters).length > 0
|
|
589
635
|
? { configUpdated: { detectedAdapters: persistedAdapters } }
|
|
590
636
|
: {}),
|
|
@@ -606,10 +652,69 @@ async function akmIndexReal(options) {
|
|
|
606
652
|
};
|
|
607
653
|
}
|
|
608
654
|
finally {
|
|
609
|
-
|
|
655
|
+
if (indexRunContext?.enrichmentLease) {
|
|
656
|
+
disposeLoweredExecutionDispatchLease(indexRunContext.enrichmentLease);
|
|
657
|
+
}
|
|
658
|
+
if (!borrowedUpdateDb)
|
|
659
|
+
closeDatabase(db);
|
|
610
660
|
}
|
|
611
661
|
});
|
|
612
662
|
}
|
|
663
|
+
function buildIndexedSourceOwners(sources) {
|
|
664
|
+
const installations = deriveInstallations([...sources]);
|
|
665
|
+
const owners = new Map();
|
|
666
|
+
sources.forEach((source, index) => {
|
|
667
|
+
const installation = installations[index];
|
|
668
|
+
if (!installation)
|
|
669
|
+
return;
|
|
670
|
+
const component = installation.components[0];
|
|
671
|
+
owners.set(path.resolve(source.path), {
|
|
672
|
+
bundleId: installation.id,
|
|
673
|
+
componentId: component?.id ?? installation.id,
|
|
674
|
+
adapterId: component?.adapter ?? "akm",
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
return owners;
|
|
678
|
+
}
|
|
679
|
+
/** Read-only mirror of the enrichment cache gate used before entry persistence. */
|
|
680
|
+
function dirRecordsNeedMetadataDispatch(db, records, ownersByRoot) {
|
|
681
|
+
for (const record of records) {
|
|
682
|
+
if (record.skip || record.remove || !record.stash)
|
|
683
|
+
continue;
|
|
684
|
+
const owner = ownersByRoot.get(path.resolve(record.currentStashDir));
|
|
685
|
+
if (!owner)
|
|
686
|
+
throw new Error(`Missing bundle provenance for indexed source ${record.currentStashDir}`);
|
|
687
|
+
for (const entry of record.stash.entries) {
|
|
688
|
+
if (entry.quality !== "generated" || isEnrichmentComplete(entry))
|
|
689
|
+
continue;
|
|
690
|
+
const entryFile = entry.filename ? path.join(record.dirPath, entry.filename) : undefined;
|
|
691
|
+
if (!entryFile)
|
|
692
|
+
continue;
|
|
693
|
+
const adapterConceptId = record.conceptIdByFile?.get(entryFile);
|
|
694
|
+
if (!adapterConceptId)
|
|
695
|
+
continue;
|
|
696
|
+
let fileContent;
|
|
697
|
+
try {
|
|
698
|
+
fileContent = fs.readFileSync(entryFile, "utf8");
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// The dispatch path uses the same deterministic metadata fallback.
|
|
702
|
+
}
|
|
703
|
+
const bodyHash = computeBodyHash(fileContent ?? `${entry.name}\n${entry.description ?? ""}`);
|
|
704
|
+
const cacheKey = deriveEntryProvenance(owner, entry.type, entry.name, adapterConceptId).itemRef;
|
|
705
|
+
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
706
|
+
if (!cached)
|
|
707
|
+
return true;
|
|
708
|
+
try {
|
|
709
|
+
JSON.parse(cached.resultJson);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
613
718
|
function removalsFirst(records) {
|
|
614
719
|
return [...records.filter((record) => record.remove), ...records.filter((record) => !record.remove)];
|
|
615
720
|
}
|
|
@@ -654,8 +759,8 @@ function groupFileContextsByDir(fileContexts) {
|
|
|
654
759
|
}
|
|
655
760
|
return groups;
|
|
656
761
|
}
|
|
657
|
-
function sourceSnapshotRemovals(db, currentStashDir, currentDirs,
|
|
658
|
-
const indexedDirs =
|
|
762
|
+
function sourceSnapshotRemovals(db, currentStashDir, bundleId, currentDirs, allIndexedDirsByBundle) {
|
|
763
|
+
const indexedDirs = allIndexedDirsByBundle?.get(bundleId) ?? getIndexedDirPathsByBundleId(db, bundleId);
|
|
659
764
|
return [...indexedDirs]
|
|
660
765
|
.map((dirPath) => path.resolve(dirPath))
|
|
661
766
|
.filter((dirPath) => !currentDirs.has(dirPath))
|
|
@@ -688,6 +793,9 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
688
793
|
}
|
|
689
794
|
const walked = walkStashFlatWithStatus(currentStashDir, {
|
|
690
795
|
includeAllDirectories: component.adapter === "okf",
|
|
796
|
+
...(component.adapter === "akm" || component.adapter === "akm-workflow"
|
|
797
|
+
? { workflowSymlinkAdapter: component.adapter }
|
|
798
|
+
: {}),
|
|
691
799
|
});
|
|
692
800
|
const dirGroups = groupFileContextsByDir(walked.files);
|
|
693
801
|
const adapter = adapterForId(component.adapter);
|
|
@@ -723,20 +831,19 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
723
831
|
// A full, globally-complete run uses the atomic table wipe below. Every
|
|
724
832
|
// other run reconciles only sources that produced trustworthy snapshots.
|
|
725
833
|
if (reconcileMissingDirs && (isIncremental || !allComplete)) {
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
834
|
+
const allIndexedDirsByBundle = !isIncremental ? new Map() : undefined;
|
|
835
|
+
if (allIndexedDirsByBundle) {
|
|
728
836
|
for (const entry of getAllEntries(db)) {
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
allIndexedDirsBySource.set(sourceRoot, dirs);
|
|
837
|
+
const dirs = allIndexedDirsByBundle.get(entry.bundleId) ?? new Set();
|
|
838
|
+
dirs.add(path.dirname(path.resolve(entry.filePath)));
|
|
839
|
+
allIndexedDirsByBundle.set(entry.bundleId, dirs);
|
|
733
840
|
}
|
|
734
841
|
}
|
|
735
842
|
for (const plan of plans) {
|
|
736
843
|
if (!plan.walkComplete || !plan.adapter)
|
|
737
844
|
continue;
|
|
738
845
|
const currentDirs = new Set([...plan.dirGroups.keys()].map((dirPath) => path.resolve(dirPath)));
|
|
739
|
-
for (const removal of sourceSnapshotRemovals(db, plan.currentStashDir, currentDirs,
|
|
846
|
+
for (const removal of sourceSnapshotRemovals(db, plan.currentStashDir, plan.component.id, currentDirs, allIndexedDirsByBundle)) {
|
|
740
847
|
addRemoval(plan, removal.dirPath, removal.currentStashDir);
|
|
741
848
|
}
|
|
742
849
|
}
|
|
@@ -748,15 +855,18 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
748
855
|
// The first configured source that exposes a physical directory owns it.
|
|
749
856
|
// Remove rows left by a prior owner even when both adapters are identical.
|
|
750
857
|
const claimedDirs = new Set();
|
|
858
|
+
const sourcePathByBundle = new Map(plans.map((plan) => [plan.component.id, plan.currentStashDir]));
|
|
751
859
|
for (const plan of plans) {
|
|
752
860
|
for (const dirPath of plan.dirGroups.keys()) {
|
|
753
861
|
const resolvedDir = path.resolve(dirPath);
|
|
754
862
|
if (claimedDirs.has(resolvedDir))
|
|
755
863
|
continue;
|
|
756
864
|
claimedDirs.add(resolvedDir);
|
|
757
|
-
for (const
|
|
758
|
-
if (
|
|
759
|
-
|
|
865
|
+
for (const priorOwnerBundle of getIndexedBundleIdsByDir(db, dirPath)) {
|
|
866
|
+
if (priorOwnerBundle !== plan.component.id) {
|
|
867
|
+
const priorOwnerPath = sourcePathByBundle.get(priorOwnerBundle);
|
|
868
|
+
if (priorOwnerPath)
|
|
869
|
+
addRemoval(plan, dirPath, priorOwnerPath);
|
|
760
870
|
}
|
|
761
871
|
}
|
|
762
872
|
}
|
|
@@ -876,7 +986,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
876
986
|
// abstains on its own bundle's walked files. The core walk keeps only the
|
|
877
987
|
// universal hygiene `walkStashFlat` already applies (.git/dot-dirs/etc.).
|
|
878
988
|
const indexableFiles = ctxs.map((ctx) => ctx.absPath);
|
|
879
|
-
const forceScan = handoffDirs.has(path.resolve(dirPath));
|
|
989
|
+
const forceScan = handoffDirs.has(path.resolve(dirPath)) || requiresWorkflowSourcePreflight(ctxs);
|
|
880
990
|
if (markSeenOrSkipDuplicate(dirPath, currentStashDir, indexableFiles))
|
|
881
991
|
continue;
|
|
882
992
|
if (indexableFiles.length === 0) {
|
|
@@ -931,6 +1041,16 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
931
1041
|
complete: plans.every((plan) => plan.walkComplete && plan.adapter !== undefined),
|
|
932
1042
|
};
|
|
933
1043
|
}
|
|
1044
|
+
function requiresWorkflowSourcePreflight(ctxs) {
|
|
1045
|
+
return ctxs.some((ctx) => {
|
|
1046
|
+
try {
|
|
1047
|
+
return fs.lstatSync(ctx.absPath).isSymbolicLink();
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
934
1054
|
function preserveExistingIndex(doFullDelete, dirRecords, sourceRoots) {
|
|
935
1055
|
if (!doFullDelete)
|
|
936
1056
|
return false;
|
|
@@ -992,36 +1112,28 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
992
1112
|
// transaction so delete and re-insert are atomic — a concurrent reader
|
|
993
1113
|
// never observes an empty database between the two operations.
|
|
994
1114
|
if (fullDelete) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
/* ignore */
|
|
1000
|
-
}
|
|
1001
|
-
if (isVecAvailable(db)) {
|
|
1002
|
-
try {
|
|
1003
|
-
db.exec("DELETE FROM entries_vec");
|
|
1004
|
-
}
|
|
1005
|
-
catch {
|
|
1006
|
-
/* ignore */
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
db.exec("DELETE FROM entries_fts");
|
|
1010
|
-
db.exec("DELETE FROM utility_scores");
|
|
1115
|
+
// Entries and every child materialization share one deletion authority.
|
|
1116
|
+
// Usage events live in state.db and survive so finalize can relink them
|
|
1117
|
+
// to the replacement generation's row ids.
|
|
1118
|
+
deleteAllEntries(db, { cleanupUsageEvents: false });
|
|
1011
1119
|
db.exec("DELETE FROM index_dir_state");
|
|
1012
1120
|
// Chunk-8 WI-8.3: usage_events lives in state.db now (not index.db), so the
|
|
1013
1121
|
// wipe no longer detaches it here. The finalize pass's relinkUsageEvents
|
|
1014
1122
|
// (cross-DB) nulls entry_ids that no longer resolve to a rebuilt entry and
|
|
1015
1123
|
// re-resolves the rest by entry_ref — subsuming the old detach.
|
|
1016
|
-
db.exec("DELETE FROM entries");
|
|
1017
1124
|
// Atomicity observation point: inside the transaction the tables are now
|
|
1018
1125
|
// empty, but no other connection may observe that. See
|
|
1019
1126
|
// tests/integration/indexer/reindex-generation-atomicity.test.ts.
|
|
1020
1127
|
indexTransactionHook("full-delete-applied");
|
|
1021
1128
|
}
|
|
1022
1129
|
for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile, indexVariant, remove, pruneMissing, } of dirRecords) {
|
|
1130
|
+
const bundle = bundleByRoot.get(path.resolve(currentStashDir));
|
|
1131
|
+
if (!bundle)
|
|
1132
|
+
throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
|
|
1023
1133
|
if (remove) {
|
|
1024
|
-
const removedIds =
|
|
1134
|
+
const removedIds = deleteEntriesByDirAndBundle(db, dirPath, bundle.bundleId, {
|
|
1135
|
+
cleanupUsageEvents: false,
|
|
1136
|
+
});
|
|
1025
1137
|
addEntryIds(deletedUsageEntryIds, removedIds);
|
|
1026
1138
|
deleteIndexDirState(db, dirPath);
|
|
1027
1139
|
continue;
|
|
@@ -1041,15 +1153,12 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1041
1153
|
// Diff-persist (F4a M-core-2, spec §14.2): upsert the current file set
|
|
1042
1154
|
// FIRST (ON CONFLICT preserving `entries.id` so embeddings / utility /
|
|
1043
1155
|
// usage stay attached to unchanged rows), tracking every upserted
|
|
1044
|
-
// `
|
|
1156
|
+
// durable `item_ref`, then prune only the departed rows below. Replaces the old
|
|
1045
1157
|
// `deleteEntriesByDir` truncate-and-reinsert (which discarded ids).
|
|
1046
|
-
const
|
|
1158
|
+
const keptItemRefs = new Set();
|
|
1047
1159
|
let persistedRows = 0;
|
|
1048
1160
|
let dedupedRows = 0;
|
|
1049
1161
|
if (stash) {
|
|
1050
|
-
const bundle = bundleByRoot.get(path.resolve(currentStashDir));
|
|
1051
|
-
if (!bundle)
|
|
1052
|
-
throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
|
|
1053
1162
|
const ownerIdentity = bundle.bundleId;
|
|
1054
1163
|
for (const entry of stash.entries) {
|
|
1055
1164
|
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : null;
|
|
@@ -1070,24 +1179,15 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1070
1179
|
continue;
|
|
1071
1180
|
}
|
|
1072
1181
|
indexedAssetIdentities.add(identityKey);
|
|
1073
|
-
const entryKey = bundle?.adapterId !== "akm" && adapterConceptId
|
|
1074
|
-
? `${currentStashDir}:concept:${adapterConceptId}`
|
|
1075
|
-
: `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
1076
|
-
keptEntryKeys.add(entryKey);
|
|
1077
1182
|
const searchText = buildSearchText(entry);
|
|
1078
1183
|
const entryWithSize = attachFileSize(entry, entryPath);
|
|
1079
1184
|
// content_hash = doc.hash from the drain, keyed by the recognized
|
|
1080
1185
|
// file's path. A missing hash preserves the existing value on upsert.
|
|
1081
1186
|
const contentHash = hashByFile?.get(entryPath);
|
|
1082
1187
|
const provenance = deriveEntryProvenance(bundle, entry.type, entry.name, adapterConceptId);
|
|
1083
|
-
|
|
1188
|
+
keptItemRefs.add(provenance.itemRef);
|
|
1189
|
+
upsertEntry(db, entryPath, entryWithSize, searchText, provenance, contentHash);
|
|
1084
1190
|
persistedRows++;
|
|
1085
|
-
if (entry.type === "workflow") {
|
|
1086
|
-
const doc = takeWorkflowDocument(entry);
|
|
1087
|
-
if (doc) {
|
|
1088
|
-
upsertWorkflowDocument(db, entryId, doc, fs.readFileSync(entryPath));
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
1191
|
}
|
|
1092
1192
|
// Collect dirs needing LLM enhancement during the first walk.
|
|
1093
1193
|
// Only dirs with "generated" entries need enrichment.
|
|
@@ -1100,7 +1200,7 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1100
1200
|
// an empty kept-set this deletes every row for the dir — the exact net
|
|
1101
1201
|
// effect of the old unconditional `deleteEntriesByDir`, minus the id churn.
|
|
1102
1202
|
if (pruneMissing !== false) {
|
|
1103
|
-
addEntryIds(deletedUsageEntryIds,
|
|
1203
|
+
addEntryIds(deletedUsageEntryIds, deleteEntriesByDirExceptRefs(db, dirPath, bundle.bundleId, keptItemRefs, { cleanupUsageEvents: false }));
|
|
1104
1204
|
}
|
|
1105
1205
|
const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
|
|
1106
1206
|
const persistedReason = persistedRows === 0
|
|
@@ -1139,32 +1239,21 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1139
1239
|
deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
|
|
1140
1240
|
return { dirsNeedingLlm };
|
|
1141
1241
|
}
|
|
1142
|
-
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress, reconcileMissingDirs = true) {
|
|
1242
|
+
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress, reconcileMissingDirs = true, beforePersist) {
|
|
1143
1243
|
// Phase 1 (async): walk directories and pre-generate all metadata outside the
|
|
1144
1244
|
// transaction.
|
|
1145
1245
|
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs);
|
|
1246
|
+
const bundleByRoot = buildIndexedSourceOwners(allSourceEntries);
|
|
1247
|
+
await beforePersist?.(dirRecords, bundleByRoot);
|
|
1146
1248
|
// Phase 2 (sync): write all pre-generated metadata inside a single transaction.
|
|
1147
1249
|
// Source roots feed the #624-P1 zero-document preflight (a full-rebuild wipe
|
|
1148
1250
|
// is suppressed when the scan is empty because roots are unreadable).
|
|
1149
1251
|
const sourceRoots = allSourceEntries.map((s) => s.path);
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1252
|
+
// Map each source root → its durable bundle id so the writer can persist
|
|
1253
|
+
// `item_ref = <bundle>//<conceptId>` and canonical component/adapter
|
|
1254
|
+
// provenance. `deriveInstallations`
|
|
1153
1255
|
// preserves source order, so a positional zip yields the SAME bundle id the
|
|
1154
1256
|
// dispatched `adapter.recognize` emits as `IndexDocument.ref` for that root.
|
|
1155
|
-
const installations = deriveInstallations(allSourceEntries);
|
|
1156
|
-
const bundleByRoot = new Map();
|
|
1157
|
-
allSourceEntries.forEach((source, i) => {
|
|
1158
|
-
const inst = installations[i];
|
|
1159
|
-
if (!inst)
|
|
1160
|
-
return;
|
|
1161
|
-
const component = inst.components[0];
|
|
1162
|
-
bundleByRoot.set(path.resolve(source.path), {
|
|
1163
|
-
bundleId: inst.id,
|
|
1164
|
-
componentId: component?.id ?? inst.id,
|
|
1165
|
-
adapterId: component?.adapter ?? "akm",
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
1257
|
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, complete, bundleByRoot);
|
|
1169
1258
|
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm, complete };
|
|
1170
1259
|
}
|
|
@@ -1184,12 +1273,11 @@ function indexedProvenanceForFile(db, filePath) {
|
|
|
1184
1273
|
adapterId: row.adapterId,
|
|
1185
1274
|
};
|
|
1186
1275
|
}
|
|
1187
|
-
async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal) {
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (!llmConfig || dirsNeedingLlm.length === 0)
|
|
1276
|
+
async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgress, signal, onNotices, lease) {
|
|
1277
|
+
// The invocation owns one frozen symbolic selection. Summary reporting and
|
|
1278
|
+
// every enrichment dispatch consume this same snapshot.
|
|
1279
|
+
const llmRunner = execution.runner;
|
|
1280
|
+
if (!llmRunner || dirsNeedingLlm.length === 0)
|
|
1193
1281
|
return;
|
|
1194
1282
|
// Aggregate per-entry failures so a misconfigured LLM endpoint surfaces
|
|
1195
1283
|
// as a single visible warning instead of silently degrading every entry
|
|
@@ -1212,7 +1300,7 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
1212
1300
|
// engine's timeoutMs (or 10 minutes if not set). Users can extend it via
|
|
1213
1301
|
// `index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`, or the
|
|
1214
1302
|
// engine's own `engines.<name>.timeoutMs`) — no separate knob needed.
|
|
1215
|
-
const enrichDeadline = createEnrichmentDeadline(
|
|
1303
|
+
const enrichDeadline = createEnrichmentDeadline(llmRunner.timeoutMs, totalEntries);
|
|
1216
1304
|
let deadlineHit = false;
|
|
1217
1305
|
const enrichSignal = (() => {
|
|
1218
1306
|
if (!enrichDeadline)
|
|
@@ -1233,12 +1321,13 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
1233
1321
|
onProgress?.({
|
|
1234
1322
|
phase: "llm",
|
|
1235
1323
|
message: `LLM enhancement starting for ${totalEntries} entr${totalEntries === 1 ? "y" : "ies"} ` +
|
|
1236
|
-
`across ${totalDirs} director${totalDirs === 1 ? "y" : "ies"} (concurrency ${getDefaultLlmConcurrency(
|
|
1324
|
+
`across ${totalDirs} director${totalDirs === 1 ? "y" : "ies"} (concurrency ${getDefaultLlmConcurrency(llmRunner.connection)}).`,
|
|
1237
1325
|
processed: 0,
|
|
1238
1326
|
total: totalEntries,
|
|
1239
1327
|
});
|
|
1240
1328
|
}
|
|
1241
1329
|
let currentDirLabel;
|
|
1330
|
+
let configFailure;
|
|
1242
1331
|
let lastProgressAt = Date.now();
|
|
1243
1332
|
let heartbeatTimer;
|
|
1244
1333
|
if (totalEntries > 0 && onProgress) {
|
|
@@ -1284,30 +1373,42 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
1284
1373
|
});
|
|
1285
1374
|
lastProgressAt = Date.now();
|
|
1286
1375
|
const targetStash = { entries: entriesToEnhance };
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
lastProgressAt = Date.now();
|
|
1291
|
-
onProgress?.({
|
|
1292
|
-
phase: "llm",
|
|
1293
|
-
message: `Enhanced ${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"}; ` +
|
|
1294
|
-
`${completedDirs}/${totalDirs} director${totalDirs === 1 ? "y" : "ies"} complete` +
|
|
1295
|
-
(event.entryName ? `; current ${event.entryName}` : "") +
|
|
1296
|
-
(currentDirLabel ? ` in ${currentDirLabel}` : "") +
|
|
1297
|
-
(event.outcome === "cache-hit" ? " (cache hit)" : ""),
|
|
1298
|
-
processed: completedEntries,
|
|
1299
|
-
total: totalEntries,
|
|
1300
|
-
});
|
|
1376
|
+
const itemRefs = entriesToEnhance.map((entry) => {
|
|
1377
|
+
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
1378
|
+
return indexedProvenanceForFile(db, entryPath).itemRef;
|
|
1301
1379
|
});
|
|
1380
|
+
let enhanced;
|
|
1381
|
+
try {
|
|
1382
|
+
enhanced = await enhanceStashWithLlm(llmRunner, targetStash, files, summary, enrichSignal, db, itemRefs, config, (event) => {
|
|
1383
|
+
completedEntries++;
|
|
1384
|
+
lastProgressAt = Date.now();
|
|
1385
|
+
onProgress?.({
|
|
1386
|
+
phase: "llm",
|
|
1387
|
+
message: `Enhanced ${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"}; ` +
|
|
1388
|
+
`${completedDirs}/${totalDirs} director${totalDirs === 1 ? "y" : "ies"} complete` +
|
|
1389
|
+
(event.entryName ? `; current ${event.entryName}` : "") +
|
|
1390
|
+
(currentDirLabel ? ` in ${currentDirLabel}` : "") +
|
|
1391
|
+
(event.outcome === "cache-hit" ? " (cache hit)" : ""),
|
|
1392
|
+
processed: completedEntries,
|
|
1393
|
+
total: totalEntries,
|
|
1394
|
+
});
|
|
1395
|
+
}, onNotices, lease);
|
|
1396
|
+
}
|
|
1397
|
+
catch (err) {
|
|
1398
|
+
if (err instanceof ConfigError) {
|
|
1399
|
+
configFailure ??= err;
|
|
1400
|
+
return undefined;
|
|
1401
|
+
}
|
|
1402
|
+
throw err;
|
|
1403
|
+
}
|
|
1302
1404
|
// Re-upsert the enhanced entries in a single transaction so a crash
|
|
1303
1405
|
// cannot leave half the entries updated and the rest stale.
|
|
1304
1406
|
db.transaction(() => {
|
|
1305
1407
|
for (const entry of enhanced.entries) {
|
|
1306
1408
|
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
1307
|
-
const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
1308
1409
|
const searchText = buildSearchText(entry);
|
|
1309
1410
|
const provenance = indexedProvenanceForFile(db, entryPath);
|
|
1310
|
-
upsertEntry(db,
|
|
1411
|
+
upsertEntry(db, entryPath, attachFileSize(entry, entryPath), searchText, provenance);
|
|
1311
1412
|
}
|
|
1312
1413
|
})();
|
|
1313
1414
|
completedDirs++;
|
|
@@ -1325,7 +1426,9 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
1325
1426
|
// Studio, Ollama run one inference at a time — parallel requests cause
|
|
1326
1427
|
// "Model reloaded" / 500 errors). No config override reaches this path:
|
|
1327
1428
|
// `resolveLlmEngineUse` does not forward `engines.<name>.concurrency`.
|
|
1328
|
-
getDefaultLlmConcurrency(
|
|
1429
|
+
getDefaultLlmConcurrency(llmRunner.connection));
|
|
1430
|
+
if (configFailure)
|
|
1431
|
+
throw configFailure;
|
|
1329
1432
|
}
|
|
1330
1433
|
finally {
|
|
1331
1434
|
if (heartbeatTimer)
|
|
@@ -1351,136 +1454,6 @@ export function createEnrichmentDeadline(timeoutMs, totalEntries) {
|
|
|
1351
1454
|
const perEntryTimeoutMs = timeoutMs === undefined ? 10 * 60 * 1000 : timeoutMs;
|
|
1352
1455
|
return perEntryTimeoutMs === null ? undefined : AbortSignal.timeout(perEntryTimeoutMs * Math.max(totalEntries, 1));
|
|
1353
1456
|
}
|
|
1354
|
-
async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
1355
|
-
throwIfAborted(signal);
|
|
1356
|
-
if (config.semanticSearchMode === "off") {
|
|
1357
|
-
onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
|
|
1358
|
-
return { success: false, reason: "index-missing", message: "Semantic search is disabled." };
|
|
1359
|
-
}
|
|
1360
|
-
// Detect embedding model/provider changes and purge stale embeddings
|
|
1361
|
-
// so that incremental reindex regenerates all vectors with the new model.
|
|
1362
|
-
const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
|
|
1363
|
-
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
1364
|
-
if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
1365
|
-
// Model/provider changed → stored vectors are incompatible. Clear them;
|
|
1366
|
-
// re-embedded by this index run.
|
|
1367
|
-
//
|
|
1368
|
-
// The vec table goes too. "Same dimension, so keep the vec table" only held
|
|
1369
|
-
// for a same-width model swap: entries_vec is a vec0 virtual table declared
|
|
1370
|
-
// at a FIXED width, so after a dimension-changing model change every insert
|
|
1371
|
-
// failed against the old width, and ensureSchema's dim-change rebuild never
|
|
1372
|
-
// fired because it only runs for callers that pass an explicit
|
|
1373
|
-
// embeddingDim. The stale table survived `--full` — the exact remedy the
|
|
1374
|
-
// warning recommended. Clearing the stored dim lets the next ensureSchema
|
|
1375
|
-
// materialize it at the new width; until then the fast-path flag reads
|
|
1376
|
-
// false (no table) and search uses the complete BLOB table.
|
|
1377
|
-
purgeEmbeddings(db, { dropVecTable: true });
|
|
1378
|
-
deleteMeta(db, "embeddingDim");
|
|
1379
|
-
}
|
|
1380
|
-
try {
|
|
1381
|
-
const { embedBatch } = await import("../llm/embedder.js");
|
|
1382
|
-
const { estimateTokenCount } = await import("../llm/embedders/remote.js");
|
|
1383
|
-
throwIfAborted(signal);
|
|
1384
|
-
const allEntries = getAllEntriesForEmbedding(db);
|
|
1385
|
-
if (allEntries.length === 0) {
|
|
1386
|
-
onProgress({ phase: "embeddings", message: "Embeddings already up to date." });
|
|
1387
|
-
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
1388
|
-
return { success: true };
|
|
1389
|
-
}
|
|
1390
|
-
onProgress({
|
|
1391
|
-
phase: "embeddings",
|
|
1392
|
-
message: `Generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}.`,
|
|
1393
|
-
});
|
|
1394
|
-
const texts = allEntries.map((e) => e.searchText);
|
|
1395
|
-
// Verbose: log each document before it is sent to the embedding API so
|
|
1396
|
-
// operators can see exactly where embedding fails without waiting for an error.
|
|
1397
|
-
if (isVerbose()) {
|
|
1398
|
-
const EMBED_BATCH_SIZE = 100; // mirrors REMOTE_BATCH_SIZE in remote.ts
|
|
1399
|
-
const totalBatches = Math.ceil(texts.length / EMBED_BATCH_SIZE);
|
|
1400
|
-
for (let i = 0; i < texts.length; i++) {
|
|
1401
|
-
const batchNum = Math.floor(i / EMBED_BATCH_SIZE) + 1;
|
|
1402
|
-
const chars = texts[i].length;
|
|
1403
|
-
const tokens = estimateTokenCount(texts[i]);
|
|
1404
|
-
const ref = allEntries[i].entryKey.split(":").slice(1).join(":"); // strip stashDir prefix
|
|
1405
|
-
warnVerbose(`[embed] ${ref} (${chars} chars, est. ${tokens} tokens) → batch ${batchNum}/${totalBatches}`);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
let heartbeatTimer;
|
|
1409
|
-
try {
|
|
1410
|
-
heartbeatTimer = setInterval(() => {
|
|
1411
|
-
onProgress({
|
|
1412
|
-
phase: "embeddings",
|
|
1413
|
-
message: `Still generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}; waiting on embedding provider.`,
|
|
1414
|
-
});
|
|
1415
|
-
}, 15000);
|
|
1416
|
-
const embeddings = await embedBatch(texts, config.embedding, signal);
|
|
1417
|
-
throwIfAborted(signal);
|
|
1418
|
-
// Wrap all embedding upserts in a single transaction so partial
|
|
1419
|
-
// state is rolled back on failure rather than leaving the table half-filled.
|
|
1420
|
-
let storedCount = 0;
|
|
1421
|
-
let skippedCount = 0;
|
|
1422
|
-
let vecFailedCount = 0;
|
|
1423
|
-
let vecUnavailableCount = 0;
|
|
1424
|
-
db.transaction(() => {
|
|
1425
|
-
for (let i = 0; i < allEntries.length; i++) {
|
|
1426
|
-
const res = upsertEmbedding(db, allEntries[i].id, embeddings[i]);
|
|
1427
|
-
if (res.stored) {
|
|
1428
|
-
storedCount++;
|
|
1429
|
-
}
|
|
1430
|
-
else {
|
|
1431
|
-
skippedCount++;
|
|
1432
|
-
}
|
|
1433
|
-
if (res.vec === "failed")
|
|
1434
|
-
vecFailedCount++;
|
|
1435
|
-
if (res.vec === "unavailable")
|
|
1436
|
-
vecUnavailableCount++;
|
|
1437
|
-
}
|
|
1438
|
-
})();
|
|
1439
|
-
if (skippedCount > 0) {
|
|
1440
|
-
warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
|
|
1441
|
-
}
|
|
1442
|
-
// Record the ACTUAL vec-insert outcome so semantic search reflects it
|
|
1443
|
-
// instead of inferring readiness from stored-BLOB counts. Any failure
|
|
1444
|
-
// marks the fast path degraded, routing search to the JS-cosine fallback
|
|
1445
|
-
// over the (complete) BLOB table — honest degradation, not a hard failure.
|
|
1446
|
-
//
|
|
1447
|
-
// 'unavailable' has to degrade the flag too. It means no vec row was
|
|
1448
|
-
// written at all, so marking the fast path ready left a later open (a
|
|
1449
|
-
// different runtime, or sqlite-vec installed afterwards) trusting an
|
|
1450
|
-
// empty entries_vec and returning zero semantic hits against a fully
|
|
1451
|
-
// populated BLOB table.
|
|
1452
|
-
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0);
|
|
1453
|
-
if (vecFailedCount > 0) {
|
|
1454
|
-
warn(`[embed] ${vecFailedCount} sqlite-vec fast-path insert${vecFailedCount === 1 ? "" : "s"} failed — ` +
|
|
1455
|
-
"semantic search will use the slower JS-cosine fallback over stored embeddings. " +
|
|
1456
|
-
"Rebuild with 'akm index --full' after resolving the vec table (often a vector-dimension mismatch).");
|
|
1457
|
-
}
|
|
1458
|
-
onProgress({
|
|
1459
|
-
phase: "embeddings",
|
|
1460
|
-
message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
|
|
1461
|
-
});
|
|
1462
|
-
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
1463
|
-
return { success: true, vecInsertFailures: vecFailedCount };
|
|
1464
|
-
}
|
|
1465
|
-
finally {
|
|
1466
|
-
if (heartbeatTimer)
|
|
1467
|
-
clearInterval(heartbeatTimer);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
catch (error) {
|
|
1471
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1472
|
-
warn("Embedding generation failed, continuing without:", message);
|
|
1473
|
-
onProgress({
|
|
1474
|
-
phase: "embeddings",
|
|
1475
|
-
message: `Embedding generation failed: ${message}`,
|
|
1476
|
-
});
|
|
1477
|
-
return {
|
|
1478
|
-
success: false,
|
|
1479
|
-
reason: classifySemanticFailure(message),
|
|
1480
|
-
message: `Semantic search verification failed: ${message}`,
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
1457
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
1485
1458
|
function attachFileSize(entry, entryPath) {
|
|
1486
1459
|
try {
|
|
@@ -1586,9 +1559,10 @@ function resolveIndexedFiles(dirPath, files, stash) {
|
|
|
1586
1559
|
}
|
|
1587
1560
|
return resolved.size > 0 ? [...resolved] : files;
|
|
1588
1561
|
}
|
|
1589
|
-
async function enhanceStashWithLlm(
|
|
1562
|
+
async function enhanceStashWithLlm(llmRunner, stash, files, summary, signal, db, itemRefs, akmConfig, onEntryDone, onNotices, lease) {
|
|
1590
1563
|
const { enhanceMetadata } = await import("../llm/metadata-enhance.js");
|
|
1591
1564
|
const { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } = await import("../storage/repositories/index-llm-cache-repository.js");
|
|
1565
|
+
let configFailure;
|
|
1592
1566
|
const results = await concurrentMap(stash.entries, async (entry, idx) => {
|
|
1593
1567
|
if (signal?.aborted)
|
|
1594
1568
|
return entry;
|
|
@@ -1607,11 +1581,12 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1607
1581
|
}
|
|
1608
1582
|
}
|
|
1609
1583
|
// Incremental cache: skip LLM call when file body is unchanged. The
|
|
1610
|
-
//
|
|
1611
|
-
// across index runs.
|
|
1584
|
+
// Cache metadata enrichment by the canonical durable item ref.
|
|
1612
1585
|
const cacheBody = fileContent ?? `${entry.name}\n${entry.description ?? ""}`;
|
|
1613
1586
|
const bodyHash = computeBodyHash(cacheBody);
|
|
1614
|
-
const cacheKey =
|
|
1587
|
+
const cacheKey = itemRefs?.[idx];
|
|
1588
|
+
if (!cacheKey)
|
|
1589
|
+
throw new Error(`Missing canonical item ref for enrichment entry ${entry.name}.`);
|
|
1615
1590
|
if (db) {
|
|
1616
1591
|
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
1617
1592
|
if (cached) {
|
|
@@ -1634,7 +1609,7 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1634
1609
|
}
|
|
1635
1610
|
}
|
|
1636
1611
|
}
|
|
1637
|
-
const outcome = await enhanceMetadata(
|
|
1612
|
+
const outcome = await enhanceMetadata(llmRunner, entry, fileContent, signal, akmConfig, onNotices, lease);
|
|
1638
1613
|
if (outcome.status !== "enriched") {
|
|
1639
1614
|
// Not a genuine LLM success: the gate was closed (`skipped`) or the
|
|
1640
1615
|
// call errored/timed out (`failed`). Do NOT mark the entry enriched
|
|
@@ -1682,6 +1657,10 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1682
1657
|
return updated;
|
|
1683
1658
|
}
|
|
1684
1659
|
catch (err) {
|
|
1660
|
+
if (err instanceof ConfigError) {
|
|
1661
|
+
configFailure ??= err;
|
|
1662
|
+
return entry;
|
|
1663
|
+
}
|
|
1685
1664
|
const msg = toErrorMessage(err);
|
|
1686
1665
|
// failureSamples is bounded to 3 items, so a linear scan is cheaper
|
|
1687
1666
|
// than maintaining a parallel Set for membership checks (#177 review).
|
|
@@ -1694,7 +1673,9 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1694
1673
|
},
|
|
1695
1674
|
// Defaults: 2 for remote LLM APIs, 1 for local model servers. No config
|
|
1696
1675
|
// override reaches this path (see getDefaultLlmConcurrency).
|
|
1697
|
-
getDefaultLlmConcurrency(
|
|
1676
|
+
getDefaultLlmConcurrency(llmRunner.connection));
|
|
1677
|
+
if (configFailure)
|
|
1678
|
+
throw configFailure;
|
|
1698
1679
|
// concurrentMap returns Array<T | undefined>; filter out undefined slots
|
|
1699
1680
|
// (which can only occur if the callback itself returned undefined, which
|
|
1700
1681
|
// it never does above — but TypeScript needs the filter for type safety).
|
|
@@ -1711,58 +1692,111 @@ async function resolveLookupSources() {
|
|
|
1711
1692
|
}
|
|
1712
1693
|
function resolveLookupScope(bundle, sources) {
|
|
1713
1694
|
if (!bundle)
|
|
1714
|
-
return {
|
|
1715
|
-
return {
|
|
1695
|
+
return { candidateSources: sources, qualified: false };
|
|
1696
|
+
return { candidateSources: resolveSourcesForOrigin(bundle, sources), qualified: true };
|
|
1716
1697
|
}
|
|
1717
|
-
|
|
1718
|
-
export async function lookupBundleRef(ref) {
|
|
1698
|
+
async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
1719
1699
|
const sources = await resolveLookupSources();
|
|
1720
1700
|
if (sources.length === 0)
|
|
1721
|
-
return null;
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1701
|
+
return { entry: null };
|
|
1702
|
+
const bundleBySourcePath = new Map(deriveInstallations(sources).map((installation, index) => [path.resolve(sources[index].path), installation.id]));
|
|
1703
|
+
const { candidateSources, qualified } = resolveLookupScope(ref.bundle, sources);
|
|
1704
|
+
if (candidateSources.length === 0)
|
|
1705
|
+
return { entry: null };
|
|
1706
|
+
let db;
|
|
1707
|
+
let indexError;
|
|
1726
1708
|
try {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1709
|
+
db = openLookupDatabase(getDbPath());
|
|
1710
|
+
}
|
|
1711
|
+
catch (error) {
|
|
1712
|
+
indexError = error;
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
for (const source of candidateSources) {
|
|
1716
|
+
const adapterId = source.adapterId ?? detectAdapterId(source.path);
|
|
1717
|
+
const owner = resolveAdapterConceptOwner(source.path, adapterId, ref.conceptId);
|
|
1718
|
+
const lookupConceptId = owner?.conceptId ?? ref.conceptId;
|
|
1719
|
+
const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, lookupConceptId);
|
|
1720
|
+
const sourceBundleId = bundleBySourcePath.get(path.resolve(source.path));
|
|
1721
|
+
const id = db && sourceBundleId ? findEntryIdByRef(db, inputRef, sourceBundleId) : undefined;
|
|
1722
|
+
if (id !== undefined && owner && db) {
|
|
1723
|
+
const entry = readLookupEntry(db, id, ref.conceptId, source.path);
|
|
1724
|
+
if (entry) {
|
|
1725
|
+
if (owner.workflowSource) {
|
|
1726
|
+
assertIndexedWorkflowSourceIdentity(inputRef, entry.filePath, owner.workflowSource);
|
|
1727
|
+
if (entry.adapterId !== adapterId) {
|
|
1728
|
+
throw new WorkflowSourceIdentityError(inputRef, entry.filePath, owner.path);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
|
|
1732
|
+
return { entry: null, owner, ...(indexError === undefined ? {} : { indexError }) };
|
|
1733
|
+
}
|
|
1734
|
+
return { entry, owner, ...(indexError === undefined ? {} : { indexError }) };
|
|
1735
|
+
}
|
|
1745
1736
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
filePath: row.filePath,
|
|
1751
|
-
stashDir: row.stashDir,
|
|
1752
|
-
type: row.type,
|
|
1753
|
-
name: document?.name ?? ref.conceptId.split("/").pop() ?? ref.conceptId,
|
|
1754
|
-
adapterId: row.adapterId,
|
|
1755
|
-
document,
|
|
1756
|
-
itemRef: row.itemRef,
|
|
1757
|
-
bundleId: row.bundleId,
|
|
1758
|
-
conceptId: row.conceptId,
|
|
1759
|
-
};
|
|
1737
|
+
// A physical owner with a missing/incomplete index row still owns this
|
|
1738
|
+
// unqualified concept. Stop here so a later source cannot retarget it.
|
|
1739
|
+
if (owner)
|
|
1740
|
+
return { entry: null, owner, ...(indexError === undefined ? {} : { indexError }) };
|
|
1760
1741
|
}
|
|
1761
|
-
return null;
|
|
1742
|
+
return { entry: null, ...(indexError === undefined ? {} : { indexError }) };
|
|
1762
1743
|
}
|
|
1763
1744
|
finally {
|
|
1764
|
-
|
|
1745
|
+
if (db)
|
|
1746
|
+
closeDatabase(db);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
export async function lookupBundleRefWithResolution(ref) {
|
|
1750
|
+
return lookupBundleRefWithResolutionUsing(ref, openExistingDatabase);
|
|
1751
|
+
}
|
|
1752
|
+
/** Resolve an adapter-owned `[bundle//]conceptId` without interpreting its path as an AKM type. */
|
|
1753
|
+
export async function lookupBundleRef(ref) {
|
|
1754
|
+
const resolution = await lookupBundleRefWithResolution(ref);
|
|
1755
|
+
if (resolution.indexError !== undefined)
|
|
1756
|
+
throw resolution.indexError;
|
|
1757
|
+
return resolution.entry;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Resolve one execution source without opening the live index database for
|
|
1761
|
+
* write or allowing SQLite read-lock bookkeeping to touch its SHM file.
|
|
1762
|
+
*/
|
|
1763
|
+
export async function lookupBundleRefReadonly(ref) {
|
|
1764
|
+
const resolution = await lookupBundleRefWithResolutionUsing(ref, (dbPath) => {
|
|
1765
|
+
const db = openReadonlyExistingDatabase(dbPath, { isolatedSnapshot: true });
|
|
1766
|
+
if (!db)
|
|
1767
|
+
throw new Error(`Index database not found at ${dbPath}. Run 'akm index' to build it.`);
|
|
1768
|
+
return db;
|
|
1769
|
+
});
|
|
1770
|
+
if (resolution.indexError !== undefined)
|
|
1771
|
+
throw resolution.indexError;
|
|
1772
|
+
return resolution.entry;
|
|
1773
|
+
}
|
|
1774
|
+
function readLookupEntry(db, id, fallbackConceptId, sourceRoot) {
|
|
1775
|
+
const row = db
|
|
1776
|
+
.prepare("SELECT file_path AS filePath, type, document_json AS documentJson, " +
|
|
1777
|
+
"item_ref AS itemRef, bundle_id AS bundleId, concept_id AS conceptId, " +
|
|
1778
|
+
"adapter_id AS adapterId FROM entries WHERE id = ?")
|
|
1779
|
+
.get(id);
|
|
1780
|
+
if (!row)
|
|
1781
|
+
return null;
|
|
1782
|
+
let document;
|
|
1783
|
+
try {
|
|
1784
|
+
document = JSON.parse(row.documentJson);
|
|
1765
1785
|
}
|
|
1786
|
+
catch {
|
|
1787
|
+
// Corrupt optional projection does not erase the durable path identity.
|
|
1788
|
+
}
|
|
1789
|
+
return {
|
|
1790
|
+
filePath: row.filePath,
|
|
1791
|
+
stashDir: sourceRoot,
|
|
1792
|
+
type: row.type,
|
|
1793
|
+
name: document?.name ?? fallbackConceptId.split("/").pop() ?? fallbackConceptId,
|
|
1794
|
+
adapterId: row.adapterId,
|
|
1795
|
+
document,
|
|
1796
|
+
itemRef: row.itemRef,
|
|
1797
|
+
bundleId: row.bundleId,
|
|
1798
|
+
conceptId: row.conceptId,
|
|
1799
|
+
};
|
|
1766
1800
|
}
|
|
1767
1801
|
/**
|
|
1768
1802
|
* Look up a single asset by ref. Spec §6.2 — `akm show` queries this and
|
|
@@ -1795,11 +1829,16 @@ const USAGE_EVENT_RETENTION_DAYS = 90;
|
|
|
1795
1829
|
*
|
|
1796
1830
|
* Called during `akm index` after FTS rebuild.
|
|
1797
1831
|
*/
|
|
1798
|
-
export function recomputeUtilityScores(db, stateDb) {
|
|
1832
|
+
export function recomputeUtilityScores(db, stateDb, options) {
|
|
1799
1833
|
const EMA_DECAY = 0.7;
|
|
1834
|
+
const stateSchema = options?.stateSchema;
|
|
1835
|
+
if (stateSchema !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(stateSchema)) {
|
|
1836
|
+
throw new Error("Invalid attached state schema name.");
|
|
1837
|
+
}
|
|
1838
|
+
const usageEvents = stateSchema === undefined ? "usage_events" : `"${stateSchema}".usage_events`;
|
|
1800
1839
|
// Purge stale usage events (90-day retention). usage_events lives in state.db
|
|
1801
1840
|
// (Chunk-8 WI-8.3); its table is created by state migration 020.
|
|
1802
|
-
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
|
|
1841
|
+
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS, { stateSchema });
|
|
1803
1842
|
// Time-proportional decay: apply one round of EMA per elapsed day so
|
|
1804
1843
|
// indexing frequency doesn't affect how fast scores decay.
|
|
1805
1844
|
const lastComputedAt = getMeta(db, "last_utility_computed_at");
|
|
@@ -1830,7 +1869,7 @@ export function recomputeUtilityScores(db, stateDb) {
|
|
|
1830
1869
|
ELSE NULL
|
|
1831
1870
|
END
|
|
1832
1871
|
) AS last_used_at
|
|
1833
|
-
FROM
|
|
1872
|
+
FROM ${usageEvents} u
|
|
1834
1873
|
WHERE u.entry_id IS NOT NULL
|
|
1835
1874
|
AND u.source = 'user'
|
|
1836
1875
|
GROUP BY u.entry_id
|