akm-cli 0.9.16 → 0.9.17-alpha.10
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 +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
package/dist/indexer/indexer.js
CHANGED
|
@@ -5,75 +5,46 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
7
7
|
import { adapterForId } from "../core/adapter/registry.js";
|
|
8
|
-
import { isHttpUrl
|
|
9
|
-
import { concurrentMap } from "../core/concurrent.js";
|
|
10
|
-
import { ConfigError } from "../core/errors.js";
|
|
11
|
-
import { defaultConcurrencyForEndpoint } from "../core/loopback.js";
|
|
8
|
+
import { isHttpUrl } from "../core/common.js";
|
|
12
9
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
13
10
|
import { getDbPath } from "../core/paths.js";
|
|
14
11
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
15
12
|
import { withStateDb } from "../core/state-db.js";
|
|
16
13
|
import { isVerbose, warn, warnOnce, warnVerbose } from "../core/warn.js";
|
|
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";
|
|
21
14
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
22
|
-
import { salvageEmbeddingsBeforeDiscard } from "../storage/repositories/embedding-salvage-repository.js";
|
|
23
15
|
import { closeDatabase, openExistingDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
|
|
24
|
-
import {
|
|
25
|
-
import { clearStaleCacheEntries
|
|
26
|
-
import { deleteIndexDirState, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
16
|
+
import { deleteEntriesByBundle, deleteEntriesByDirAndBundle, deleteEntriesByDirExceptRefs, deleteEntriesByIds, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedBundleIdsByDir, getIndexedDirPathsByBundleId, relinkUsageEvents, upsertEntry, } from "../storage/repositories/index-entries-repository.js";
|
|
17
|
+
import { clearStaleCacheEntries } from "../storage/repositories/index-llm-cache-repository.js";
|
|
18
|
+
import { deleteIndexDirState, deleteMeta, getIndexDirState, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
19
|
+
import { VACUUM_PENDING_META } from "../storage/repositories/index-schema.js";
|
|
27
20
|
import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
28
|
-
import { getEmbeddingCount
|
|
21
|
+
import { getEmbeddingCount } from "../storage/repositories/index-vec-repository.js";
|
|
22
|
+
import { INDEX_DB_VACUUMED_EVENT, readFreelistInfo, vacuumIfReclaimable } from "../storage/state-db-integrity.js";
|
|
29
23
|
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
30
|
-
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
31
24
|
import { reclassifyIndexDbContention } from "./index-db-contention.js";
|
|
32
25
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
33
26
|
import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
|
|
34
27
|
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
35
28
|
import { canUseIncrementalSkip, computeDirFingerprint, getCachedDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
|
|
36
|
-
import {
|
|
29
|
+
import { isWorkflowSkipWarning, withFileSize } from "./passes/metadata.js";
|
|
37
30
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
38
|
-
import {
|
|
39
|
-
import { purgeOldUsageEvents } from "./usage/usage-events.js";
|
|
31
|
+
import { purgeOldUsageEvents, USAGE_EVENT_RETENTION_DAYS } from "./usage/usage-events.js";
|
|
40
32
|
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
|
-
}
|
|
51
33
|
function throwIfAborted(signal) {
|
|
52
34
|
if (signal?.aborted) {
|
|
53
35
|
throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
|
|
54
36
|
}
|
|
55
37
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// `defaultConcurrencyForEndpoint`), shared with the embedding pool
|
|
61
|
-
// (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`) and the
|
|
62
|
-
// workflow engine's frozen concurrency default.
|
|
63
|
-
//
|
|
64
|
-
// The explicit-override branch above only fires for callers that put
|
|
65
|
-
// `concurrency` on the connection themselves — `engines.<name>.concurrency`
|
|
66
|
-
// is a valid schema field but `resolveLlmEngineUse` does NOT copy it into
|
|
67
|
-
// the resolved connection, so on the enrichment path the auto-derived 1/2
|
|
68
|
-
// is what runs (see docs/architecture/internals/indexing.md).
|
|
69
|
-
return defaultConcurrencyForEndpoint(llmConfig?.endpoint);
|
|
38
|
+
// ── Source ownership ─────────────────────────────────────────────────────────
|
|
39
|
+
/** Each source's durable bundle component (`deriveInstallations`: one per source, source order). */
|
|
40
|
+
function sourceComponents(sources) {
|
|
41
|
+
return deriveInstallations([...sources]).map((installation) => installation.components[0]);
|
|
70
42
|
}
|
|
71
43
|
function sourceOwners(sources) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
44
|
+
return sourceComponents(sources).map((component, index) => ({
|
|
45
|
+
bundleId: component.id,
|
|
46
|
+
sourceRoot: path.resolve(sources[index].path),
|
|
47
|
+
}));
|
|
77
48
|
}
|
|
78
49
|
function parseStoredSourceOwners(raw) {
|
|
79
50
|
if (!raw)
|
|
@@ -99,102 +70,40 @@ function parseStoredSourceOwners(raw) {
|
|
|
99
70
|
}
|
|
100
71
|
}
|
|
101
72
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
73
|
+
* Sources removed (or moved) since the last complete run. Their entries are
|
|
74
|
+
* purged by {@link applyRemovedSources} once the walk completes.
|
|
104
75
|
*/
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
ctx.removedSources.push({
|
|
114
|
-
...previous,
|
|
115
|
-
removeBundleEntries: current === undefined,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Source caches are hydrated before akmIndex() calls this phase; nothing
|
|
121
|
-
// further to do here. The flag is exposed on ctx for runWalkPhase().
|
|
76
|
+
function findRemovedSources(db, sources) {
|
|
77
|
+
const currentByBundle = new Map(sourceOwners(sources).map((owner) => [owner.bundleId, owner]));
|
|
78
|
+
return parseStoredSourceOwners(getMeta(db, "sourceOwners")).flatMap((previous) => {
|
|
79
|
+
const current = currentByBundle.get(previous.bundleId);
|
|
80
|
+
if (current && current.sourceRoot === previous.sourceRoot)
|
|
81
|
+
return [];
|
|
82
|
+
return [{ ...previous, removeBundleEntries: current === undefined }];
|
|
83
|
+
});
|
|
122
84
|
}
|
|
123
|
-
function applyRemovedSources(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const currentRoots = new Set(sourceOwners(ctx.sources).map((owner) => owner.sourceRoot));
|
|
127
|
-
for (const removed of ctx.removedSources) {
|
|
85
|
+
function applyRemovedSources(db, sources, removedSources, isIncremental) {
|
|
86
|
+
const owners = sourceOwners(sources);
|
|
87
|
+
for (const removed of removedSources) {
|
|
128
88
|
if (removed.removeBundleEntries)
|
|
129
|
-
deleteEntriesByBundle(
|
|
130
|
-
if (!currentRoots.has(removed.sourceRoot))
|
|
131
|
-
deleteStoredGraph(ctx.db, removed.sourceRoot);
|
|
89
|
+
deleteEntriesByBundle(db, removed.bundleId);
|
|
132
90
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full, clean, signal, onProgress, config } = ctx;
|
|
143
|
-
throwIfAborted(signal);
|
|
144
|
-
ctx.timing.tWalkStart = Date.now();
|
|
145
|
-
const doFullDelete = full || !isIncremental;
|
|
146
|
-
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings, complete } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress, !clean, async (dirRecords, ownersByRoot) => {
|
|
147
|
-
const runner = ctx.enrichmentExecution.runner;
|
|
148
|
-
if (runner &&
|
|
149
|
-
isLlmFeatureEnabled(config, "metadata_enhance") &&
|
|
150
|
-
dirRecordsNeedMetadataDispatch(db, dirRecords, ownersByRoot)) {
|
|
151
|
-
ctx.enrichmentLease = await preflightStructuredLlmRunner(runner);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
ctx.scannedDirs = scannedDirs;
|
|
155
|
-
ctx.skippedDirs = skippedDirs;
|
|
156
|
-
ctx.generatedCount = generatedCount;
|
|
157
|
-
ctx.walkWarnings = warnings;
|
|
158
|
-
ctx.dirsNeedingLlm = dirsNeedingLlm;
|
|
159
|
-
ctx.scanComplete = complete;
|
|
160
|
-
onProgress({
|
|
161
|
-
phase: "scan",
|
|
162
|
-
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
163
|
-
});
|
|
164
|
-
// Workflow validation noise gate (issue #273): suppress per-spec stderr lines
|
|
165
|
-
// at default verbosity and emit a single summary instead.
|
|
166
|
-
// In verbose mode the per-spec lines are already printed by
|
|
167
|
-
// buildMetadataSkipWarning at generation time — no second pass needed here.
|
|
168
|
-
if (!isVerbose()) {
|
|
169
|
-
const workflowSkipWarnings = warnings.filter(isWorkflowSkipWarning);
|
|
170
|
-
const skippedWorkflowCount = workflowSkipWarnings.length;
|
|
171
|
-
if (skippedWorkflowCount > 0) {
|
|
172
|
-
const noun = skippedWorkflowCount === 1 ? "workflow spec" : "workflow specs";
|
|
173
|
-
warn(`${skippedWorkflowCount} ${noun} skipped due to validation errors; ` +
|
|
174
|
-
"rerun with --verbose (or AKM_VERBOSE=1) to see details.");
|
|
175
|
-
}
|
|
91
|
+
// A full run re-drains every configured source, so any other bundle's rows
|
|
92
|
+
// are stale even when no stored owner names them.
|
|
93
|
+
if (isIncremental)
|
|
94
|
+
return;
|
|
95
|
+
const currentBundles = new Set(owners.map((owner) => owner.bundleId));
|
|
96
|
+
const indexed = db.prepare("SELECT DISTINCT bundle_id AS bundleId FROM entries").all();
|
|
97
|
+
for (const { bundleId } of indexed) {
|
|
98
|
+
if (!currentBundles.has(bundleId))
|
|
99
|
+
deleteEntriesByBundle(db, bundleId);
|
|
176
100
|
}
|
|
177
|
-
ctx.timing.tWalkEnd = Date.now();
|
|
178
|
-
throwIfAborted(signal);
|
|
179
|
-
// LLM enrichment for directories that need it
|
|
180
|
-
await enhanceDirsWithLlm(db, config, ctx.enrichmentExecution, dirsNeedingLlm, onProgress, signal, (notices) => collectLoweringNotices(ctx.loweringNotices, notices), ctx.enrichmentLease);
|
|
181
|
-
onProgress({
|
|
182
|
-
phase: "llm",
|
|
183
|
-
message: ctx.enrichmentExecution.runner
|
|
184
|
-
? `LLM enhancement reviewed ${dirsNeedingLlm.length} ${dirsNeedingLlm.length === 1 ? "directory" : "directories"}.`
|
|
185
|
-
: "LLM enhancement disabled.",
|
|
186
|
-
});
|
|
187
|
-
ctx.timing.tLlmEnd = Date.now();
|
|
188
101
|
}
|
|
189
102
|
/**
|
|
190
|
-
* The
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* bundle update`'s coordinator calls it directly on its own connection AFTER
|
|
195
|
-
* its unified update transaction commits, since the ambient-transaction drift
|
|
196
|
-
* guard (and the whole point of per-batch commit, #954) requires `db` to have
|
|
197
|
-
* no ambient transaction open.
|
|
103
|
+
* The embedding phase (#954): generate and store vectors for every entry
|
|
104
|
+
* missing one, then compute the `hasEmbeddings` fact and the semantic-search
|
|
105
|
+
* verification off the result. `db` must have no ambient transaction open —
|
|
106
|
+
* each per-batch commit is its own durable write.
|
|
198
107
|
*/
|
|
199
108
|
export async function runEmbeddingPass(params) {
|
|
200
109
|
const { db, config, onProgress, signal, reembed } = params;
|
|
@@ -209,69 +118,24 @@ export async function runEmbeddingPass(params) {
|
|
|
209
118
|
return { embeddingResult, verification };
|
|
210
119
|
}
|
|
211
120
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
async function runEmbeddingPhase(ctx) {
|
|
217
|
-
const { db, config, signal, onProgress, reembed, deferredUpdateTransaction } = ctx;
|
|
218
|
-
throwIfAborted(signal);
|
|
219
|
-
if (deferredUpdateTransaction) {
|
|
220
|
-
// `akm bundle update`'s deferred pass (#954): the embedding
|
|
221
|
-
// phase runs AFTER the coordinator's own commit, on its own connection,
|
|
222
|
-
// via the coordinator's direct `runEmbeddingPass` call — never here,
|
|
223
|
-
// inside the borrowed transaction (the ambient-transaction drift guard
|
|
224
|
-
// would reject it anyway). `runFinalizePhase` records semantic state as
|
|
225
|
-
// "pending".
|
|
226
|
-
ctx.timing.tEmbedEnd = Date.now();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Forward the signal. Without it generateEmbeddingsForDb's abort machinery was
|
|
230
|
-
// inert — its throwIfAborted checks and the signal it threads into embedBatch
|
|
231
|
-
// (which RemoteEmbedder passes to every fetch and LocalEmbedder honours between
|
|
232
|
-
// chunks) never saw a controller. Ctrl-C and the improve budget abort could not
|
|
233
|
-
// stop the embedding phase, the longest phase of an index run.
|
|
234
|
-
const { embeddingResult, verification } = await runEmbeddingPass({ db, config, onProgress, signal, reembed });
|
|
235
|
-
ctx.embeddingResult = embeddingResult;
|
|
236
|
-
ctx.verification = verification;
|
|
237
|
-
ctx.timing.tEmbedEnd = Date.now();
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Finalize phase: confirm transactionally materialized FTS state, re-link
|
|
241
|
-
* usage events, recompute utility scores, update index metadata, and emit the
|
|
242
|
-
* verify event.
|
|
121
|
+
* Finalize: re-link usage events, recompute utility scores, clear stale LLM
|
|
122
|
+
* cache rows and record this run's freshness metadata. `upsertEntry` and every
|
|
123
|
+
* canonical delete own their FTS projection, so the "fts" event is an
|
|
124
|
+
* observation point, not a second materialization pass.
|
|
243
125
|
*/
|
|
244
|
-
|
|
245
|
-
const { db,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// an observation point, not a second materialization pass.
|
|
249
|
-
onProgress({
|
|
250
|
-
phase: "fts",
|
|
251
|
-
message: "Full-text search index is current.",
|
|
252
|
-
});
|
|
253
|
-
ctx.timing.tFtsEnd = Date.now();
|
|
126
|
+
function finalizeIndex(args) {
|
|
127
|
+
const { db, sources, sourceDirs, stashDir, signal, onProgress } = args;
|
|
128
|
+
onProgress({ phase: "fts", message: "Full-text search index is current." });
|
|
129
|
+
const tFtsEnd = Date.now();
|
|
254
130
|
// Re-link state.db usage events to the regenerated index and recompute the
|
|
255
131
|
// derived utility cache. Stored refs already use the current item-ref grammar,
|
|
256
132
|
// so this idempotent pass only restores derived entry ids.
|
|
257
|
-
|
|
133
|
+
withStateDb((stateDb) => {
|
|
258
134
|
onProgress({ phase: "finalize", message: "Relinking usage events." });
|
|
259
|
-
relinkUsageEvents(db, stateDb
|
|
135
|
+
relinkUsageEvents(db, stateDb);
|
|
260
136
|
onProgress({ phase: "finalize", message: "Recomputing utility scores." });
|
|
261
|
-
recomputeUtilityScores(db, stateDb
|
|
262
|
-
};
|
|
263
|
-
if (deferredUpdateTransaction) {
|
|
264
|
-
if (deferredUpdateTransaction.db !== db || !db.inTransaction) {
|
|
265
|
-
throw new Error("Source update index finalization requires its borrowed unified transaction.");
|
|
266
|
-
}
|
|
267
|
-
// state.db is ATTACHed to this same index connection before the outer
|
|
268
|
-
// BEGIN IMMEDIATE. Index and state mutations therefore share one SQLite
|
|
269
|
-
// commit/rollback decision rather than an unsafe two-connection ordering.
|
|
270
|
-
mutateState(db, deferredUpdateTransaction.stateSchema);
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
withStateDb(mutateState);
|
|
274
|
-
}
|
|
137
|
+
recomputeUtilityScores(db, stateDb);
|
|
138
|
+
});
|
|
275
139
|
// Purge LLM cache entries for assets that no longer exist in the index.
|
|
276
140
|
try {
|
|
277
141
|
onProgress({ phase: "finalize", message: "Clearing stale LLM cache entries." });
|
|
@@ -284,41 +148,34 @@ async function runFinalizePhase(ctx) {
|
|
|
284
148
|
// An incomplete run preserves the prior freshness watermark. Advancing it
|
|
285
149
|
// could make a recovered source look unchanged even though this run never
|
|
286
150
|
// persisted its files.
|
|
287
|
-
if (
|
|
151
|
+
if (args.scanComplete) {
|
|
288
152
|
setMeta(db, "builtAt", new Date().toISOString());
|
|
289
153
|
setMeta(db, "stashDir", stashDir);
|
|
290
154
|
setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
|
|
291
155
|
setMeta(db, "sourceOwners", JSON.stringify(sourceOwners(sources)));
|
|
292
156
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
// Non-deferred: ctx.verification was already populated by runEmbeddingPhase
|
|
317
|
-
// (via the shared runEmbeddingPass).
|
|
318
|
-
ctx.totalEntries = totalEntries;
|
|
319
|
-
ctx.timing.tFinalizeEnd = Date.now();
|
|
320
|
-
// suppress unused warning — sources was previously used inline
|
|
321
|
-
void sources;
|
|
157
|
+
return { tFtsEnd };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Compact index.db once a layout migration has left tables and columns'
|
|
161
|
+
* pages free, or whenever more than half its pages are free (the same
|
|
162
|
+
* threshold improve applies to state.db). Nothing else ever VACUUMs this
|
|
163
|
+
* file, and a delete-heavy history leaves it mostly free pages.
|
|
164
|
+
*/
|
|
165
|
+
function vacuumIndexDb(db, onProgress) {
|
|
166
|
+
const migrated = getMeta(db, VACUUM_PENDING_META) === "1";
|
|
167
|
+
const outcome = vacuumIfReclaimable(db, readFreelistInfo(db), {
|
|
168
|
+
eventType: INDEX_DB_VACUUMED_EVENT,
|
|
169
|
+
force: migrated,
|
|
170
|
+
});
|
|
171
|
+
if (!outcome.ran)
|
|
172
|
+
return;
|
|
173
|
+
if (migrated)
|
|
174
|
+
deleteMeta(db, VACUUM_PENDING_META);
|
|
175
|
+
onProgress({
|
|
176
|
+
phase: "finalize",
|
|
177
|
+
message: `Compacted index.db with VACUUM: ${outcome.pagesBefore} → ${outcome.pagesAfter} pages.`,
|
|
178
|
+
});
|
|
322
179
|
}
|
|
323
180
|
// ── Clean pass ───────────────────────────────────────────────────────────────
|
|
324
181
|
/**
|
|
@@ -372,29 +229,12 @@ let akmIndexOverride;
|
|
|
372
229
|
export function _setAkmIndexForTests(fake) {
|
|
373
230
|
akmIndexOverride = fake;
|
|
374
231
|
}
|
|
375
|
-
// Moved to its own module (field follow-up to #956) so
|
|
376
|
-
// `generateEmbeddingsForDb` (materialize-embeddings.ts) can reuse the same
|
|
377
|
-
// classifier without an indexer.ts <-> materialize-embeddings.ts import
|
|
378
|
-
// cycle. Re-exported here for back-compat with existing call sites/tests
|
|
379
|
-
// that import it from `./indexer`. See index-db-contention.ts for the full
|
|
380
|
-
// rationale.
|
|
381
|
-
export { reclassifyIndexDbContention };
|
|
382
232
|
export async function akmIndex(options) {
|
|
383
233
|
try {
|
|
384
234
|
const override = akmIndexOverride;
|
|
385
235
|
return override ? await override(options) : await akmIndexReal(options);
|
|
386
236
|
}
|
|
387
237
|
catch (error) {
|
|
388
|
-
const updateDb = options.deferredUpdateTransaction?.db;
|
|
389
|
-
if (updateDb?.inTransaction) {
|
|
390
|
-
try {
|
|
391
|
-
updateDb.exec("ROLLBACK");
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
// Preserve the indexing error. The update coordinator will retry
|
|
395
|
-
// rollback before closing its borrowed unified handle.
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
238
|
throw reclassifyIndexDbContention(error);
|
|
399
239
|
}
|
|
400
240
|
}
|
|
@@ -402,20 +242,16 @@ let indexTransactionHookForTests;
|
|
|
402
242
|
/**
|
|
403
243
|
* TEST-ONLY. Observe the in-flight reindex transaction; `undefined` restores.
|
|
404
244
|
*
|
|
405
|
-
* Exists because the
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
245
|
+
* Exists because the persist transaction's atomicity is, by construction,
|
|
246
|
+
* invisible from outside it: by the time `akmIndex()` resolves, the commit
|
|
247
|
+
* has already collapsed both generations into one observable state.
|
|
248
|
+
* Concurrency tests install a hook that opens a SECOND connection at this
|
|
249
|
+
* point and asserts it still sees the previous complete generation. Inert in
|
|
250
|
+
* production (one `undefined?.()` per reindex).
|
|
411
251
|
*/
|
|
412
252
|
export function _setIndexTransactionHookForTests(hook) {
|
|
413
253
|
indexTransactionHookForTests = hook;
|
|
414
254
|
}
|
|
415
|
-
/** Fire a named in-transaction observation point (no-op outside tests). */
|
|
416
|
-
function indexTransactionHook(point) {
|
|
417
|
-
indexTransactionHookForTests?.(point);
|
|
418
|
-
}
|
|
419
255
|
let drainObserverForTests;
|
|
420
256
|
/**
|
|
421
257
|
* TEST-ONLY. Observe every directory that actually reaches
|
|
@@ -429,6 +265,18 @@ let drainObserverForTests;
|
|
|
429
265
|
export function _setDrainObserverForTests(observer) {
|
|
430
266
|
drainObserverForTests = observer;
|
|
431
267
|
}
|
|
268
|
+
/** Whether `root` is a directory whose listing can be read (adapter detection only probes such roots). */
|
|
269
|
+
function isReadableDirectory(root) {
|
|
270
|
+
try {
|
|
271
|
+
if (!fs.statSync(root).isDirectory())
|
|
272
|
+
return false;
|
|
273
|
+
fs.readdirSync(root); // probe readability, not just existence
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
432
280
|
/**
|
|
433
281
|
* Detect an adapter for every resolvable source that does not declare one, and
|
|
434
282
|
* persist each detection into `config.json`.
|
|
@@ -440,17 +288,13 @@ export function _setDrainObserverForTests(observer) {
|
|
|
440
288
|
* emitted here. The map is cleared at the top of every callback invocation
|
|
441
289
|
* because `mutateConfig` may retry optimistically, and a retry must not report
|
|
442
290
|
* a superseded attempt.
|
|
443
|
-
*
|
|
444
|
-
* Extracted from `akmIndexReal` as one self-contained named pass, both to keep
|
|
445
|
-
* that function under the src-wide function-size bar and because the detection
|
|
446
|
-
* and its disclosure belong together.
|
|
447
291
|
*/
|
|
448
292
|
function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, opts) {
|
|
449
293
|
const detectedByBundle = new Map();
|
|
450
294
|
for (const source of allSourceEntries) {
|
|
451
295
|
if (source.adapterId || source.unresolved)
|
|
452
296
|
continue;
|
|
453
|
-
if (
|
|
297
|
+
if (isReadableDirectory(source.path)) {
|
|
454
298
|
source.adapterId = detectAdapterId(source.path);
|
|
455
299
|
if (source.registryId)
|
|
456
300
|
detectedByBundle.set(source.registryId, source.adapterId);
|
|
@@ -490,43 +334,11 @@ function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig,
|
|
|
490
334
|
}
|
|
491
335
|
return { config: nextConfig, persistedAdapters };
|
|
492
336
|
}
|
|
493
|
-
function createIndexRunContext(options) {
|
|
494
|
-
const prevStashDir = getMeta(options.db, "stashDir");
|
|
495
|
-
const prevBuiltAt = getMeta(options.db, "builtAt");
|
|
496
|
-
const isIncremental = !options.full && prevStashDir === options.stashDir && !!prevBuiltAt;
|
|
497
|
-
const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
|
|
498
|
-
const { t0, ...context } = options;
|
|
499
|
-
return {
|
|
500
|
-
...context,
|
|
501
|
-
loweringNotices: [...options.enrichmentExecution.notices],
|
|
502
|
-
timing: {
|
|
503
|
-
t0,
|
|
504
|
-
tWalkStart: t0,
|
|
505
|
-
tWalkEnd: t0,
|
|
506
|
-
tLlmEnd: t0,
|
|
507
|
-
tFtsEnd: t0,
|
|
508
|
-
tEmbedEnd: t0,
|
|
509
|
-
tFinalizeStart: t0,
|
|
510
|
-
tFinalizeEnd: t0,
|
|
511
|
-
},
|
|
512
|
-
isIncremental,
|
|
513
|
-
builtAtMs,
|
|
514
|
-
hadRemovedSources: false,
|
|
515
|
-
removedSources: [],
|
|
516
|
-
scanComplete: true,
|
|
517
|
-
scannedDirs: 0,
|
|
518
|
-
skippedDirs: 0,
|
|
519
|
-
generatedCount: 0,
|
|
520
|
-
walkWarnings: [],
|
|
521
|
-
dirsNeedingLlm: [],
|
|
522
|
-
embeddingResult: null,
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
337
|
async function akmIndexReal(options) {
|
|
526
338
|
// R-022: `dryRun` only ever gated the `--clean` stale-entry removal pass
|
|
527
|
-
// (see `runCleanPass` below) — every other phase (walk,
|
|
528
|
-
//
|
|
529
|
-
//
|
|
339
|
+
// (see `runCleanPass` below) — every other phase (walk, embeddings, FTS,
|
|
340
|
+
// the adapter-detection config write) ran for real regardless, so
|
|
341
|
+
// `akm index --dry-run` alone silently performed a full,
|
|
530
342
|
// real index. The flag's own docs (`IndexOptions.dryRun` above, and the
|
|
531
343
|
// CLI help in stash-cli.ts) already scope it to `--clean`; reject the
|
|
532
344
|
// combination that was never implemented instead of quietly doing
|
|
@@ -538,222 +350,185 @@ async function akmIndexReal(options) {
|
|
|
538
350
|
"Pass `akm index --clean --dry-run`, or drop `--dry-run` to run a real index.", "INVALID_FLAG_VALUE", "Run `akm index --clean --dry-run` to preview, or `akm index --clean` to apply.");
|
|
539
351
|
}
|
|
540
352
|
const requestedAt = Date.now();
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
353
|
+
const { stashDir, signal } = options;
|
|
354
|
+
const onProgress = options.onProgress ?? (() => { });
|
|
355
|
+
const full = options.full === true;
|
|
356
|
+
const clean = options.clean === true;
|
|
357
|
+
const dryRun = options.dryRun === true;
|
|
358
|
+
// Load config and resolve all stash sources
|
|
359
|
+
const { loadConfig, mutateConfig } = await import("../core/config/config.js");
|
|
360
|
+
let config = loadConfig();
|
|
361
|
+
// Durable state must be runtime-compatible before source hydration,
|
|
362
|
+
// adapter persistence, or index.db creation can mutate the installation.
|
|
363
|
+
onProgress({ phase: "preflight", message: "Validating durable state." });
|
|
364
|
+
withStateDb(() => undefined);
|
|
365
|
+
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
366
|
+
// so their content directories exist on disk for the walker to discover.
|
|
367
|
+
const sourceCacheStart = Date.now();
|
|
368
|
+
onProgress({ phase: "preflight", message: "Hydrating source caches." });
|
|
369
|
+
const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
|
|
370
|
+
// Inject the store-backed secret resolver from here — a composition root
|
|
371
|
+
// ABOVE the provider/fetcher import cycle (this module reaches
|
|
372
|
+
// search-source only via dynamic import). This is what lets a website
|
|
373
|
+
// source's X fetcher resolve `secrets/x-bearer-token` during
|
|
374
|
+
// bundle-update / hydrate, not just from the command-layer URL-ingest
|
|
375
|
+
// path. `secret-seam` is imported here, never from inside the cycle.
|
|
376
|
+
const { storeSecretResolver } = await import("../sources/snapshot-fetchers/secret-seam.js");
|
|
377
|
+
await ensureSourceCaches(config, {
|
|
378
|
+
force: full,
|
|
379
|
+
materialize: options.hydrateSources !== false,
|
|
380
|
+
secrets: storeSecretResolver,
|
|
381
|
+
// Same progress channel as every other phase (#954) — a
|
|
382
|
+
// stalled clone/fetch here runs BEFORE index.db is even opened, so
|
|
383
|
+
// without this it looked identical to "no database open, nothing
|
|
384
|
+
// written".
|
|
385
|
+
onProgress: (message) => onProgress({ phase: "preflight", message }),
|
|
386
|
+
});
|
|
387
|
+
const sourceCacheEnd = Date.now();
|
|
388
|
+
const sources = resolveSourceEntries(stashDir, config);
|
|
389
|
+
const detected = detectAndPersistBundleAdapters(sources, config, mutateConfig, {
|
|
390
|
+
announce: options.implicit !== true,
|
|
391
|
+
persist: options.persistDetectedAdapters !== false,
|
|
392
|
+
});
|
|
393
|
+
config = detected.config;
|
|
394
|
+
const { persistedAdapters } = detected;
|
|
395
|
+
const sourceDirs = sources.map((s) => s.path);
|
|
396
|
+
onProgress({
|
|
397
|
+
phase: "preflight",
|
|
398
|
+
message: `Resolved ${sourceDirs.length} stash source${sourceDirs.length === 1 ? "" : "s"}.`,
|
|
399
|
+
});
|
|
400
|
+
const t0 = Date.now();
|
|
401
|
+
const dbPath = getDbPath();
|
|
402
|
+
const db = openIndexDatabase(dbPath);
|
|
403
|
+
try {
|
|
404
|
+
// `--full` folds into `isIncremental`: a full run drains every directory
|
|
405
|
+
// through the same diff-persist path as an incremental one.
|
|
406
|
+
const prevStashDir = getMeta(db, "stashDir");
|
|
407
|
+
const prevBuiltAt = getMeta(db, "builtAt");
|
|
408
|
+
const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
|
|
409
|
+
const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
|
|
410
|
+
const mode = isIncremental ? "incremental" : "full";
|
|
411
|
+
onProgress({
|
|
412
|
+
phase: "summary",
|
|
413
|
+
message: buildIndexSummaryMessage({
|
|
414
|
+
mode,
|
|
415
|
+
sourcesCount: sourceDirs.length,
|
|
416
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
417
|
+
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
418
|
+
}),
|
|
584
419
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
420
|
+
const removedSources = findRemovedSources(db, sources);
|
|
421
|
+
// ── Walk: scan the filesystem, generate metadata, persist entries ─────
|
|
422
|
+
throwIfAborted(signal);
|
|
423
|
+
const tWalkStart = Date.now();
|
|
424
|
+
// Walk every source and pre-generate all metadata outside any transaction…
|
|
425
|
+
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, sources, isIncremental, builtAtMs, removedSources.length > 0, onProgress, !clean);
|
|
426
|
+
// …then write it in one transaction: `item_ref = <bundle>//<conceptId>`
|
|
427
|
+
// plus canonical component/adapter provenance per source root.
|
|
428
|
+
const bundleByRoot = buildIndexedSourceOwners(sources);
|
|
429
|
+
persistDirRecords(db, dirRecords, warnings, bundleByRoot);
|
|
588
430
|
onProgress({
|
|
589
|
-
phase: "
|
|
590
|
-
message: `
|
|
431
|
+
phase: "scan",
|
|
432
|
+
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
591
433
|
});
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
434
|
+
// Workflow validation noise gate (issue #273): suppress per-spec stderr
|
|
435
|
+
// lines at default verbosity and emit a single summary instead. In verbose
|
|
436
|
+
// mode the per-spec lines are already printed by buildMetadataSkipWarning.
|
|
437
|
+
if (!isVerbose()) {
|
|
438
|
+
const skippedWorkflowCount = warnings.filter(isWorkflowSkipWarning).length;
|
|
439
|
+
if (skippedWorkflowCount > 0) {
|
|
440
|
+
const noun = skippedWorkflowCount === 1 ? "workflow spec" : "workflow specs";
|
|
441
|
+
warn(`${skippedWorkflowCount} ${noun} skipped due to validation errors; ` +
|
|
442
|
+
"rerun with --verbose (or AKM_VERBOSE=1) to see details.");
|
|
443
|
+
}
|
|
601
444
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
onProgress,
|
|
616
|
-
signal,
|
|
617
|
-
t0,
|
|
618
|
-
deferredUpdateTransaction: options.deferredUpdateTransaction,
|
|
619
|
-
});
|
|
620
|
-
indexRunContext = ctx;
|
|
445
|
+
const tWalkEnd = Date.now();
|
|
446
|
+
// Metadata enhancement (the LLM pass that used to run here) is retired
|
|
447
|
+
// (RS-D, 0.9.17-alpha.9) — see CHANGELOG. `timing.llmMs` stays (always
|
|
448
|
+
// ~0 now) so the JSON shape is unchanged.
|
|
449
|
+
const tLlmEnd = Date.now();
|
|
450
|
+
if (complete)
|
|
451
|
+
applyRemovedSources(db, sources, removedSources, isIncremental);
|
|
452
|
+
// Reconcile explicit missing-file cleanup before embeddings, totals, or
|
|
453
|
+
// verification describe this generation. Dry-run intentionally leaves
|
|
454
|
+
// the generation unchanged while still returning the previewed refs.
|
|
455
|
+
let cleanResult;
|
|
456
|
+
const cleanStart = Date.now();
|
|
457
|
+
if (clean) {
|
|
621
458
|
onProgress({
|
|
622
|
-
phase: "
|
|
623
|
-
message:
|
|
624
|
-
mode: ctx.isIncremental ? "incremental" : "full",
|
|
625
|
-
sourcesCount: allSourceDirs.length,
|
|
626
|
-
semanticSearchMode: config.semanticSearchMode,
|
|
627
|
-
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
628
|
-
llmEnabled: !!enrichmentExecution.runner,
|
|
629
|
-
vecAvailable: isVecAvailable(db),
|
|
630
|
-
}),
|
|
459
|
+
phase: "finalize",
|
|
460
|
+
message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
|
|
631
461
|
});
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
let cleanEnd = cleanStart;
|
|
635
|
-
// ── Phase sequence ───────────────────────────────────────────────────────
|
|
636
|
-
await runSourceCachePhase(ctx);
|
|
637
|
-
await runWalkPhase(ctx);
|
|
638
|
-
applyRemovedSources(ctx);
|
|
639
|
-
// Reconcile explicit missing-file cleanup before embeddings, totals, or
|
|
640
|
-
// verification describe this generation. Dry-run intentionally leaves
|
|
641
|
-
// the generation unchanged while still returning the previewed refs.
|
|
642
|
-
cleanStart = Date.now();
|
|
643
|
-
if (clean) {
|
|
644
|
-
onProgress({
|
|
645
|
-
phase: "finalize",
|
|
646
|
-
message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
|
|
647
|
-
});
|
|
648
|
-
if (ctx.scanComplete) {
|
|
649
|
-
cleanResult = runCleanPass(db, dryRun);
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
warn("[index] --clean skipped because one or more configured sources were not scanned completely.");
|
|
653
|
-
cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
|
|
654
|
-
}
|
|
462
|
+
if (complete) {
|
|
463
|
+
cleanResult = runCleanPass(db, dryRun);
|
|
655
464
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
660
|
-
// runFinalizePhase always populates these before returning.
|
|
661
|
-
const verification = ctx.verification;
|
|
662
|
-
const totalEntries = ctx.totalEntries;
|
|
663
|
-
const { timing } = ctx;
|
|
664
|
-
return {
|
|
665
|
-
stashDir,
|
|
666
|
-
totalEntries,
|
|
667
|
-
generatedMetadata: ctx.generatedCount,
|
|
668
|
-
indexPath: dbPath,
|
|
669
|
-
mode: ctx.isIncremental ? "incremental" : "full",
|
|
670
|
-
directoriesScanned: ctx.scannedDirs,
|
|
671
|
-
directoriesSkipped: ctx.skippedDirs,
|
|
672
|
-
scanComplete: ctx.scanComplete,
|
|
673
|
-
...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
|
|
674
|
-
...(ctx.loweringNotices.length > 0 ? { notices: Object.freeze([...ctx.loweringNotices]) } : {}),
|
|
675
|
-
...(Object.keys(persistedAdapters).length > 0
|
|
676
|
-
? { configUpdated: { detectedAdapters: persistedAdapters } }
|
|
677
|
-
: {}),
|
|
678
|
-
verification,
|
|
679
|
-
timing: {
|
|
680
|
-
totalMs: Date.now() - timing.t0,
|
|
681
|
-
walkMs: timing.tWalkEnd - timing.tWalkStart,
|
|
682
|
-
llmMs: timing.tLlmEnd - timing.tWalkEnd,
|
|
683
|
-
embedMs: timing.tEmbedEnd - timing.tLlmEnd,
|
|
684
|
-
ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
|
|
685
|
-
finalizeMs: timing.tFinalizeEnd - timing.tFinalizeStart,
|
|
686
|
-
cleanMs: clean ? cleanEnd - cleanStart : 0,
|
|
687
|
-
preflightMs: timing.t0 - requestedAt,
|
|
688
|
-
sourceCacheMs: sourceCacheEnd - sourceCacheStart,
|
|
689
|
-
endToEndMs: Date.now() - requestedAt,
|
|
690
|
-
},
|
|
691
|
-
...(cleanResult !== undefined ? { clean: cleanResult } : {}),
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
finally {
|
|
695
|
-
if (indexRunContext?.enrichmentLease) {
|
|
696
|
-
disposeLoweredExecutionDispatchLease(indexRunContext.enrichmentLease);
|
|
465
|
+
else {
|
|
466
|
+
warn("[index] --clean skipped because one or more configured sources were not scanned completely.");
|
|
467
|
+
cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
|
|
697
468
|
}
|
|
698
|
-
if (!borrowedUpdateDb)
|
|
699
|
-
closeDatabase(db);
|
|
700
469
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const component = installation.components[0];
|
|
711
|
-
owners.set(path.resolve(source.path), {
|
|
712
|
-
bundleId: installation.id,
|
|
713
|
-
componentId: component?.id ?? installation.id,
|
|
714
|
-
adapterId: component?.adapter ?? "akm",
|
|
470
|
+
const cleanEnd = Date.now();
|
|
471
|
+
// ── Embeddings ─────────────────────────────────────────────────────────
|
|
472
|
+
throwIfAborted(signal);
|
|
473
|
+
const { verification } = await runEmbeddingPass({
|
|
474
|
+
db,
|
|
475
|
+
config,
|
|
476
|
+
onProgress,
|
|
477
|
+
signal,
|
|
478
|
+
reembed: options.reembed === true,
|
|
715
479
|
});
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
480
|
+
const tEmbedEnd = Date.now();
|
|
481
|
+
// ── Finalize ──────────────────────────────────────────────────────────
|
|
482
|
+
const tFinalizeStart = Date.now();
|
|
483
|
+
const { tFtsEnd } = finalizeIndex({
|
|
484
|
+
db,
|
|
485
|
+
sources,
|
|
486
|
+
sourceDirs,
|
|
487
|
+
stashDir,
|
|
488
|
+
scanComplete: complete,
|
|
489
|
+
signal,
|
|
490
|
+
onProgress,
|
|
491
|
+
});
|
|
492
|
+
vacuumIndexDb(db, onProgress);
|
|
493
|
+
const totalEntries = getEntryCount(db);
|
|
494
|
+
const tFinalizeEnd = Date.now();
|
|
495
|
+
return {
|
|
496
|
+
stashDir,
|
|
497
|
+
totalEntries,
|
|
498
|
+
generatedMetadata: generatedCount,
|
|
499
|
+
indexPath: dbPath,
|
|
500
|
+
mode,
|
|
501
|
+
directoriesScanned: scannedDirs,
|
|
502
|
+
directoriesSkipped: skippedDirs,
|
|
503
|
+
scanComplete: complete,
|
|
504
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
505
|
+
...(Object.keys(persistedAdapters).length > 0 ? { configUpdated: { detectedAdapters: persistedAdapters } } : {}),
|
|
506
|
+
verification,
|
|
507
|
+
timing: {
|
|
508
|
+
totalMs: Date.now() - t0,
|
|
509
|
+
walkMs: tWalkEnd - tWalkStart,
|
|
510
|
+
llmMs: tLlmEnd - tWalkEnd,
|
|
511
|
+
embedMs: tEmbedEnd - tLlmEnd,
|
|
512
|
+
ftsMs: tFtsEnd - tEmbedEnd,
|
|
513
|
+
finalizeMs: tFinalizeEnd - tFinalizeStart,
|
|
514
|
+
cleanMs: clean ? cleanEnd - cleanStart : 0,
|
|
515
|
+
preflightMs: t0 - requestedAt,
|
|
516
|
+
sourceCacheMs: sourceCacheEnd - sourceCacheStart,
|
|
517
|
+
endToEndMs: Date.now() - requestedAt,
|
|
518
|
+
},
|
|
519
|
+
...(cleanResult !== undefined ? { clean: cleanResult } : {}),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
closeDatabase(db);
|
|
755
524
|
}
|
|
756
|
-
|
|
525
|
+
}
|
|
526
|
+
function buildIndexedSourceOwners(sources) {
|
|
527
|
+
const components = sourceComponents(sources);
|
|
528
|
+
return new Map(sources.map((source, index) => {
|
|
529
|
+
const { id, adapter } = components[index];
|
|
530
|
+
return [path.resolve(source.path), { bundleId: id, componentId: id, adapterId: adapter }];
|
|
531
|
+
}));
|
|
757
532
|
}
|
|
758
533
|
function removalsFirst(records) {
|
|
759
534
|
return [...records.filter((record) => record.remove), ...records.filter((record) => !record.remove)];
|
|
@@ -762,32 +537,6 @@ function addEntryIds(target, ids) {
|
|
|
762
537
|
for (const id of ids)
|
|
763
538
|
target.add(id);
|
|
764
539
|
}
|
|
765
|
-
/**
|
|
766
|
-
* Map each source root → its durable `BundleComponent` (`deriveInstallations`,
|
|
767
|
-
* batch-unique bundle ids, source order preserved). The per-dir document drain
|
|
768
|
-
* dispatches `adapterForId(component.adapter).recognize` for this component. The
|
|
769
|
-
* component id only surfaces on `IndexDocument.ref`, which the persist layer
|
|
770
|
-
* re-derives independently — so a source missing from the map (never happens: the
|
|
771
|
-
* map is built from the same sources) is harmless.
|
|
772
|
-
*/
|
|
773
|
-
function buildComponentBySource(sources) {
|
|
774
|
-
const map = new Map();
|
|
775
|
-
const installations = deriveInstallations(sources);
|
|
776
|
-
sources.forEach((source, i) => {
|
|
777
|
-
const component = installations[i]?.components[0];
|
|
778
|
-
if (component)
|
|
779
|
-
map.set(source.path, component);
|
|
780
|
-
});
|
|
781
|
-
return map;
|
|
782
|
-
}
|
|
783
|
-
function componentForSource(components, sourcePath) {
|
|
784
|
-
return (components.get(sourcePath) ?? {
|
|
785
|
-
id: sourcePath,
|
|
786
|
-
adapter: "akm",
|
|
787
|
-
root: sourcePath,
|
|
788
|
-
writable: false,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
540
|
function groupFileContextsByDir(fileContexts) {
|
|
792
541
|
const groups = new Map();
|
|
793
542
|
for (const ctx of fileContexts) {
|
|
@@ -876,17 +625,19 @@ function warnIfAdapterSkipsAkmContent(component, files, adapter) {
|
|
|
876
625
|
`${skippedDirs.map((dir) => `${dir}/`).join(", ")} — set components.<name>.adapter to "akm" to index them`);
|
|
877
626
|
}
|
|
878
627
|
function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs) {
|
|
879
|
-
|
|
628
|
+
// Each source root → its durable `BundleComponent`; the per-dir document
|
|
629
|
+
// drain dispatches `adapterForId(component.adapter).recognize` for it.
|
|
630
|
+
const components = sourceComponents(allSourceEntries);
|
|
631
|
+
const componentBySource = new Map(allSourceEntries.map((source, index) => [source.path, components[index]]));
|
|
880
632
|
const handoffDirs = new Set();
|
|
881
633
|
const plans = allSourceEntries.map((sourceAdded) => {
|
|
882
634
|
const currentStashDir = sourceAdded.path;
|
|
883
|
-
const component =
|
|
635
|
+
const component = componentBySource.get(currentStashDir);
|
|
884
636
|
if (sourceAdded.unresolved) {
|
|
885
637
|
return {
|
|
886
638
|
currentStashDir,
|
|
887
639
|
component,
|
|
888
640
|
adapter: undefined,
|
|
889
|
-
indexVariant: undefined,
|
|
890
641
|
dirGroups: new Map(),
|
|
891
642
|
removals: [],
|
|
892
643
|
walkComplete: false,
|
|
@@ -906,7 +657,6 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
906
657
|
currentStashDir,
|
|
907
658
|
component,
|
|
908
659
|
adapter,
|
|
909
|
-
indexVariant: adapter ? `${adapter.id}@${adapter.version}` : undefined,
|
|
910
660
|
dirGroups,
|
|
911
661
|
removals: [],
|
|
912
662
|
walkComplete: walked.complete,
|
|
@@ -931,9 +681,10 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
931
681
|
handoffDirs.add(resolvedDir);
|
|
932
682
|
};
|
|
933
683
|
const allComplete = plans.every((plan) => plan.walkComplete && plan.adapter !== undefined);
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
|
|
684
|
+
// Reconcile departed directories for every source that produced a
|
|
685
|
+
// trustworthy snapshot; a source that was not walked completely keeps its
|
|
686
|
+
// last-known-good rows.
|
|
687
|
+
if (reconcileMissingDirs) {
|
|
937
688
|
const allIndexedDirsByBundle = !isIncremental ? new Map() : undefined;
|
|
938
689
|
if (allIndexedDirsByBundle) {
|
|
939
690
|
for (const entry of getAllEntries(db)) {
|
|
@@ -986,9 +737,6 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
986
737
|
* outside `db.transaction()` so the persist pass can be a single synchronous
|
|
987
738
|
* transaction.
|
|
988
739
|
*/
|
|
989
|
-
function reportSourceScanProgress(onProgress, processed, total, message) {
|
|
990
|
-
onProgress?.({ phase: "scan", message, processed, total });
|
|
991
|
-
}
|
|
992
740
|
async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs = true) {
|
|
993
741
|
let scannedDirs = 0;
|
|
994
742
|
let skippedDirs = 0;
|
|
@@ -999,7 +747,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
999
747
|
const dirRecords = [];
|
|
1000
748
|
let processedDirs = 0;
|
|
1001
749
|
let priorDirsChanged = hadRemovedSources;
|
|
1002
|
-
const reportScanProgress = (message) =>
|
|
750
|
+
const reportScanProgress = (message) => onProgress?.({ phase: "scan", message, processed: processedDirs, total: allSourceEntries.length });
|
|
1003
751
|
const reportDirDecision = (kind, dirPath, currentStashDir, reason, persistedRowCount) => {
|
|
1004
752
|
if (!isVerbose())
|
|
1005
753
|
return;
|
|
@@ -1020,46 +768,6 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
1020
768
|
seenPaths.add(resolved);
|
|
1021
769
|
return false;
|
|
1022
770
|
};
|
|
1023
|
-
// Incremental freshness gate shared by both branches: consult the persisted
|
|
1024
|
-
// dir state and record either a skip (unchanged + eligible for incremental
|
|
1025
|
-
// skip) or a scan record carrying the candidate stash.
|
|
1026
|
-
const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, fingerprint, stash, hashByFile, conceptIdByFile, indexVariant, forceScan, pruneMissing) => {
|
|
1027
|
-
const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs, indexVariant, fingerprint);
|
|
1028
|
-
if (isIncremental && !forceScan && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
1029
|
-
skippedDirs++;
|
|
1030
|
-
dirRecords.push({
|
|
1031
|
-
dirPath,
|
|
1032
|
-
currentStashDir,
|
|
1033
|
-
files: stateFiles,
|
|
1034
|
-
fingerprint,
|
|
1035
|
-
stash: null,
|
|
1036
|
-
skip: true,
|
|
1037
|
-
reason: previousState.reason,
|
|
1038
|
-
persistedRowCount: previousState.persistedRowCount,
|
|
1039
|
-
indexVariant,
|
|
1040
|
-
});
|
|
1041
|
-
reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
scannedDirs++;
|
|
1045
|
-
priorDirsChanged = true;
|
|
1046
|
-
const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
|
|
1047
|
-
dirRecords.push({
|
|
1048
|
-
dirPath,
|
|
1049
|
-
currentStashDir,
|
|
1050
|
-
files: stateFiles,
|
|
1051
|
-
fingerprint,
|
|
1052
|
-
stash,
|
|
1053
|
-
skip: false,
|
|
1054
|
-
reason,
|
|
1055
|
-
persistedRowCount: previousState.persistedRowCount,
|
|
1056
|
-
hashByFile,
|
|
1057
|
-
conceptIdByFile,
|
|
1058
|
-
indexVariant,
|
|
1059
|
-
pruneMissing,
|
|
1060
|
-
});
|
|
1061
|
-
reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
|
|
1062
|
-
};
|
|
1063
771
|
for (const plan of plans) {
|
|
1064
772
|
const { currentStashDir, component, adapter, dirGroups, removals, walkComplete } = plan;
|
|
1065
773
|
processedDirs++;
|
|
@@ -1079,7 +787,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
1079
787
|
warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
|
|
1080
788
|
continue;
|
|
1081
789
|
}
|
|
1082
|
-
const indexVariant =
|
|
790
|
+
const indexVariant = `${adapter.id}@${adapter.version}`;
|
|
1083
791
|
for (const removal of removals) {
|
|
1084
792
|
dirRecords.push(removal);
|
|
1085
793
|
scannedDirs++;
|
|
@@ -1129,17 +837,53 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
1129
837
|
const drained = drainDirDocuments(adapter, component, ctxs);
|
|
1130
838
|
if (drained.warnings.length)
|
|
1131
839
|
warnings.push(...drained.warnings);
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
840
|
+
generatedCount += drained.entries.length;
|
|
841
|
+
const stash = drained.entries.length > 0 ? { entries: drained.entries } : null;
|
|
842
|
+
// Freshness is judged over the files the entries came from (every walked
|
|
843
|
+
// file when none was recognized).
|
|
844
|
+
const entryFiles = new Set(drained.entries.flatMap((entry) => (entry.filename ? [path.join(dirPath, entry.filename)] : [])));
|
|
845
|
+
const staleFiles = entryFiles.size > 0 ? [...entryFiles] : indexableFiles;
|
|
846
|
+
// Incremental freshness gate: consult the persisted dir state and record
|
|
847
|
+
// either a skip (unchanged + eligible) or a scan carrying the drained stash.
|
|
848
|
+
const previousState = getDirIndexState(db, dirPath, staleFiles, builtAtMs, indexVariant, fingerprint);
|
|
849
|
+
const { persistedRowCount } = previousState;
|
|
850
|
+
if (isIncremental &&
|
|
851
|
+
!forceScan &&
|
|
852
|
+
!previousState.stale &&
|
|
853
|
+
canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
854
|
+
skippedDirs++;
|
|
855
|
+
const reason = previousState.reason;
|
|
856
|
+
dirRecords.push({
|
|
857
|
+
dirPath,
|
|
858
|
+
currentStashDir,
|
|
859
|
+
files: staleFiles,
|
|
860
|
+
fingerprint,
|
|
861
|
+
stash: null,
|
|
862
|
+
skip: true,
|
|
863
|
+
reason,
|
|
864
|
+
persistedRowCount,
|
|
865
|
+
indexVariant,
|
|
866
|
+
});
|
|
867
|
+
reportDirDecision("skip", dirPath, currentStashDir, reason, persistedRowCount);
|
|
868
|
+
continue;
|
|
1141
869
|
}
|
|
1142
|
-
|
|
870
|
+
scannedDirs++;
|
|
871
|
+
priorDirsChanged = true;
|
|
872
|
+
const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
|
|
873
|
+
dirRecords.push({
|
|
874
|
+
dirPath,
|
|
875
|
+
currentStashDir,
|
|
876
|
+
files: staleFiles,
|
|
877
|
+
fingerprint,
|
|
878
|
+
stash,
|
|
879
|
+
skip: false,
|
|
880
|
+
reason,
|
|
881
|
+
persistedRowCount,
|
|
882
|
+
hashByFile: drained.hashByFile,
|
|
883
|
+
conceptIdByFile: drained.conceptIdByFile,
|
|
884
|
+
indexVariant,
|
|
885
|
+
});
|
|
886
|
+
reportDirDecision("scan", dirPath, currentStashDir, reason, persistedRowCount);
|
|
1143
887
|
}
|
|
1144
888
|
}
|
|
1145
889
|
return {
|
|
@@ -1161,90 +905,17 @@ function requiresWorkflowSourcePreflight(ctxs) {
|
|
|
1161
905
|
}
|
|
1162
906
|
});
|
|
1163
907
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
return false;
|
|
1167
|
-
const incomingDocCount = dirRecords.reduce((n, record) => n + (record.skip ? 0 : (record.stash?.entries.length ?? 0)), 0);
|
|
1168
|
-
if (incomingDocCount > 0 || allSourceRootsReadable(sourceRoots))
|
|
1169
|
-
return false;
|
|
1170
|
-
warn("[index] --full produced zero documents while one or more source roots are missing or unreadable — " +
|
|
1171
|
-
"preserving the existing index (last-known-good) rather than wiping it. Re-run once the sources are available.");
|
|
1172
|
-
return true;
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
* #624-P1 zero-document preflight probe. A source root counts as "readable"
|
|
1176
|
-
* when it exists on disk as a directory whose listing can be read. A root that
|
|
1177
|
-
* is missing or unreadable (a transient mount failure, a permission race, or a
|
|
1178
|
-
* source that vanished mid-run) makes a zero-document scan untrustworthy: the
|
|
1179
|
-
* walk saw nothing not because the stash is empty but because it could not be
|
|
1180
|
-
* read. Returns true only when EVERY root is readable, so a single unreadable
|
|
1181
|
-
* root blocks the full-rebuild wipe.
|
|
1182
|
-
*/
|
|
1183
|
-
function allSourceRootsReadable(roots) {
|
|
1184
|
-
for (const root of roots) {
|
|
1185
|
-
try {
|
|
1186
|
-
const st = fs.statSync(root);
|
|
1187
|
-
if (!st.isDirectory())
|
|
1188
|
-
return false;
|
|
1189
|
-
fs.readdirSync(root); // probe readability, not just existence
|
|
1190
|
-
}
|
|
1191
|
-
catch {
|
|
1192
|
-
return false;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
return true;
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Phase 2 (sync): write all pre-generated scan records inside a single
|
|
1199
|
-
* transaction, returning the directories that still need LLM enrichment.
|
|
1200
|
-
*/
|
|
1201
|
-
function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, scanComplete, bundleByRoot) {
|
|
1202
|
-
const dirsNeedingLlm = [];
|
|
1203
|
-
const fullDelete = doFullDelete && scanComplete;
|
|
1204
|
-
// #624-P1 zero-document preflight (spec §4). A full-rebuild wipe is a
|
|
1205
|
-
// legitimate mass-delete ONLY when the scan legitimately found nothing. If
|
|
1206
|
-
// the walk produced zero documents AND any configured source root is missing
|
|
1207
|
-
// or unreadable, the empty result is almost certainly a transient scan
|
|
1208
|
-
// failure, not an emptied stash — wiping here would cascade-destroy the
|
|
1209
|
-
// last-known-good index (entries + embeddings + utility/usage). Preserve it
|
|
1210
|
-
// and warn instead; the next successful run reconciles. A genuinely empty
|
|
1211
|
-
// stash whose roots ARE readable still wipes, as before.
|
|
1212
|
-
if (preserveExistingIndex(fullDelete, dirRecords, sourceRoots))
|
|
1213
|
-
return { dirsNeedingLlm };
|
|
908
|
+
/** Phase 2 (sync): write all pre-generated scan records inside a single transaction. */
|
|
909
|
+
function persistDirRecords(db, dirRecords, warnings, bundleByRoot) {
|
|
1214
910
|
// Per-source dedup: the same logical asset can appear more than once within
|
|
1215
911
|
// one owning source, where source order still makes the first occurrence win.
|
|
1216
912
|
// The owner is part of the key so identical concepts in different bundles
|
|
1217
913
|
// remain distinct indexed rows.
|
|
1218
914
|
const indexedAssetIdentities = new Set();
|
|
1219
915
|
const deletedUsageEntryIds = new Set();
|
|
916
|
+
const findPersisted = db.prepare("SELECT id, content_hash, file_path, adapter_id FROM entries WHERE item_ref = ?");
|
|
1220
917
|
const insertTransaction = db.transaction(() => {
|
|
1221
|
-
|
|
1222
|
-
// transaction so delete and re-insert are atomic — a concurrent reader
|
|
1223
|
-
// never observes an empty database between the two operations.
|
|
1224
|
-
if (fullDelete) {
|
|
1225
|
-
// #955: copy every (search_text hash, embedding) pair about to be
|
|
1226
|
-
// discarded wholesale into `embedding_salvage`, tagged with the
|
|
1227
|
-
// fingerprint the discarded vectors were generated under, BEFORE the
|
|
1228
|
-
// wipe below — inside the SAME transaction so the copy and the
|
|
1229
|
-
// discard commit or roll back together. The embedding phase later in
|
|
1230
|
-
// this run hands salvaged vectors back to unchanged content instead
|
|
1231
|
-
// of re-embedding the whole corpus.
|
|
1232
|
-
salvageEmbeddingsBeforeDiscard(db);
|
|
1233
|
-
// Entries and every child materialization share one deletion authority.
|
|
1234
|
-
// Usage events live in state.db and survive so finalize can relink them
|
|
1235
|
-
// to the replacement generation's row ids.
|
|
1236
|
-
deleteAllEntries(db, { cleanupUsageEvents: false });
|
|
1237
|
-
db.exec("DELETE FROM index_dir_state");
|
|
1238
|
-
// Chunk-8 WI-8.3: usage_events lives in state.db now (not index.db), so the
|
|
1239
|
-
// wipe no longer detaches it here. The finalize pass's relinkUsageEvents
|
|
1240
|
-
// (cross-DB) nulls entry_ids that no longer resolve to a rebuilt entry and
|
|
1241
|
-
// re-resolves the rest by entry_ref — subsuming the old detach.
|
|
1242
|
-
// Atomicity observation point: inside the transaction the tables are now
|
|
1243
|
-
// empty, but no other connection may observe that. See
|
|
1244
|
-
// tests/integration/indexer/reindex-generation-atomicity.test.ts.
|
|
1245
|
-
indexTransactionHook("full-delete-applied");
|
|
1246
|
-
}
|
|
1247
|
-
for (const { dirPath, currentStashDir, files, fingerprint, stash, skip, reason, persistedRowCount, hashByFile, conceptIdByFile, indexVariant, remove, pruneMissing, } of dirRecords) {
|
|
918
|
+
for (const { dirPath, currentStashDir, files, fingerprint, stash, skip, reason, persistedRowCount, hashByFile, conceptIdByFile, indexVariant, remove, } of dirRecords) {
|
|
1248
919
|
const bundle = bundleByRoot.get(path.resolve(currentStashDir));
|
|
1249
920
|
if (!bundle)
|
|
1250
921
|
throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
|
|
@@ -1261,7 +932,13 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1261
932
|
// row_count and the gate skips this directory before draining next
|
|
1262
933
|
// time. "unchanged-precheck" already matched the stored row.
|
|
1263
934
|
if (reason?.kind === "unchanged" && fingerprint) {
|
|
1264
|
-
upsertIndexDirState(db, {
|
|
935
|
+
upsertIndexDirState(db, {
|
|
936
|
+
dirPath,
|
|
937
|
+
...fingerprint,
|
|
938
|
+
reason: reason.kind,
|
|
939
|
+
rowCount: persistedRowCount,
|
|
940
|
+
indexVariant,
|
|
941
|
+
});
|
|
1265
942
|
}
|
|
1266
943
|
continue;
|
|
1267
944
|
}
|
|
@@ -1271,6 +948,16 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1271
948
|
// durable `item_ref`, then prune only the departed rows below. Replaces the old
|
|
1272
949
|
// `deleteEntriesByDir` truncate-and-reinsert (which discarded ids).
|
|
1273
950
|
const keptItemRefs = new Set();
|
|
951
|
+
// Per-file cursor: a directory is drained whole (one changed file
|
|
952
|
+
// re-reads its siblings), but on an incremental run a sibling whose
|
|
953
|
+
// content hash, path and adapter are unchanged since the last drain
|
|
954
|
+
// under the same adapter variant is already persisted exactly as this
|
|
955
|
+
// drain would persist it — including any metadata layered onto its row
|
|
956
|
+
// — so it is neither rewritten nor re-persisted. `--full` re-persists
|
|
957
|
+
// every entry.
|
|
958
|
+
const sameVariant = reason?.kind !== "full-rebuild" &&
|
|
959
|
+
indexVariant !== undefined &&
|
|
960
|
+
getIndexDirState(db, dirPath)?.indexVariant === indexVariant;
|
|
1274
961
|
let persistedRows = 0;
|
|
1275
962
|
let dedupedRows = 0;
|
|
1276
963
|
if (stash) {
|
|
@@ -1294,29 +981,32 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1294
981
|
continue;
|
|
1295
982
|
}
|
|
1296
983
|
indexedAssetIdentities.add(identityKey);
|
|
1297
|
-
const searchText = buildSearchText(entry);
|
|
1298
|
-
const entryWithSize = attachFileSize(entry, entryPath);
|
|
1299
984
|
// content_hash = doc.hash from the drain, keyed by the recognized
|
|
1300
985
|
// file's path. A missing hash preserves the existing value on upsert.
|
|
1301
986
|
const contentHash = hashByFile?.get(entryPath);
|
|
1302
987
|
const provenance = deriveEntryProvenance(bundle, entry.type, entry.name, adapterConceptId);
|
|
1303
988
|
keptItemRefs.add(provenance.itemRef);
|
|
1304
|
-
upsertEntry(db, entryPath, entryWithSize, searchText, provenance, contentHash);
|
|
1305
989
|
persistedRows++;
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
990
|
+
const previous = sameVariant
|
|
991
|
+
? (findPersisted.get(provenance.itemRef) ?? undefined)
|
|
992
|
+
: undefined;
|
|
993
|
+
const unchanged = previous !== undefined &&
|
|
994
|
+
contentHash !== undefined &&
|
|
995
|
+
previous.content_hash === contentHash &&
|
|
996
|
+
previous.file_path === entryPath &&
|
|
997
|
+
previous.adapter_id === bundle.adapterId;
|
|
998
|
+
if (unchanged) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance, contentHash);
|
|
1311
1002
|
}
|
|
1312
1003
|
}
|
|
1313
1004
|
// Prune the departed rows: everything under this dir NOT re-upserted above
|
|
1314
1005
|
// (files deleted, deduped away, or abstained on by the adapter). With
|
|
1315
1006
|
// an empty kept-set this deletes every row for the dir — the exact net
|
|
1316
1007
|
// effect of the old unconditional `deleteEntriesByDir`, minus the id churn.
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1008
|
+
// (A source whose walk was incomplete never reaches this point.)
|
|
1009
|
+
addEntryIds(deletedUsageEntryIds, deleteEntriesByDirExceptRefs(db, dirPath, bundle.bundleId, keptItemRefs, { cleanupUsageEvents: false }));
|
|
1320
1010
|
const persistedFingerprint = fingerprint ?? computeDirFingerprint(dirPath, files, indexVariant);
|
|
1321
1011
|
const persistedReason = persistedRows === 0
|
|
1322
1012
|
? inferZeroRowReason(stash, reason, warnings, dirPath, dedupedRows)
|
|
@@ -1332,6 +1022,7 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1332
1022
|
// must keep draining every run (as it did before the gate) until a
|
|
1333
1023
|
// drain persists it without dedup. NULL keeps the gate closed.
|
|
1334
1024
|
rowCount: dedupedRows === 0 ? persistedRows : undefined,
|
|
1025
|
+
indexVariant,
|
|
1335
1026
|
});
|
|
1336
1027
|
if (persistedRows === 0) {
|
|
1337
1028
|
// Warn only when the dir had files that *could* produce entries (.md or
|
|
@@ -1352,457 +1043,48 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1352
1043
|
}
|
|
1353
1044
|
// Atomicity observation point: the new generation is fully written but
|
|
1354
1045
|
// uncommitted, so it must still be invisible to other connections.
|
|
1355
|
-
|
|
1046
|
+
indexTransactionHookForTests?.("records-persisted");
|
|
1356
1047
|
});
|
|
1357
1048
|
insertTransaction();
|
|
1358
1049
|
deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
|
|
1359
|
-
return { dirsNeedingLlm };
|
|
1360
|
-
}
|
|
1361
|
-
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress, reconcileMissingDirs = true, beforePersist) {
|
|
1362
|
-
// Phase 1 (async): walk directories and pre-generate all metadata outside the
|
|
1363
|
-
// transaction.
|
|
1364
|
-
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs);
|
|
1365
|
-
const bundleByRoot = buildIndexedSourceOwners(allSourceEntries);
|
|
1366
|
-
await beforePersist?.(dirRecords, bundleByRoot);
|
|
1367
|
-
// Phase 2 (sync): write all pre-generated metadata inside a single transaction.
|
|
1368
|
-
// Source roots feed the #624-P1 zero-document preflight (a full-rebuild wipe
|
|
1369
|
-
// is suppressed when the scan is empty because roots are unreadable).
|
|
1370
|
-
const sourceRoots = allSourceEntries.map((s) => s.path);
|
|
1371
|
-
// Map each source root → its durable bundle id so the writer can persist
|
|
1372
|
-
// `item_ref = <bundle>//<conceptId>` and canonical component/adapter
|
|
1373
|
-
// provenance. `deriveInstallations`
|
|
1374
|
-
// preserves source order, so a positional zip yields the SAME bundle id the
|
|
1375
|
-
// dispatched `adapter.recognize` emits as `IndexDocument.ref` for that root.
|
|
1376
|
-
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, complete, bundleByRoot);
|
|
1377
|
-
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm, complete };
|
|
1378
|
-
}
|
|
1379
|
-
function indexedProvenanceForFile(db, filePath) {
|
|
1380
|
-
const row = db
|
|
1381
|
-
.prepare("SELECT item_ref AS itemRef, bundle_id AS bundleId, component_id AS componentId, " +
|
|
1382
|
-
"concept_id AS conceptId, adapter_id AS adapterId FROM entries WHERE file_path = ? LIMIT 1")
|
|
1383
|
-
.get(filePath);
|
|
1384
|
-
if (!row?.itemRef || !row.bundleId || !row.componentId || !row.conceptId || !row.adapterId) {
|
|
1385
|
-
throw new Error(`Missing indexed provenance for ${filePath}`);
|
|
1386
|
-
}
|
|
1387
|
-
return {
|
|
1388
|
-
itemRef: row.itemRef,
|
|
1389
|
-
bundleId: row.bundleId,
|
|
1390
|
-
componentId: row.componentId,
|
|
1391
|
-
conceptId: row.conceptId,
|
|
1392
|
-
adapterId: row.adapterId,
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgress, signal, onNotices, lease) {
|
|
1396
|
-
// The invocation owns one frozen symbolic selection. Summary reporting and
|
|
1397
|
-
// every enrichment dispatch consume this same snapshot.
|
|
1398
|
-
const llmRunner = execution.runner;
|
|
1399
|
-
if (!llmRunner || dirsNeedingLlm.length === 0)
|
|
1400
|
-
return;
|
|
1401
|
-
// Aggregate per-entry failures so a misconfigured LLM endpoint surfaces
|
|
1402
|
-
// as a single visible warning instead of silently degrading every entry
|
|
1403
|
-
// and leaving the user wondering why nothing got enhanced.
|
|
1404
|
-
const summary = { attempted: 0, succeeded: 0, skipped: 0, failureSamples: [] };
|
|
1405
|
-
let completedDirs = 0;
|
|
1406
|
-
let completedEntries = 0;
|
|
1407
|
-
const totalDirs = dirsNeedingLlm.length;
|
|
1408
|
-
const totalEntries = dirsNeedingLlm.reduce((sum, { stash }) => {
|
|
1409
|
-
const entriesToEnhance = stash.entries.filter((e) => {
|
|
1410
|
-
if (e.quality !== "generated")
|
|
1411
|
-
return false;
|
|
1412
|
-
if (isEnrichmentComplete(e))
|
|
1413
|
-
return false;
|
|
1414
|
-
return true;
|
|
1415
|
-
});
|
|
1416
|
-
return sum + entriesToEnhance.length;
|
|
1417
|
-
}, 0);
|
|
1418
|
-
// P3 — wall-clock budget for the enrichment pass. Defaults to the resolved
|
|
1419
|
-
// engine's timeoutMs (or 10 minutes if not set). Users can extend it via
|
|
1420
|
-
// `index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`, or the
|
|
1421
|
-
// engine's own `engines.<name>.timeoutMs`) — no separate knob needed.
|
|
1422
|
-
const enrichDeadline = createEnrichmentDeadline(llmRunner.timeoutMs, totalEntries);
|
|
1423
|
-
let deadlineHit = false;
|
|
1424
|
-
const enrichSignal = (() => {
|
|
1425
|
-
if (!enrichDeadline)
|
|
1426
|
-
return signal ?? new AbortController().signal;
|
|
1427
|
-
if (!signal)
|
|
1428
|
-
return enrichDeadline;
|
|
1429
|
-
// Combine: abort when either fires.
|
|
1430
|
-
const controller = new AbortController();
|
|
1431
|
-
const onAbort = () => controller.abort();
|
|
1432
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1433
|
-
enrichDeadline.addEventListener("abort", () => {
|
|
1434
|
-
deadlineHit = true;
|
|
1435
|
-
controller.abort();
|
|
1436
|
-
}, { once: true });
|
|
1437
|
-
return controller.signal;
|
|
1438
|
-
})();
|
|
1439
|
-
if (totalEntries > 0) {
|
|
1440
|
-
onProgress?.({
|
|
1441
|
-
phase: "llm",
|
|
1442
|
-
message: `LLM enhancement starting for ${totalEntries} entr${totalEntries === 1 ? "y" : "ies"} ` +
|
|
1443
|
-
`across ${totalDirs} director${totalDirs === 1 ? "y" : "ies"} (concurrency ${getDefaultLlmConcurrency(llmRunner.connection)}).`,
|
|
1444
|
-
processed: 0,
|
|
1445
|
-
total: totalEntries,
|
|
1446
|
-
});
|
|
1447
|
-
}
|
|
1448
|
-
let currentDirLabel;
|
|
1449
|
-
let configFailure;
|
|
1450
|
-
let lastProgressAt = Date.now();
|
|
1451
|
-
let heartbeatTimer;
|
|
1452
|
-
if (totalEntries > 0 && onProgress) {
|
|
1453
|
-
heartbeatTimer = setInterval(() => {
|
|
1454
|
-
if (Date.now() - lastProgressAt < 15000)
|
|
1455
|
-
return;
|
|
1456
|
-
onProgress({
|
|
1457
|
-
phase: "llm",
|
|
1458
|
-
message: `Still enriching ${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"}` +
|
|
1459
|
-
(currentDirLabel ? `; waiting on ${currentDirLabel}` : "") +
|
|
1460
|
-
".",
|
|
1461
|
-
processed: completedEntries,
|
|
1462
|
-
total: totalEntries,
|
|
1463
|
-
});
|
|
1464
|
-
lastProgressAt = Date.now();
|
|
1465
|
-
}, 15000);
|
|
1466
|
-
}
|
|
1467
|
-
try {
|
|
1468
|
-
await concurrentMap(dirsNeedingLlm, async ({ dirPath, files, currentStashDir, stash: originalStash }) => {
|
|
1469
|
-
if (enrichSignal.aborted)
|
|
1470
|
-
return undefined;
|
|
1471
|
-
// Only enhance generated entries; user-provided overrides should not
|
|
1472
|
-
// be overwritten. Skip entries that are already fully enriched
|
|
1473
|
-
// (description + tags + searchHints).
|
|
1474
|
-
const entriesToEnhance = originalStash.entries.filter((e) => {
|
|
1475
|
-
if (e.quality !== "generated")
|
|
1476
|
-
return false;
|
|
1477
|
-
if (isEnrichmentComplete(e)) {
|
|
1478
|
-
warnVerbose(`[akm] skipping LLM enrichment for "${e.name}" — entry already complete`);
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1481
|
-
return true;
|
|
1482
|
-
});
|
|
1483
|
-
if (entriesToEnhance.length === 0)
|
|
1484
|
-
return undefined;
|
|
1485
|
-
currentDirLabel = path.relative(currentStashDir, dirPath) || ".";
|
|
1486
|
-
onProgress?.({
|
|
1487
|
-
phase: "llm",
|
|
1488
|
-
message: `Enhancing ${currentDirLabel} ` +
|
|
1489
|
-
`(${entriesToEnhance.length} entr${entriesToEnhance.length === 1 ? "y" : "ies"}).`,
|
|
1490
|
-
processed: completedEntries,
|
|
1491
|
-
total: totalEntries,
|
|
1492
|
-
});
|
|
1493
|
-
lastProgressAt = Date.now();
|
|
1494
|
-
const targetStash = { entries: entriesToEnhance };
|
|
1495
|
-
const itemRefs = entriesToEnhance.map((entry) => {
|
|
1496
|
-
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
1497
|
-
return indexedProvenanceForFile(db, entryPath).itemRef;
|
|
1498
|
-
});
|
|
1499
|
-
let enhanced;
|
|
1500
|
-
try {
|
|
1501
|
-
enhanced = await enhanceStashWithLlm(llmRunner, targetStash, files, summary, enrichSignal, db, itemRefs, config, (event) => {
|
|
1502
|
-
completedEntries++;
|
|
1503
|
-
lastProgressAt = Date.now();
|
|
1504
|
-
onProgress?.({
|
|
1505
|
-
phase: "llm",
|
|
1506
|
-
message: `Enhanced ${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"}; ` +
|
|
1507
|
-
`${completedDirs}/${totalDirs} director${totalDirs === 1 ? "y" : "ies"} complete` +
|
|
1508
|
-
(event.entryName ? `; current ${event.entryName}` : "") +
|
|
1509
|
-
(currentDirLabel ? ` in ${currentDirLabel}` : "") +
|
|
1510
|
-
(event.outcome === "cache-hit" ? " (cache hit)" : ""),
|
|
1511
|
-
processed: completedEntries,
|
|
1512
|
-
total: totalEntries,
|
|
1513
|
-
});
|
|
1514
|
-
}, onNotices, lease);
|
|
1515
|
-
}
|
|
1516
|
-
catch (err) {
|
|
1517
|
-
if (err instanceof ConfigError) {
|
|
1518
|
-
configFailure ??= err;
|
|
1519
|
-
return undefined;
|
|
1520
|
-
}
|
|
1521
|
-
throw err;
|
|
1522
|
-
}
|
|
1523
|
-
// Re-upsert the enhanced entries in a single transaction so a crash
|
|
1524
|
-
// cannot leave half the entries updated and the rest stale.
|
|
1525
|
-
db.transaction(() => {
|
|
1526
|
-
for (const entry of enhanced.entries) {
|
|
1527
|
-
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
1528
|
-
const searchText = buildSearchText(entry);
|
|
1529
|
-
const provenance = indexedProvenanceForFile(db, entryPath);
|
|
1530
|
-
upsertEntry(db, entryPath, attachFileSize(entry, entryPath), searchText, provenance);
|
|
1531
|
-
}
|
|
1532
|
-
})();
|
|
1533
|
-
completedDirs++;
|
|
1534
|
-
lastProgressAt = Date.now();
|
|
1535
|
-
onProgress?.({
|
|
1536
|
-
phase: "llm",
|
|
1537
|
-
message: `Completed ${completedDirs}/${totalDirs} director${totalDirs === 1 ? "y" : "ies"}; ` +
|
|
1538
|
-
`${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"} processed.`,
|
|
1539
|
-
processed: completedEntries,
|
|
1540
|
-
total: totalEntries,
|
|
1541
|
-
});
|
|
1542
|
-
return undefined;
|
|
1543
|
-
},
|
|
1544
|
-
// Defaults: 2 for remote LLM APIs, 1 for local model servers (LM
|
|
1545
|
-
// Studio, Ollama run one inference at a time — parallel requests cause
|
|
1546
|
-
// "Model reloaded" / 500 errors). No config override reaches this path:
|
|
1547
|
-
// `resolveLlmEngineUse` does not forward `engines.<name>.concurrency`.
|
|
1548
|
-
getDefaultLlmConcurrency(llmRunner.connection));
|
|
1549
|
-
if (configFailure)
|
|
1550
|
-
throw configFailure;
|
|
1551
|
-
}
|
|
1552
|
-
finally {
|
|
1553
|
-
if (heartbeatTimer)
|
|
1554
|
-
clearInterval(heartbeatTimer);
|
|
1555
|
-
}
|
|
1556
|
-
if (deadlineHit) {
|
|
1557
|
-
warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase index.enrichment.timeoutMs for a larger budget.");
|
|
1558
|
-
}
|
|
1559
|
-
// Gate-closed (`skipped`) entries are not failures — exclude them so a
|
|
1560
|
-
// deliberately disabled feature never surfaces as an enrichment error.
|
|
1561
|
-
const failed = summary.attempted - summary.succeeded - summary.skipped;
|
|
1562
|
-
if (failed > 0 && summary.succeeded === 0) {
|
|
1563
|
-
const sample = summary.failureSamples.length ? ` Example: ${summary.failureSamples[0]}` : "";
|
|
1564
|
-
warn(`LLM enhancement failed for all ${failed} attempted entries — index built without LLM enrichment.` +
|
|
1565
|
-
` Check llm.endpoint and llm.model in your config.${sample}`);
|
|
1566
|
-
}
|
|
1567
|
-
else if (failed > 0) {
|
|
1568
|
-
const sample = summary.failureSamples.length ? ` Examples: ${summary.failureSamples.join("; ")}` : "";
|
|
1569
|
-
warn(`LLM enhancement failed for ${failed}/${summary.attempted} entries — they were left un-enhanced.${sample}`);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
export function createEnrichmentDeadline(timeoutMs, totalEntries) {
|
|
1573
|
-
const perEntryTimeoutMs = timeoutMs === undefined ? 10 * 60 * 1000 : timeoutMs;
|
|
1574
|
-
return perEntryTimeoutMs === null ? undefined : AbortSignal.timeout(perEntryTimeoutMs * Math.max(totalEntries, 1));
|
|
1575
1050
|
}
|
|
1576
1051
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
1577
|
-
function attachFileSize(entry, entryPath) {
|
|
1578
|
-
try {
|
|
1579
|
-
const sized = { ...entry, fileSize: fs.statSync(entryPath).size };
|
|
1580
|
-
if (hasMarkdownFragmentContent(entry))
|
|
1581
|
-
setMarkdownFragmentContent(sized, getMarkdownFragmentContent(entry));
|
|
1582
|
-
return sized;
|
|
1583
|
-
}
|
|
1584
|
-
catch {
|
|
1585
|
-
return entry;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
1052
|
function buildIndexSummaryMessage(options) {
|
|
1589
1053
|
const stashSourceLabel = options.sourcesCount === 1 ? "stash source" : "stash sources";
|
|
1590
|
-
const semanticDetail =
|
|
1591
|
-
return `Starting ${options.mode} index (${options.sourcesCount} ${stashSourceLabel}, semantic search: ${semanticDetail}
|
|
1054
|
+
const semanticDetail = options.semanticSearchMode === "off" ? "disabled" : `${options.embeddingProvider} embeddings`;
|
|
1055
|
+
return `Starting ${options.mode} index (${options.sourcesCount} ${stashSourceLabel}, semantic search: ${semanticDetail}).`;
|
|
1592
1056
|
}
|
|
1593
1057
|
function getEmbeddingProvider(embedding) {
|
|
1594
1058
|
return isHttpUrl(embedding?.endpoint) ? "remote" : "local";
|
|
1595
1059
|
}
|
|
1596
|
-
function getSemanticSearchLabel(semanticSearchMode, embeddingProvider, vecAvailable) {
|
|
1597
|
-
if (semanticSearchMode === "off")
|
|
1598
|
-
return "disabled";
|
|
1599
|
-
return `${embeddingProvider} embeddings, ${vecAvailable ? "sqlite-vec" : "JS fallback"}`;
|
|
1600
|
-
}
|
|
1601
1060
|
function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
|
|
1602
1061
|
const embeddingCount = getEmbeddingCount(db);
|
|
1603
|
-
const vecAvailable = isVecAvailable(db);
|
|
1604
1062
|
const embeddingProvider = getEmbeddingProvider(config.embedding);
|
|
1063
|
+
const verification = (ok, message, semanticSearchEnabled, semanticStatus, guidance) => ({
|
|
1064
|
+
ok,
|
|
1065
|
+
message,
|
|
1066
|
+
...(guidance !== undefined ? { guidance } : {}),
|
|
1067
|
+
semanticSearchEnabled,
|
|
1068
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
1069
|
+
semanticStatus,
|
|
1070
|
+
embeddingProvider,
|
|
1071
|
+
entryCount: embeddableEntries,
|
|
1072
|
+
embeddingCount,
|
|
1073
|
+
});
|
|
1074
|
+
const pendingStatus = config.semanticSearchMode === "off" ? "disabled" : "pending";
|
|
1605
1075
|
if (embeddableEntries === 0) {
|
|
1606
|
-
return
|
|
1607
|
-
ok: true,
|
|
1608
|
-
message: "Index ready. No assets were found yet.",
|
|
1609
|
-
semanticSearchEnabled: config.semanticSearchMode === "auto",
|
|
1610
|
-
semanticSearchMode: config.semanticSearchMode,
|
|
1611
|
-
semanticStatus: config.semanticSearchMode === "off" ? "disabled" : "pending",
|
|
1612
|
-
embeddingProvider,
|
|
1613
|
-
entryCount: embeddableEntries,
|
|
1614
|
-
embeddingCount,
|
|
1615
|
-
vecAvailable,
|
|
1616
|
-
};
|
|
1076
|
+
return verification(true, "Index ready. No assets were found yet.", config.semanticSearchMode === "auto", pendingStatus);
|
|
1617
1077
|
}
|
|
1618
1078
|
if (config.semanticSearchMode === "off") {
|
|
1619
|
-
return
|
|
1620
|
-
ok: true,
|
|
1621
|
-
message: "Keyword index ready. Semantic search is disabled.",
|
|
1622
|
-
semanticSearchEnabled: false,
|
|
1623
|
-
semanticSearchMode: config.semanticSearchMode,
|
|
1624
|
-
semanticStatus: "disabled",
|
|
1625
|
-
embeddingProvider,
|
|
1626
|
-
entryCount: embeddableEntries,
|
|
1627
|
-
embeddingCount,
|
|
1628
|
-
vecAvailable,
|
|
1629
|
-
};
|
|
1079
|
+
return verification(true, "Keyword index ready. Semantic search is disabled.", false, "disabled");
|
|
1630
1080
|
}
|
|
1631
1081
|
if (embeddingCount >= embeddableEntries) {
|
|
1632
|
-
|
|
1633
|
-
// extension being loaded is not enough when the embedding phase recorded
|
|
1634
|
-
// fast-path insert failures (searchVec then routes to the JS-cosine
|
|
1635
|
-
// fallback via isVecFastPathReady). Reporting vec health from
|
|
1636
|
-
// isVecAvailable alone overstated `akm info` after partial vec failures
|
|
1637
|
-
// (§24.2 "Semantic" gate — truthful ready-vec).
|
|
1638
|
-
const vecActive = vecAvailable && isVecFastPathReady(db);
|
|
1639
|
-
return {
|
|
1640
|
-
ok: true,
|
|
1641
|
-
message: `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings, ${vecActive
|
|
1642
|
-
? "sqlite-vec active"
|
|
1643
|
-
: vecAvailable
|
|
1644
|
-
? "JS fallback active — vec fast path degraded; run 'akm index' to repair valid stored vectors or 'akm index --reembed' to regenerate incompatible ones"
|
|
1645
|
-
: "JS fallback active"}).`,
|
|
1646
|
-
semanticSearchEnabled: true,
|
|
1647
|
-
semanticSearchMode: config.semanticSearchMode,
|
|
1648
|
-
semanticStatus: vecActive ? "ready-vec" : "ready-js",
|
|
1649
|
-
embeddingProvider,
|
|
1650
|
-
entryCount: embeddableEntries,
|
|
1651
|
-
embeddingCount,
|
|
1652
|
-
vecAvailable,
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
return {
|
|
1656
|
-
ok: false,
|
|
1657
|
-
message: embeddingResult.message ??
|
|
1658
|
-
`Semantic search verification failed (${embeddingCount}/${embeddableEntries} embeddings available).`,
|
|
1659
|
-
guidance: embeddingProvider === "remote"
|
|
1660
|
-
? "Check your embedding endpoint and credentials, then retry `akm index --full --verbose`."
|
|
1661
|
-
: "Retry `akm index --full --verbose`. If it still fails, confirm local model downloads are permitted and see docs/reference/configuration.md for local embedding dependency setup.",
|
|
1662
|
-
semanticSearchEnabled: true,
|
|
1663
|
-
semanticSearchMode: config.semanticSearchMode,
|
|
1664
|
-
semanticStatus: "blocked",
|
|
1665
|
-
embeddingProvider,
|
|
1666
|
-
entryCount: embeddableEntries,
|
|
1667
|
-
embeddingCount,
|
|
1668
|
-
vecAvailable,
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
function buildIndexedDirCandidate(dirPath, indexableFiles, generated) {
|
|
1672
|
-
const stash = generated.entries.length > 0 ? { entries: generated.entries } : null;
|
|
1673
|
-
const staleFiles = stash ? resolveIndexedFiles(dirPath, indexableFiles, stash) : indexableFiles;
|
|
1674
|
-
return { stash, staleFiles };
|
|
1675
|
-
}
|
|
1676
|
-
function resolveIndexedFiles(dirPath, files, stash) {
|
|
1677
|
-
const resolved = new Set();
|
|
1678
|
-
for (const entry of stash.entries) {
|
|
1679
|
-
if (entry.filename)
|
|
1680
|
-
resolved.add(path.join(dirPath, entry.filename));
|
|
1082
|
+
return verification(true, `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings).`, true, "ready-js");
|
|
1681
1083
|
}
|
|
1682
|
-
return
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } = await import("../storage/repositories/index-llm-cache-repository.js");
|
|
1687
|
-
let configFailure;
|
|
1688
|
-
const results = await concurrentMap(stash.entries, async (entry, idx) => {
|
|
1689
|
-
if (signal?.aborted)
|
|
1690
|
-
return entry;
|
|
1691
|
-
summary.attempted++;
|
|
1692
|
-
try {
|
|
1693
|
-
const entryFile = entry.filename
|
|
1694
|
-
? (files.find((f) => path.basename(f) === entry.filename) ?? files[0])
|
|
1695
|
-
: files[0];
|
|
1696
|
-
let fileContent;
|
|
1697
|
-
if (entryFile) {
|
|
1698
|
-
try {
|
|
1699
|
-
fileContent = fs.readFileSync(entryFile, "utf8");
|
|
1700
|
-
}
|
|
1701
|
-
catch {
|
|
1702
|
-
warn(`Could not read file for LLM enrichment: ${entry.filename ?? entry.name}`);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
// Incremental cache: skip LLM call when file body is unchanged. The
|
|
1706
|
-
// Cache metadata enrichment by the canonical durable item ref.
|
|
1707
|
-
const cacheBody = fileContent ?? `${entry.name}\n${entry.description ?? ""}`;
|
|
1708
|
-
const bodyHash = computeBodyHash(cacheBody);
|
|
1709
|
-
const cacheKey = itemRefs?.[idx];
|
|
1710
|
-
if (!cacheKey)
|
|
1711
|
-
throw new Error(`Missing canonical item ref for enrichment entry ${entry.name}.`);
|
|
1712
|
-
if (db) {
|
|
1713
|
-
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
1714
|
-
if (cached) {
|
|
1715
|
-
try {
|
|
1716
|
-
const parsed = JSON.parse(cached.resultJson);
|
|
1717
|
-
const updated = { ...entry };
|
|
1718
|
-
if (parsed.description)
|
|
1719
|
-
updated.description = parsed.description;
|
|
1720
|
-
if (parsed.searchHints?.length)
|
|
1721
|
-
updated.searchHints = parsed.searchHints;
|
|
1722
|
-
if (parsed.tags?.length)
|
|
1723
|
-
updated.tags = parsed.tags;
|
|
1724
|
-
updated.quality = "enriched";
|
|
1725
|
-
summary.succeeded++;
|
|
1726
|
-
onEntryDone?.({ entryName: entry.name, outcome: "cache-hit" });
|
|
1727
|
-
return updated;
|
|
1728
|
-
}
|
|
1729
|
-
catch {
|
|
1730
|
-
warn(`LLM enrichment cache entry corrupt for ${entry.name}; re-running enrichment`);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
const outcome = await enhanceMetadata(llmRunner, entry, fileContent, signal, akmConfig, onNotices, lease);
|
|
1735
|
-
if (outcome.status !== "enriched") {
|
|
1736
|
-
// Not a genuine LLM success: the gate was closed (`skipped`) or the
|
|
1737
|
-
// call errored/timed out (`failed`). Do NOT mark the entry enriched
|
|
1738
|
-
// and do NOT write the LLM cache — caching here would poison the
|
|
1739
|
-
// entry into a permanent enrichment skip even though nothing was
|
|
1740
|
-
// enhanced. Surface failures honestly; stay silent on gated-off skips.
|
|
1741
|
-
if (outcome.status === "failed") {
|
|
1742
|
-
const msg = outcome.error ?? "metadata enrichment failed";
|
|
1743
|
-
if (summary.failureSamples.length < 3 && !summary.failureSamples.includes(msg)) {
|
|
1744
|
-
summary.failureSamples.push(msg);
|
|
1745
|
-
}
|
|
1746
|
-
onEntryDone?.({ entryName: entry.name, outcome: "failed" });
|
|
1747
|
-
}
|
|
1748
|
-
else {
|
|
1749
|
-
summary.skipped++;
|
|
1750
|
-
onEntryDone?.({ entryName: entry.name, outcome: "skipped" });
|
|
1751
|
-
}
|
|
1752
|
-
return entry;
|
|
1753
|
-
}
|
|
1754
|
-
const improvements = outcome.metadata;
|
|
1755
|
-
const updated = { ...entry };
|
|
1756
|
-
if (improvements.description)
|
|
1757
|
-
updated.description = improvements.description;
|
|
1758
|
-
if (improvements.searchHints?.length)
|
|
1759
|
-
updated.searchHints = improvements.searchHints;
|
|
1760
|
-
if (improvements.tags?.length)
|
|
1761
|
-
updated.tags = improvements.tags;
|
|
1762
|
-
// Mark as enriched so subsequent index runs skip re-enrichment (P2).
|
|
1763
|
-
// An empty-but-successful response is still cached: the LLM was paid
|
|
1764
|
-
// for this body_hash and produced no improvements, so re-running would
|
|
1765
|
-
// only re-pay for the same no-op. (The cache protects against re-paying
|
|
1766
|
-
// for the LLM call when the file body is unchanged.)
|
|
1767
|
-
updated.quality = "enriched";
|
|
1768
|
-
// Persist to cache so the next run can skip the LLM call when the
|
|
1769
|
-
// file body has not changed.
|
|
1770
|
-
if (db) {
|
|
1771
|
-
upsertLlmCacheEntry(db, cacheKey, bodyHash, JSON.stringify({
|
|
1772
|
-
description: improvements.description,
|
|
1773
|
-
searchHints: improvements.searchHints,
|
|
1774
|
-
tags: improvements.tags,
|
|
1775
|
-
}));
|
|
1776
|
-
}
|
|
1777
|
-
summary.succeeded++;
|
|
1778
|
-
onEntryDone?.({ entryName: entry.name, outcome: "llm" });
|
|
1779
|
-
return updated;
|
|
1780
|
-
}
|
|
1781
|
-
catch (err) {
|
|
1782
|
-
if (err instanceof ConfigError) {
|
|
1783
|
-
configFailure ??= err;
|
|
1784
|
-
return entry;
|
|
1785
|
-
}
|
|
1786
|
-
const msg = toErrorMessage(err);
|
|
1787
|
-
// failureSamples is bounded to 3 items, so a linear scan is cheaper
|
|
1788
|
-
// than maintaining a parallel Set for membership checks (#177 review).
|
|
1789
|
-
if (summary.failureSamples.length < 3 && !summary.failureSamples.includes(msg)) {
|
|
1790
|
-
summary.failureSamples.push(msg);
|
|
1791
|
-
}
|
|
1792
|
-
onEntryDone?.({ entryName: entry.name, outcome: "failed" });
|
|
1793
|
-
return entry;
|
|
1794
|
-
}
|
|
1795
|
-
},
|
|
1796
|
-
// Defaults: 2 for remote LLM APIs, 1 for local model servers. No config
|
|
1797
|
-
// override reaches this path (see getDefaultLlmConcurrency).
|
|
1798
|
-
getDefaultLlmConcurrency(llmRunner.connection));
|
|
1799
|
-
if (configFailure)
|
|
1800
|
-
throw configFailure;
|
|
1801
|
-
// concurrentMap returns Array<T | undefined>; filter out undefined slots
|
|
1802
|
-
// (which can only occur if the callback itself returned undefined, which
|
|
1803
|
-
// it never does above — but TypeScript needs the filter for type safety).
|
|
1804
|
-
const enhanced = results.map((r, i) => r ?? stash.entries[i]);
|
|
1805
|
-
return { entries: enhanced };
|
|
1084
|
+
return verification(false, embeddingResult.message ??
|
|
1085
|
+
`Semantic search verification failed (${embeddingCount}/${embeddableEntries} embeddings available).`, true, "blocked", embeddingProvider === "remote"
|
|
1086
|
+
? "Check your embedding endpoint and credentials, then retry `akm index --full --verbose`."
|
|
1087
|
+
: "Retry `akm index --full --verbose`. If it still fails, confirm local model downloads are permitted and see docs/reference/configuration.md for local embedding dependency setup.");
|
|
1806
1088
|
}
|
|
1807
1089
|
// ── lookup ─────────────────────────────────────────────────────────────────
|
|
1808
1090
|
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
@@ -1812,17 +1094,13 @@ async function resolveLookupSources() {
|
|
|
1812
1094
|
const { resolveSourceEntries } = await import("./search/search-source.js");
|
|
1813
1095
|
return resolveSourceEntries(undefined, loadConfig());
|
|
1814
1096
|
}
|
|
1815
|
-
function resolveLookupScope(bundle, sources) {
|
|
1816
|
-
if (!bundle)
|
|
1817
|
-
return { candidateSources: sources, qualified: false };
|
|
1818
|
-
return { candidateSources: resolveSourcesForOrigin(bundle, sources), qualified: true };
|
|
1819
|
-
}
|
|
1820
1097
|
async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
1821
1098
|
const sources = await resolveLookupSources();
|
|
1822
1099
|
if (sources.length === 0)
|
|
1823
1100
|
return { entry: null };
|
|
1824
|
-
const bundleBySourcePath = new Map(
|
|
1825
|
-
const
|
|
1101
|
+
const bundleBySourcePath = new Map(sourceComponents(sources).map((component, index) => [path.resolve(sources[index].path), component.id]));
|
|
1102
|
+
const qualified = Boolean(ref.bundle);
|
|
1103
|
+
const candidateSources = ref.bundle ? resolveSourcesForOrigin(ref.bundle, sources) : sources;
|
|
1826
1104
|
if (candidateSources.length === 0)
|
|
1827
1105
|
return { entry: null };
|
|
1828
1106
|
let db;
|
|
@@ -1833,6 +1111,11 @@ async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
|
1833
1111
|
catch (error) {
|
|
1834
1112
|
indexError = error;
|
|
1835
1113
|
}
|
|
1114
|
+
const resolved = (entry, owner) => ({
|
|
1115
|
+
entry,
|
|
1116
|
+
...(owner ? { owner } : {}),
|
|
1117
|
+
...(indexError === undefined ? {} : { indexError }),
|
|
1118
|
+
});
|
|
1836
1119
|
try {
|
|
1837
1120
|
for (const source of candidateSources) {
|
|
1838
1121
|
const adapterId = source.adapterId ?? detectAdapterId(source.path);
|
|
@@ -1855,21 +1138,21 @@ async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
|
1855
1138
|
if (!(error instanceof WorkflowSourceIdentityError))
|
|
1856
1139
|
throw error;
|
|
1857
1140
|
warn(`${error.message} Falling back to the physical owner.`);
|
|
1858
|
-
return
|
|
1141
|
+
return resolved(null, owner);
|
|
1859
1142
|
}
|
|
1860
1143
|
}
|
|
1861
1144
|
else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
|
|
1862
|
-
return
|
|
1145
|
+
return resolved(null, owner);
|
|
1863
1146
|
}
|
|
1864
|
-
return
|
|
1147
|
+
return resolved(entry, owner);
|
|
1865
1148
|
}
|
|
1866
1149
|
}
|
|
1867
1150
|
// A physical owner with a missing/incomplete index row still owns this
|
|
1868
1151
|
// unqualified concept. Stop here so a later source cannot retarget it.
|
|
1869
1152
|
if (owner)
|
|
1870
|
-
return
|
|
1153
|
+
return resolved(null, owner);
|
|
1871
1154
|
}
|
|
1872
|
-
return
|
|
1155
|
+
return resolved(null);
|
|
1873
1156
|
}
|
|
1874
1157
|
finally {
|
|
1875
1158
|
if (db)
|
|
@@ -1941,8 +1224,6 @@ export async function lookup(ref) {
|
|
|
1941
1224
|
return lookupBundleRef({ bundle: ref.origin, conceptId: conceptIdFromTypeName(ref.type, ref.name) });
|
|
1942
1225
|
}
|
|
1943
1226
|
// ── Utility score recomputation ──────────────────────────────────────────────
|
|
1944
|
-
/** Retention window for usage events: events older than this are purged. */
|
|
1945
|
-
const USAGE_EVENT_RETENTION_DAYS = 90;
|
|
1946
1227
|
/**
|
|
1947
1228
|
* Recompute utility scores for all entries based on usage_events data.
|
|
1948
1229
|
*
|
|
@@ -1959,16 +1240,11 @@ const USAGE_EVENT_RETENTION_DAYS = 90;
|
|
|
1959
1240
|
*
|
|
1960
1241
|
* Called during `akm index` after FTS rebuild.
|
|
1961
1242
|
*/
|
|
1962
|
-
export function recomputeUtilityScores(db, stateDb
|
|
1243
|
+
export function recomputeUtilityScores(db, stateDb) {
|
|
1963
1244
|
const EMA_DECAY = 0.7;
|
|
1964
|
-
const stateSchema = options?.stateSchema;
|
|
1965
|
-
if (stateSchema !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(stateSchema)) {
|
|
1966
|
-
throw new Error("Invalid attached state schema name.");
|
|
1967
|
-
}
|
|
1968
|
-
const usageEvents = stateSchema === undefined ? "usage_events" : `"${stateSchema}".usage_events`;
|
|
1969
1245
|
// Purge stale usage events (90-day retention). usage_events lives in state.db
|
|
1970
1246
|
// (Chunk-8 WI-8.3); its table is created by state migration 020.
|
|
1971
|
-
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS
|
|
1247
|
+
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
|
|
1972
1248
|
// Time-proportional decay: apply one round of EMA per elapsed day so
|
|
1973
1249
|
// indexing frequency doesn't affect how fast scores decay.
|
|
1974
1250
|
const lastComputedAt = getMeta(db, "last_utility_computed_at");
|
|
@@ -1999,7 +1275,7 @@ export function recomputeUtilityScores(db, stateDb, options) {
|
|
|
1999
1275
|
ELSE NULL
|
|
2000
1276
|
END
|
|
2001
1277
|
) AS last_used_at
|
|
2002
|
-
FROM
|
|
1278
|
+
FROM usage_events u
|
|
2003
1279
|
WHERE u.entry_id IS NOT NULL
|
|
2004
1280
|
AND u.source = 'user'
|
|
2005
1281
|
GROUP BY u.entry_id
|