akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4
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 +731 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- 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 +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- 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 +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- 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 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- 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 +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- 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/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- 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 +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- 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-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- 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 +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- 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/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- 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 +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -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 +2 -7
- 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/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.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 +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- 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 +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- 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/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 +0 -17
- 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 +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- 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 +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- 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 +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- 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/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +361 -751
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- 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 +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- 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 +87 -406
- 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 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- 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 -21
- 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/config/retired-experimental-keys-shim.js +0 -62
- 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 -441
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- 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/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/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 -78
- 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
|
@@ -14,18 +14,18 @@ import { getDbPath } from "../core/paths.js";
|
|
|
14
14
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
15
15
|
import { withStateDb } from "../core/state-db.js";
|
|
16
16
|
import { isVerbose, warn, warnOnce, warnVerbose } from "../core/warn.js";
|
|
17
|
-
import {
|
|
17
|
+
import { assertRunnerCredentials } from "../integrations/agent/runner-dispatch.js";
|
|
18
18
|
import { isLlmFeatureEnabled } from "../llm/feature-gate.js";
|
|
19
19
|
import { resolveIndexPassExecution } from "../llm/index-passes.js";
|
|
20
|
-
import { preflightStructuredLlmRunner } from "../llm/structured-call.js";
|
|
21
20
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
22
|
-
import { salvageEmbeddingsBeforeDiscard } from "../storage/repositories/embedding-salvage-repository.js";
|
|
23
21
|
import { closeDatabase, openExistingDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
|
|
24
|
-
import {
|
|
25
|
-
import { clearStaleCacheEntries, computeBodyHash, getLlmCacheEntry, } from "../storage/repositories/index-llm-cache-repository.js";
|
|
26
|
-
import { deleteIndexDirState, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
22
|
+
import { deleteEntriesByBundle, deleteEntriesByDirAndBundle, deleteEntriesByDirExceptRefs, deleteEntriesByIds, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedBundleIdsByDir, getIndexedDirPathsByBundleId, relinkUsageEvents, upsertEntry, } from "../storage/repositories/index-entries-repository.js";
|
|
23
|
+
import { clearStaleCacheEntries, computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry, } from "../storage/repositories/index-llm-cache-repository.js";
|
|
24
|
+
import { deleteIndexDirState, deleteMeta, getIndexDirState, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
25
|
+
import { VACUUM_PENDING_META } from "../storage/repositories/index-schema.js";
|
|
27
26
|
import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
28
|
-
import { getEmbeddingCount
|
|
27
|
+
import { getEmbeddingCount } from "../storage/repositories/index-vec-repository.js";
|
|
28
|
+
import { INDEX_DB_VACUUMED_EVENT, readFreelistInfo, vacuumIfReclaimable } from "../storage/state-db-integrity.js";
|
|
29
29
|
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
30
30
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
31
31
|
import { reclassifyIndexDbContention } from "./index-db-contention.js";
|
|
@@ -33,9 +33,8 @@ import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
|
33
33
|
import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
|
|
34
34
|
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
35
35
|
import { canUseIncrementalSkip, computeDirFingerprint, getCachedDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
|
|
36
|
-
import {
|
|
36
|
+
import { isEnrichmentComplete, isWorkflowSkipWarning, withFileSize, } from "./passes/metadata.js";
|
|
37
37
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
38
|
-
import { buildSearchText } from "./search/search-fields.js";
|
|
39
38
|
import { purgeOldUsageEvents } from "./usage/usage-events.js";
|
|
40
39
|
import { walkStashFlatWithStatus } from "./walk/walker.js";
|
|
41
40
|
function collectLoweringNotices(target, notices) {
|
|
@@ -68,12 +67,16 @@ export function getDefaultLlmConcurrency(llmConfig) {
|
|
|
68
67
|
// is what runs (see docs/architecture/internals/indexing.md).
|
|
69
68
|
return defaultConcurrencyForEndpoint(llmConfig?.endpoint);
|
|
70
69
|
}
|
|
70
|
+
// ── Source ownership ─────────────────────────────────────────────────────────
|
|
71
|
+
/** Each source's durable bundle component (`deriveInstallations`: one per source, source order). */
|
|
72
|
+
function sourceComponents(sources) {
|
|
73
|
+
return deriveInstallations([...sources]).map((installation) => installation.components[0]);
|
|
74
|
+
}
|
|
71
75
|
function sourceOwners(sources) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
76
|
+
return sourceComponents(sources).map((component, index) => ({
|
|
77
|
+
bundleId: component.id,
|
|
78
|
+
sourceRoot: path.resolve(sources[index].path),
|
|
79
|
+
}));
|
|
77
80
|
}
|
|
78
81
|
function parseStoredSourceOwners(raw) {
|
|
79
82
|
if (!raw)
|
|
@@ -99,102 +102,43 @@ function parseStoredSourceOwners(raw) {
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
105
|
+
* Sources removed (or moved) since the last complete run. Their entries and
|
|
106
|
+
* graph rows are purged by {@link applyRemovedSources} once the walk completes.
|
|
104
107
|
*/
|
|
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().
|
|
108
|
+
function findRemovedSources(db, sources) {
|
|
109
|
+
const currentByBundle = new Map(sourceOwners(sources).map((owner) => [owner.bundleId, owner]));
|
|
110
|
+
return parseStoredSourceOwners(getMeta(db, "sourceOwners")).flatMap((previous) => {
|
|
111
|
+
const current = currentByBundle.get(previous.bundleId);
|
|
112
|
+
if (current && current.sourceRoot === previous.sourceRoot)
|
|
113
|
+
return [];
|
|
114
|
+
return [{ ...previous, removeBundleEntries: current === undefined }];
|
|
115
|
+
});
|
|
122
116
|
}
|
|
123
|
-
function applyRemovedSources(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
for (const removed of ctx.removedSources) {
|
|
117
|
+
function applyRemovedSources(db, sources, removedSources, isIncremental) {
|
|
118
|
+
const owners = sourceOwners(sources);
|
|
119
|
+
const currentRoots = new Set(owners.map((owner) => owner.sourceRoot));
|
|
120
|
+
for (const removed of removedSources) {
|
|
128
121
|
if (removed.removeBundleEntries)
|
|
129
|
-
deleteEntriesByBundle(
|
|
122
|
+
deleteEntriesByBundle(db, removed.bundleId);
|
|
130
123
|
if (!currentRoots.has(removed.sourceRoot))
|
|
131
|
-
deleteStoredGraph(
|
|
124
|
+
deleteStoredGraph(db, removed.sourceRoot);
|
|
132
125
|
}
|
|
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
|
-
}
|
|
126
|
+
// A full run re-drains every configured source, so any other bundle's rows
|
|
127
|
+
// are stale even when no stored owner names them.
|
|
128
|
+
if (isIncremental)
|
|
129
|
+
return;
|
|
130
|
+
const currentBundles = new Set(owners.map((owner) => owner.bundleId));
|
|
131
|
+
const indexed = db.prepare("SELECT DISTINCT bundle_id AS bundleId FROM entries").all();
|
|
132
|
+
for (const { bundleId } of indexed) {
|
|
133
|
+
if (!currentBundles.has(bundleId))
|
|
134
|
+
deleteEntriesByBundle(db, bundleId);
|
|
176
135
|
}
|
|
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
136
|
}
|
|
189
137
|
/**
|
|
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.
|
|
138
|
+
* The embedding phase (#954): generate and store vectors for every entry
|
|
139
|
+
* missing one, then compute the `hasEmbeddings` fact and the semantic-search
|
|
140
|
+
* verification off the result. `db` must have no ambient transaction open —
|
|
141
|
+
* each per-batch commit is its own durable write.
|
|
198
142
|
*/
|
|
199
143
|
export async function runEmbeddingPass(params) {
|
|
200
144
|
const { db, config, onProgress, signal, reembed } = params;
|
|
@@ -209,69 +153,24 @@ export async function runEmbeddingPass(params) {
|
|
|
209
153
|
return { embeddingResult, verification };
|
|
210
154
|
}
|
|
211
155
|
/**
|
|
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.
|
|
156
|
+
* Finalize: re-link usage events, recompute utility scores, clear stale LLM
|
|
157
|
+
* cache rows and record this run's freshness metadata. `upsertEntry` and every
|
|
158
|
+
* canonical delete own their FTS projection, so the "fts" event is an
|
|
159
|
+
* observation point, not a second materialization pass.
|
|
243
160
|
*/
|
|
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();
|
|
161
|
+
function finalizeIndex(args) {
|
|
162
|
+
const { db, sources, sourceDirs, stashDir, signal, onProgress } = args;
|
|
163
|
+
onProgress({ phase: "fts", message: "Full-text search index is current." });
|
|
164
|
+
const tFtsEnd = Date.now();
|
|
254
165
|
// Re-link state.db usage events to the regenerated index and recompute the
|
|
255
166
|
// derived utility cache. Stored refs already use the current item-ref grammar,
|
|
256
167
|
// so this idempotent pass only restores derived entry ids.
|
|
257
|
-
|
|
168
|
+
withStateDb((stateDb) => {
|
|
258
169
|
onProgress({ phase: "finalize", message: "Relinking usage events." });
|
|
259
|
-
relinkUsageEvents(db, stateDb
|
|
170
|
+
relinkUsageEvents(db, stateDb);
|
|
260
171
|
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
|
-
}
|
|
172
|
+
recomputeUtilityScores(db, stateDb);
|
|
173
|
+
});
|
|
275
174
|
// Purge LLM cache entries for assets that no longer exist in the index.
|
|
276
175
|
try {
|
|
277
176
|
onProgress({ phase: "finalize", message: "Clearing stale LLM cache entries." });
|
|
@@ -284,41 +183,34 @@ async function runFinalizePhase(ctx) {
|
|
|
284
183
|
// An incomplete run preserves the prior freshness watermark. Advancing it
|
|
285
184
|
// could make a recovered source look unchanged even though this run never
|
|
286
185
|
// persisted its files.
|
|
287
|
-
if (
|
|
186
|
+
if (args.scanComplete) {
|
|
288
187
|
setMeta(db, "builtAt", new Date().toISOString());
|
|
289
188
|
setMeta(db, "stashDir", stashDir);
|
|
290
189
|
setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
|
|
291
190
|
setMeta(db, "sourceOwners", JSON.stringify(sourceOwners(sources)));
|
|
292
191
|
}
|
|
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;
|
|
192
|
+
return { tFtsEnd };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Compact index.db once a layout migration has left tables and columns'
|
|
196
|
+
* pages free, or whenever more than half its pages are free (the same
|
|
197
|
+
* threshold improve applies to state.db). Nothing else ever VACUUMs this
|
|
198
|
+
* file, and a delete-heavy history leaves it mostly free pages.
|
|
199
|
+
*/
|
|
200
|
+
function vacuumIndexDb(db, onProgress) {
|
|
201
|
+
const migrated = getMeta(db, VACUUM_PENDING_META) === "1";
|
|
202
|
+
const outcome = vacuumIfReclaimable(db, readFreelistInfo(db), {
|
|
203
|
+
eventType: INDEX_DB_VACUUMED_EVENT,
|
|
204
|
+
force: migrated,
|
|
205
|
+
});
|
|
206
|
+
if (!outcome.ran)
|
|
207
|
+
return;
|
|
208
|
+
if (migrated)
|
|
209
|
+
deleteMeta(db, VACUUM_PENDING_META);
|
|
210
|
+
onProgress({
|
|
211
|
+
phase: "finalize",
|
|
212
|
+
message: `Compacted index.db with VACUUM: ${outcome.pagesBefore} → ${outcome.pagesAfter} pages.`,
|
|
213
|
+
});
|
|
322
214
|
}
|
|
323
215
|
// ── Clean pass ───────────────────────────────────────────────────────────────
|
|
324
216
|
/**
|
|
@@ -372,29 +264,12 @@ let akmIndexOverride;
|
|
|
372
264
|
export function _setAkmIndexForTests(fake) {
|
|
373
265
|
akmIndexOverride = fake;
|
|
374
266
|
}
|
|
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
267
|
export async function akmIndex(options) {
|
|
383
268
|
try {
|
|
384
269
|
const override = akmIndexOverride;
|
|
385
270
|
return override ? await override(options) : await akmIndexReal(options);
|
|
386
271
|
}
|
|
387
272
|
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
273
|
throw reclassifyIndexDbContention(error);
|
|
399
274
|
}
|
|
400
275
|
}
|
|
@@ -402,20 +277,16 @@ let indexTransactionHookForTests;
|
|
|
402
277
|
/**
|
|
403
278
|
* TEST-ONLY. Observe the in-flight reindex transaction; `undefined` restores.
|
|
404
279
|
*
|
|
405
|
-
* Exists because the
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
280
|
+
* Exists because the persist transaction's atomicity is, by construction,
|
|
281
|
+
* invisible from outside it: by the time `akmIndex()` resolves, the commit
|
|
282
|
+
* has already collapsed both generations into one observable state.
|
|
283
|
+
* Concurrency tests install a hook that opens a SECOND connection at this
|
|
284
|
+
* point and asserts it still sees the previous complete generation. Inert in
|
|
285
|
+
* production (one `undefined?.()` per reindex).
|
|
411
286
|
*/
|
|
412
287
|
export function _setIndexTransactionHookForTests(hook) {
|
|
413
288
|
indexTransactionHookForTests = hook;
|
|
414
289
|
}
|
|
415
|
-
/** Fire a named in-transaction observation point (no-op outside tests). */
|
|
416
|
-
function indexTransactionHook(point) {
|
|
417
|
-
indexTransactionHookForTests?.(point);
|
|
418
|
-
}
|
|
419
290
|
let drainObserverForTests;
|
|
420
291
|
/**
|
|
421
292
|
* TEST-ONLY. Observe every directory that actually reaches
|
|
@@ -429,6 +300,18 @@ let drainObserverForTests;
|
|
|
429
300
|
export function _setDrainObserverForTests(observer) {
|
|
430
301
|
drainObserverForTests = observer;
|
|
431
302
|
}
|
|
303
|
+
/** Whether `root` is a directory whose listing can be read (adapter detection only probes such roots). */
|
|
304
|
+
function isReadableDirectory(root) {
|
|
305
|
+
try {
|
|
306
|
+
if (!fs.statSync(root).isDirectory())
|
|
307
|
+
return false;
|
|
308
|
+
fs.readdirSync(root); // probe readability, not just existence
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
432
315
|
/**
|
|
433
316
|
* Detect an adapter for every resolvable source that does not declare one, and
|
|
434
317
|
* persist each detection into `config.json`.
|
|
@@ -440,17 +323,13 @@ export function _setDrainObserverForTests(observer) {
|
|
|
440
323
|
* emitted here. The map is cleared at the top of every callback invocation
|
|
441
324
|
* because `mutateConfig` may retry optimistically, and a retry must not report
|
|
442
325
|
* 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
326
|
*/
|
|
448
327
|
function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, opts) {
|
|
449
328
|
const detectedByBundle = new Map();
|
|
450
329
|
for (const source of allSourceEntries) {
|
|
451
330
|
if (source.adapterId || source.unresolved)
|
|
452
331
|
continue;
|
|
453
|
-
if (
|
|
332
|
+
if (isReadableDirectory(source.path)) {
|
|
454
333
|
source.adapterId = detectAdapterId(source.path);
|
|
455
334
|
if (source.registryId)
|
|
456
335
|
detectedByBundle.set(source.registryId, source.adapterId);
|
|
@@ -490,38 +369,6 @@ function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig,
|
|
|
490
369
|
}
|
|
491
370
|
return { config: nextConfig, persistedAdapters };
|
|
492
371
|
}
|
|
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
372
|
async function akmIndexReal(options) {
|
|
526
373
|
// R-022: `dryRun` only ever gated the `--clean` stale-entry removal pass
|
|
527
374
|
// (see `runCleanPass` below) — every other phase (walk, LLM enrichment,
|
|
@@ -538,183 +385,209 @@ async function akmIndexReal(options) {
|
|
|
538
385
|
"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
386
|
}
|
|
540
387
|
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
|
-
|
|
388
|
+
const { stashDir, signal } = options;
|
|
389
|
+
const onProgress = options.onProgress ?? (() => { });
|
|
390
|
+
const full = options.full === true;
|
|
391
|
+
const clean = options.clean === true;
|
|
392
|
+
const dryRun = options.dryRun === true;
|
|
393
|
+
// Load config and resolve all stash sources
|
|
394
|
+
const { loadConfig, mutateConfig } = await import("../core/config/config.js");
|
|
395
|
+
let config = loadConfig();
|
|
396
|
+
// Durable state must be runtime-compatible before source hydration,
|
|
397
|
+
// adapter persistence, or index.db creation can mutate the installation.
|
|
398
|
+
onProgress({ phase: "preflight", message: "Validating durable state." });
|
|
399
|
+
withStateDb(() => undefined);
|
|
400
|
+
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
401
|
+
// so their content directories exist on disk for the walker to discover.
|
|
402
|
+
const sourceCacheStart = Date.now();
|
|
403
|
+
onProgress({ phase: "preflight", message: "Hydrating source caches." });
|
|
404
|
+
const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
|
|
405
|
+
// Inject the store-backed secret resolver from here — a composition root
|
|
406
|
+
// ABOVE the provider/fetcher import cycle (this module reaches
|
|
407
|
+
// search-source only via dynamic import). This is what lets a website
|
|
408
|
+
// source's X fetcher resolve `secrets/x-bearer-token` during
|
|
409
|
+
// bundle-update / hydrate, not just from the command-layer URL-ingest
|
|
410
|
+
// path. `secret-seam` is imported here, never from inside the cycle.
|
|
411
|
+
const { storeSecretResolver } = await import("../sources/snapshot-fetchers/secret-seam.js");
|
|
412
|
+
await ensureSourceCaches(config, {
|
|
413
|
+
force: full,
|
|
414
|
+
materialize: options.hydrateSources !== false,
|
|
415
|
+
secrets: storeSecretResolver,
|
|
416
|
+
// Same progress channel as every other phase (#954) — a
|
|
417
|
+
// stalled clone/fetch here runs BEFORE index.db is even opened, so
|
|
418
|
+
// without this it looked identical to "no database open, nothing
|
|
419
|
+
// written".
|
|
420
|
+
onProgress: (message) => onProgress({ phase: "preflight", message }),
|
|
421
|
+
});
|
|
422
|
+
const sourceCacheEnd = Date.now();
|
|
423
|
+
const sources = resolveSourceEntries(stashDir, config);
|
|
424
|
+
const detected = detectAndPersistBundleAdapters(sources, config, mutateConfig, {
|
|
425
|
+
announce: options.implicit !== true,
|
|
426
|
+
persist: options.persistDetectedAdapters !== false,
|
|
427
|
+
});
|
|
428
|
+
config = detected.config;
|
|
429
|
+
const { persistedAdapters } = detected;
|
|
430
|
+
const sourceDirs = sources.map((s) => s.path);
|
|
431
|
+
onProgress({
|
|
432
|
+
phase: "preflight",
|
|
433
|
+
message: `Resolved ${sourceDirs.length} stash source${sourceDirs.length === 1 ? "" : "s"}.`,
|
|
434
|
+
});
|
|
435
|
+
const t0 = Date.now();
|
|
436
|
+
const enrichmentExecution = resolveIndexPassExecution("enrichment", config);
|
|
437
|
+
const loweringNotices = [...enrichmentExecution.notices];
|
|
438
|
+
const dbPath = getDbPath();
|
|
439
|
+
const db = openIndexDatabase(dbPath);
|
|
440
|
+
try {
|
|
441
|
+
// `--full` folds into `isIncremental`: a full run drains every directory
|
|
442
|
+
// through the same diff-persist path as an incremental one.
|
|
443
|
+
const prevStashDir = getMeta(db, "stashDir");
|
|
444
|
+
const prevBuiltAt = getMeta(db, "builtAt");
|
|
445
|
+
const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
|
|
446
|
+
const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
|
|
447
|
+
const mode = isIncremental ? "incremental" : "full";
|
|
448
|
+
onProgress({
|
|
449
|
+
phase: "summary",
|
|
450
|
+
message: buildIndexSummaryMessage({
|
|
451
|
+
mode,
|
|
452
|
+
sourcesCount: sourceDirs.length,
|
|
453
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
454
|
+
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
455
|
+
llmEnabled: !!enrichmentExecution.runner,
|
|
456
|
+
}),
|
|
584
457
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
458
|
+
const removedSources = findRemovedSources(db, sources);
|
|
459
|
+
// ── Walk: scan the filesystem, generate metadata, persist entries ─────
|
|
460
|
+
throwIfAborted(signal);
|
|
461
|
+
const tWalkStart = Date.now();
|
|
462
|
+
// Walk every source and pre-generate all metadata outside any transaction…
|
|
463
|
+
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, sources, isIncremental, builtAtMs, removedSources.length > 0, onProgress, !clean);
|
|
464
|
+
// …fail on a missing enrichment credential before anything is persisted…
|
|
465
|
+
const bundleByRoot = buildIndexedSourceOwners(sources);
|
|
466
|
+
const runner = enrichmentExecution.runner;
|
|
467
|
+
if (runner &&
|
|
468
|
+
isLlmFeatureEnabled(config, "metadata_enhance") &&
|
|
469
|
+
dirRecordsNeedMetadataDispatch(db, dirRecords, bundleByRoot)) {
|
|
470
|
+
assertRunnerCredentials(runner);
|
|
471
|
+
}
|
|
472
|
+
// …then write it in one transaction: `item_ref = <bundle>//<conceptId>`
|
|
473
|
+
// plus canonical component/adapter provenance per source root.
|
|
474
|
+
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, warnings, bundleByRoot);
|
|
588
475
|
onProgress({
|
|
589
|
-
phase: "
|
|
590
|
-
message: `
|
|
476
|
+
phase: "scan",
|
|
477
|
+
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
591
478
|
});
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
479
|
+
// Workflow validation noise gate (issue #273): suppress per-spec stderr
|
|
480
|
+
// lines at default verbosity and emit a single summary instead. In verbose
|
|
481
|
+
// mode the per-spec lines are already printed by buildMetadataSkipWarning.
|
|
482
|
+
if (!isVerbose()) {
|
|
483
|
+
const skippedWorkflowCount = warnings.filter(isWorkflowSkipWarning).length;
|
|
484
|
+
if (skippedWorkflowCount > 0) {
|
|
485
|
+
const noun = skippedWorkflowCount === 1 ? "workflow spec" : "workflow specs";
|
|
486
|
+
warn(`${skippedWorkflowCount} ${noun} skipped due to validation errors; ` +
|
|
487
|
+
"rerun with --verbose (or AKM_VERBOSE=1) to see details.");
|
|
488
|
+
}
|
|
601
489
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
490
|
+
const tWalkEnd = Date.now();
|
|
491
|
+
// ── LLM enrichment for the directories that need it ───────────────────
|
|
492
|
+
throwIfAborted(signal);
|
|
493
|
+
await enhanceDirsWithLlm(db, config, enrichmentExecution, dirsNeedingLlm, onProgress, signal, (notices) => collectLoweringNotices(loweringNotices, notices));
|
|
494
|
+
onProgress({
|
|
495
|
+
phase: "llm",
|
|
496
|
+
message: enrichmentExecution.runner
|
|
497
|
+
? `LLM enhancement reviewed ${dirsNeedingLlm.length} ${dirsNeedingLlm.length === 1 ? "directory" : "directories"}.`
|
|
498
|
+
: "LLM enhancement disabled.",
|
|
499
|
+
});
|
|
500
|
+
const tLlmEnd = Date.now();
|
|
501
|
+
if (complete)
|
|
502
|
+
applyRemovedSources(db, sources, removedSources, isIncremental);
|
|
503
|
+
// Reconcile explicit missing-file cleanup before embeddings, totals, or
|
|
504
|
+
// verification describe this generation. Dry-run intentionally leaves
|
|
505
|
+
// the generation unchanged while still returning the previewed refs.
|
|
506
|
+
let cleanResult;
|
|
507
|
+
const cleanStart = Date.now();
|
|
508
|
+
if (clean) {
|
|
621
509
|
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
|
-
}),
|
|
510
|
+
phase: "finalize",
|
|
511
|
+
message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
|
|
631
512
|
});
|
|
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
|
-
}
|
|
513
|
+
if (complete) {
|
|
514
|
+
cleanResult = runCleanPass(db, dryRun);
|
|
655
515
|
}
|
|
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);
|
|
516
|
+
else {
|
|
517
|
+
warn("[index] --clean skipped because one or more configured sources were not scanned completely.");
|
|
518
|
+
cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
|
|
697
519
|
}
|
|
698
|
-
if (!borrowedUpdateDb)
|
|
699
|
-
closeDatabase(db);
|
|
700
520
|
}
|
|
701
|
-
|
|
521
|
+
const cleanEnd = Date.now();
|
|
522
|
+
// ── Embeddings ─────────────────────────────────────────────────────────
|
|
523
|
+
throwIfAborted(signal);
|
|
524
|
+
const { verification } = await runEmbeddingPass({
|
|
525
|
+
db,
|
|
526
|
+
config,
|
|
527
|
+
onProgress,
|
|
528
|
+
signal,
|
|
529
|
+
reembed: options.reembed === true,
|
|
530
|
+
});
|
|
531
|
+
const tEmbedEnd = Date.now();
|
|
532
|
+
// ── Finalize ──────────────────────────────────────────────────────────
|
|
533
|
+
const tFinalizeStart = Date.now();
|
|
534
|
+
const { tFtsEnd } = finalizeIndex({
|
|
535
|
+
db,
|
|
536
|
+
sources,
|
|
537
|
+
sourceDirs,
|
|
538
|
+
stashDir,
|
|
539
|
+
scanComplete: complete,
|
|
540
|
+
signal,
|
|
541
|
+
onProgress,
|
|
542
|
+
});
|
|
543
|
+
vacuumIndexDb(db, onProgress);
|
|
544
|
+
const totalEntries = getEntryCount(db);
|
|
545
|
+
const tFinalizeEnd = Date.now();
|
|
546
|
+
return {
|
|
547
|
+
stashDir,
|
|
548
|
+
totalEntries,
|
|
549
|
+
generatedMetadata: generatedCount,
|
|
550
|
+
indexPath: dbPath,
|
|
551
|
+
mode,
|
|
552
|
+
directoriesScanned: scannedDirs,
|
|
553
|
+
directoriesSkipped: skippedDirs,
|
|
554
|
+
scanComplete: complete,
|
|
555
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
556
|
+
...(loweringNotices.length > 0 ? { notices: Object.freeze([...loweringNotices]) } : {}),
|
|
557
|
+
...(Object.keys(persistedAdapters).length > 0 ? { configUpdated: { detectedAdapters: persistedAdapters } } : {}),
|
|
558
|
+
verification,
|
|
559
|
+
timing: {
|
|
560
|
+
totalMs: Date.now() - t0,
|
|
561
|
+
walkMs: tWalkEnd - tWalkStart,
|
|
562
|
+
llmMs: tLlmEnd - tWalkEnd,
|
|
563
|
+
embedMs: tEmbedEnd - tLlmEnd,
|
|
564
|
+
ftsMs: tFtsEnd - tEmbedEnd,
|
|
565
|
+
finalizeMs: tFinalizeEnd - tFinalizeStart,
|
|
566
|
+
cleanMs: clean ? cleanEnd - cleanStart : 0,
|
|
567
|
+
preflightMs: t0 - requestedAt,
|
|
568
|
+
sourceCacheMs: sourceCacheEnd - sourceCacheStart,
|
|
569
|
+
endToEndMs: Date.now() - requestedAt,
|
|
570
|
+
},
|
|
571
|
+
...(cleanResult !== undefined ? { clean: cleanResult } : {}),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
closeDatabase(db);
|
|
576
|
+
}
|
|
702
577
|
}
|
|
703
578
|
function buildIndexedSourceOwners(sources) {
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
});
|
|
717
|
-
return owners;
|
|
579
|
+
const components = sourceComponents(sources);
|
|
580
|
+
return new Map(sources.map((source, index) => {
|
|
581
|
+
const { id, adapter } = components[index];
|
|
582
|
+
return [path.resolve(source.path), { bundleId: id, componentId: id, adapterId: adapter }];
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The metadata-enrichment cache's body hash (`llm_enrichment_cache`, keyed by
|
|
587
|
+
* item ref + this hash): the entry file's text, else its name and description.
|
|
588
|
+
*/
|
|
589
|
+
function enrichmentBodyHash(entry, fileContent) {
|
|
590
|
+
return computeBodyHash(fileContent ?? `${entry.name}\n${entry.description ?? ""}`);
|
|
718
591
|
}
|
|
719
592
|
/** Read-only mirror of the enrichment cache gate used before entry persistence. */
|
|
720
593
|
function dirRecordsNeedMetadataDispatch(db, records, ownersByRoot) {
|
|
@@ -740,7 +613,7 @@ function dirRecordsNeedMetadataDispatch(db, records, ownersByRoot) {
|
|
|
740
613
|
catch {
|
|
741
614
|
// The dispatch path uses the same deterministic metadata fallback.
|
|
742
615
|
}
|
|
743
|
-
const bodyHash =
|
|
616
|
+
const bodyHash = enrichmentBodyHash(entry, fileContent);
|
|
744
617
|
const cacheKey = deriveEntryProvenance(owner, entry.type, entry.name, adapterConceptId).itemRef;
|
|
745
618
|
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
746
619
|
if (!cached)
|
|
@@ -762,32 +635,6 @@ function addEntryIds(target, ids) {
|
|
|
762
635
|
for (const id of ids)
|
|
763
636
|
target.add(id);
|
|
764
637
|
}
|
|
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
638
|
function groupFileContextsByDir(fileContexts) {
|
|
792
639
|
const groups = new Map();
|
|
793
640
|
for (const ctx of fileContexts) {
|
|
@@ -876,17 +723,19 @@ function warnIfAdapterSkipsAkmContent(component, files, adapter) {
|
|
|
876
723
|
`${skippedDirs.map((dir) => `${dir}/`).join(", ")} — set components.<name>.adapter to "akm" to index them`);
|
|
877
724
|
}
|
|
878
725
|
function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs) {
|
|
879
|
-
|
|
726
|
+
// Each source root → its durable `BundleComponent`; the per-dir document
|
|
727
|
+
// drain dispatches `adapterForId(component.adapter).recognize` for it.
|
|
728
|
+
const components = sourceComponents(allSourceEntries);
|
|
729
|
+
const componentBySource = new Map(allSourceEntries.map((source, index) => [source.path, components[index]]));
|
|
880
730
|
const handoffDirs = new Set();
|
|
881
731
|
const plans = allSourceEntries.map((sourceAdded) => {
|
|
882
732
|
const currentStashDir = sourceAdded.path;
|
|
883
|
-
const component =
|
|
733
|
+
const component = componentBySource.get(currentStashDir);
|
|
884
734
|
if (sourceAdded.unresolved) {
|
|
885
735
|
return {
|
|
886
736
|
currentStashDir,
|
|
887
737
|
component,
|
|
888
738
|
adapter: undefined,
|
|
889
|
-
indexVariant: undefined,
|
|
890
739
|
dirGroups: new Map(),
|
|
891
740
|
removals: [],
|
|
892
741
|
walkComplete: false,
|
|
@@ -906,7 +755,6 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
906
755
|
currentStashDir,
|
|
907
756
|
component,
|
|
908
757
|
adapter,
|
|
909
|
-
indexVariant: adapter ? `${adapter.id}@${adapter.version}` : undefined,
|
|
910
758
|
dirGroups,
|
|
911
759
|
removals: [],
|
|
912
760
|
walkComplete: walked.complete,
|
|
@@ -931,9 +779,10 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
931
779
|
handoffDirs.add(resolvedDir);
|
|
932
780
|
};
|
|
933
781
|
const allComplete = plans.every((plan) => plan.walkComplete && plan.adapter !== undefined);
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
|
|
782
|
+
// Reconcile departed directories for every source that produced a
|
|
783
|
+
// trustworthy snapshot; a source that was not walked completely keeps its
|
|
784
|
+
// last-known-good rows.
|
|
785
|
+
if (reconcileMissingDirs) {
|
|
937
786
|
const allIndexedDirsByBundle = !isIncremental ? new Map() : undefined;
|
|
938
787
|
if (allIndexedDirsByBundle) {
|
|
939
788
|
for (const entry of getAllEntries(db)) {
|
|
@@ -986,9 +835,6 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
986
835
|
* outside `db.transaction()` so the persist pass can be a single synchronous
|
|
987
836
|
* transaction.
|
|
988
837
|
*/
|
|
989
|
-
function reportSourceScanProgress(onProgress, processed, total, message) {
|
|
990
|
-
onProgress?.({ phase: "scan", message, processed, total });
|
|
991
|
-
}
|
|
992
838
|
async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs = true) {
|
|
993
839
|
let scannedDirs = 0;
|
|
994
840
|
let skippedDirs = 0;
|
|
@@ -999,7 +845,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
999
845
|
const dirRecords = [];
|
|
1000
846
|
let processedDirs = 0;
|
|
1001
847
|
let priorDirsChanged = hadRemovedSources;
|
|
1002
|
-
const reportScanProgress = (message) =>
|
|
848
|
+
const reportScanProgress = (message) => onProgress?.({ phase: "scan", message, processed: processedDirs, total: allSourceEntries.length });
|
|
1003
849
|
const reportDirDecision = (kind, dirPath, currentStashDir, reason, persistedRowCount) => {
|
|
1004
850
|
if (!isVerbose())
|
|
1005
851
|
return;
|
|
@@ -1020,46 +866,6 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
1020
866
|
seenPaths.add(resolved);
|
|
1021
867
|
return false;
|
|
1022
868
|
};
|
|
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
869
|
for (const plan of plans) {
|
|
1064
870
|
const { currentStashDir, component, adapter, dirGroups, removals, walkComplete } = plan;
|
|
1065
871
|
processedDirs++;
|
|
@@ -1079,7 +885,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
1079
885
|
warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
|
|
1080
886
|
continue;
|
|
1081
887
|
}
|
|
1082
|
-
const indexVariant =
|
|
888
|
+
const indexVariant = `${adapter.id}@${adapter.version}`;
|
|
1083
889
|
for (const removal of removals) {
|
|
1084
890
|
dirRecords.push(removal);
|
|
1085
891
|
scannedDirs++;
|
|
@@ -1129,17 +935,53 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
1129
935
|
const drained = drainDirDocuments(adapter, component, ctxs);
|
|
1130
936
|
if (drained.warnings.length)
|
|
1131
937
|
warnings.push(...drained.warnings);
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
938
|
+
generatedCount += drained.entries.length;
|
|
939
|
+
const stash = drained.entries.length > 0 ? { entries: drained.entries } : null;
|
|
940
|
+
// Freshness is judged over the files the entries came from (every walked
|
|
941
|
+
// file when none was recognized).
|
|
942
|
+
const entryFiles = new Set(drained.entries.flatMap((entry) => (entry.filename ? [path.join(dirPath, entry.filename)] : [])));
|
|
943
|
+
const staleFiles = entryFiles.size > 0 ? [...entryFiles] : indexableFiles;
|
|
944
|
+
// Incremental freshness gate: consult the persisted dir state and record
|
|
945
|
+
// either a skip (unchanged + eligible) or a scan carrying the drained stash.
|
|
946
|
+
const previousState = getDirIndexState(db, dirPath, staleFiles, builtAtMs, indexVariant, fingerprint);
|
|
947
|
+
const { persistedRowCount } = previousState;
|
|
948
|
+
if (isIncremental &&
|
|
949
|
+
!forceScan &&
|
|
950
|
+
!previousState.stale &&
|
|
951
|
+
canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
952
|
+
skippedDirs++;
|
|
953
|
+
const reason = previousState.reason;
|
|
954
|
+
dirRecords.push({
|
|
955
|
+
dirPath,
|
|
956
|
+
currentStashDir,
|
|
957
|
+
files: staleFiles,
|
|
958
|
+
fingerprint,
|
|
959
|
+
stash: null,
|
|
960
|
+
skip: true,
|
|
961
|
+
reason,
|
|
962
|
+
persistedRowCount,
|
|
963
|
+
indexVariant,
|
|
964
|
+
});
|
|
965
|
+
reportDirDecision("skip", dirPath, currentStashDir, reason, persistedRowCount);
|
|
966
|
+
continue;
|
|
1141
967
|
}
|
|
1142
|
-
|
|
968
|
+
scannedDirs++;
|
|
969
|
+
priorDirsChanged = true;
|
|
970
|
+
const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
|
|
971
|
+
dirRecords.push({
|
|
972
|
+
dirPath,
|
|
973
|
+
currentStashDir,
|
|
974
|
+
files: staleFiles,
|
|
975
|
+
fingerprint,
|
|
976
|
+
stash,
|
|
977
|
+
skip: false,
|
|
978
|
+
reason,
|
|
979
|
+
persistedRowCount,
|
|
980
|
+
hashByFile: drained.hashByFile,
|
|
981
|
+
conceptIdByFile: drained.conceptIdByFile,
|
|
982
|
+
indexVariant,
|
|
983
|
+
});
|
|
984
|
+
reportDirDecision("scan", dirPath, currentStashDir, reason, persistedRowCount);
|
|
1143
985
|
}
|
|
1144
986
|
}
|
|
1145
987
|
return {
|
|
@@ -1161,90 +1003,22 @@ function requiresWorkflowSourcePreflight(ctxs) {
|
|
|
1161
1003
|
}
|
|
1162
1004
|
});
|
|
1163
1005
|
}
|
|
1164
|
-
function preserveExistingIndex(doFullDelete, dirRecords, sourceRoots) {
|
|
1165
|
-
if (!doFullDelete)
|
|
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
1006
|
/**
|
|
1198
1007
|
* Phase 2 (sync): write all pre-generated scan records inside a single
|
|
1199
1008
|
* transaction, returning the directories that still need LLM enrichment.
|
|
1200
1009
|
*/
|
|
1201
|
-
function persistDirRecords(db, dirRecords,
|
|
1010
|
+
function persistDirRecords(db, dirRecords, warnings, bundleByRoot) {
|
|
1202
1011
|
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 };
|
|
1214
1012
|
// Per-source dedup: the same logical asset can appear more than once within
|
|
1215
1013
|
// one owning source, where source order still makes the first occurrence win.
|
|
1216
1014
|
// The owner is part of the key so identical concepts in different bundles
|
|
1217
1015
|
// remain distinct indexed rows.
|
|
1218
1016
|
const indexedAssetIdentities = new Set();
|
|
1219
1017
|
const deletedUsageEntryIds = new Set();
|
|
1018
|
+
const findPersisted = db.prepare("SELECT id, content_hash, file_path, adapter_id, json_extract(document_json, '$.quality') AS quality " +
|
|
1019
|
+
"FROM entries WHERE item_ref = ?");
|
|
1220
1020
|
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) {
|
|
1021
|
+
for (const { dirPath, currentStashDir, files, fingerprint, stash, skip, reason, persistedRowCount, hashByFile, conceptIdByFile, indexVariant, remove, } of dirRecords) {
|
|
1248
1022
|
const bundle = bundleByRoot.get(path.resolve(currentStashDir));
|
|
1249
1023
|
if (!bundle)
|
|
1250
1024
|
throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
|
|
@@ -1261,7 +1035,13 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1261
1035
|
// row_count and the gate skips this directory before draining next
|
|
1262
1036
|
// time. "unchanged-precheck" already matched the stored row.
|
|
1263
1037
|
if (reason?.kind === "unchanged" && fingerprint) {
|
|
1264
|
-
upsertIndexDirState(db, {
|
|
1038
|
+
upsertIndexDirState(db, {
|
|
1039
|
+
dirPath,
|
|
1040
|
+
...fingerprint,
|
|
1041
|
+
reason: reason.kind,
|
|
1042
|
+
rowCount: persistedRowCount,
|
|
1043
|
+
indexVariant,
|
|
1044
|
+
});
|
|
1265
1045
|
}
|
|
1266
1046
|
continue;
|
|
1267
1047
|
}
|
|
@@ -1271,8 +1051,19 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1271
1051
|
// durable `item_ref`, then prune only the departed rows below. Replaces the old
|
|
1272
1052
|
// `deleteEntriesByDir` truncate-and-reinsert (which discarded ids).
|
|
1273
1053
|
const keptItemRefs = new Set();
|
|
1054
|
+
// Per-file cursor: a directory is drained whole (one changed file
|
|
1055
|
+
// re-reads its siblings), but on an incremental run a sibling whose
|
|
1056
|
+
// content hash, path and adapter are unchanged since the last drain
|
|
1057
|
+
// under the same adapter variant is already persisted exactly as this
|
|
1058
|
+
// drain would persist it — including any LLM enrichment layered onto its
|
|
1059
|
+
// row — so it is neither rewritten nor re-enriched. `--full` re-persists
|
|
1060
|
+
// every entry.
|
|
1061
|
+
const sameVariant = reason?.kind !== "full-rebuild" &&
|
|
1062
|
+
indexVariant !== undefined &&
|
|
1063
|
+
getIndexDirState(db, dirPath)?.indexVariant === indexVariant;
|
|
1274
1064
|
let persistedRows = 0;
|
|
1275
1065
|
let dedupedRows = 0;
|
|
1066
|
+
const entriesToEnrich = [];
|
|
1276
1067
|
if (stash) {
|
|
1277
1068
|
const ownerIdentity = bundle.bundleId;
|
|
1278
1069
|
for (const entry of stash.entries) {
|
|
@@ -1294,29 +1085,42 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1294
1085
|
continue;
|
|
1295
1086
|
}
|
|
1296
1087
|
indexedAssetIdentities.add(identityKey);
|
|
1297
|
-
const searchText = buildSearchText(entry);
|
|
1298
|
-
const entryWithSize = attachFileSize(entry, entryPath);
|
|
1299
1088
|
// content_hash = doc.hash from the drain, keyed by the recognized
|
|
1300
1089
|
// file's path. A missing hash preserves the existing value on upsert.
|
|
1301
1090
|
const contentHash = hashByFile?.get(entryPath);
|
|
1302
1091
|
const provenance = deriveEntryProvenance(bundle, entry.type, entry.name, adapterConceptId);
|
|
1303
1092
|
keptItemRefs.add(provenance.itemRef);
|
|
1304
|
-
upsertEntry(db, entryPath, entryWithSize, searchText, provenance, contentHash);
|
|
1305
1093
|
persistedRows++;
|
|
1094
|
+
const previous = sameVariant
|
|
1095
|
+
? (findPersisted.get(provenance.itemRef) ?? undefined)
|
|
1096
|
+
: undefined;
|
|
1097
|
+
const unchanged = previous !== undefined &&
|
|
1098
|
+
contentHash !== undefined &&
|
|
1099
|
+
previous.content_hash === contentHash &&
|
|
1100
|
+
previous.file_path === entryPath &&
|
|
1101
|
+
previous.adapter_id === bundle.adapterId;
|
|
1102
|
+
if (unchanged) {
|
|
1103
|
+
// An unchanged row that was never enriched still wants the LLM
|
|
1104
|
+
// pass (the cache decides whether a call is needed).
|
|
1105
|
+
if (entry.quality === "generated" && previous.quality !== "enriched")
|
|
1106
|
+
entriesToEnrich.push(entry);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance, contentHash);
|
|
1110
|
+
if (entry.quality === "generated")
|
|
1111
|
+
entriesToEnrich.push(entry);
|
|
1306
1112
|
}
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
dirsNeedingLlm.push({ dirPath, files, currentStashDir, stash });
|
|
1113
|
+
// Only "generated" entries (never user-curated ones) are enriched.
|
|
1114
|
+
if (entriesToEnrich.length > 0) {
|
|
1115
|
+
dirsNeedingLlm.push({ dirPath, files, currentStashDir, stash: { entries: entriesToEnrich } });
|
|
1311
1116
|
}
|
|
1312
1117
|
}
|
|
1313
1118
|
// Prune the departed rows: everything under this dir NOT re-upserted above
|
|
1314
1119
|
// (files deleted, deduped away, or abstained on by the adapter). With
|
|
1315
1120
|
// an empty kept-set this deletes every row for the dir — the exact net
|
|
1316
1121
|
// effect of the old unconditional `deleteEntriesByDir`, minus the id churn.
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1122
|
+
// (A source whose walk was incomplete never reaches this point.)
|
|
1123
|
+
addEntryIds(deletedUsageEntryIds, deleteEntriesByDirExceptRefs(db, dirPath, bundle.bundleId, keptItemRefs, { cleanupUsageEvents: false }));
|
|
1320
1124
|
const persistedFingerprint = fingerprint ?? computeDirFingerprint(dirPath, files, indexVariant);
|
|
1321
1125
|
const persistedReason = persistedRows === 0
|
|
1322
1126
|
? inferZeroRowReason(stash, reason, warnings, dirPath, dedupedRows)
|
|
@@ -1332,6 +1136,7 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1332
1136
|
// must keep draining every run (as it did before the gate) until a
|
|
1333
1137
|
// drain persists it without dedup. NULL keeps the gate closed.
|
|
1334
1138
|
rowCount: dedupedRows === 0 ? persistedRows : undefined,
|
|
1139
|
+
indexVariant,
|
|
1335
1140
|
});
|
|
1336
1141
|
if (persistedRows === 0) {
|
|
1337
1142
|
// Warn only when the dir had files that *could* produce entries (.md or
|
|
@@ -1352,30 +1157,12 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1352
1157
|
}
|
|
1353
1158
|
// Atomicity observation point: the new generation is fully written but
|
|
1354
1159
|
// uncommitted, so it must still be invisible to other connections.
|
|
1355
|
-
|
|
1160
|
+
indexTransactionHookForTests?.("records-persisted");
|
|
1356
1161
|
});
|
|
1357
1162
|
insertTransaction();
|
|
1358
1163
|
deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
|
|
1359
1164
|
return { dirsNeedingLlm };
|
|
1360
1165
|
}
|
|
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
1166
|
function indexedProvenanceForFile(db, filePath) {
|
|
1380
1167
|
const row = db
|
|
1381
1168
|
.prepare("SELECT item_ref AS itemRef, bundle_id AS bundleId, component_id AS componentId, " +
|
|
@@ -1392,7 +1179,7 @@ function indexedProvenanceForFile(db, filePath) {
|
|
|
1392
1179
|
adapterId: row.adapterId,
|
|
1393
1180
|
};
|
|
1394
1181
|
}
|
|
1395
|
-
async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgress, signal, onNotices
|
|
1182
|
+
async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgress, signal, onNotices) {
|
|
1396
1183
|
// The invocation owns one frozen symbolic selection. Summary reporting and
|
|
1397
1184
|
// every enrichment dispatch consume this same snapshot.
|
|
1398
1185
|
const llmRunner = execution.runner;
|
|
@@ -1405,39 +1192,15 @@ async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgr
|
|
|
1405
1192
|
let completedDirs = 0;
|
|
1406
1193
|
let completedEntries = 0;
|
|
1407
1194
|
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);
|
|
1195
|
+
const totalEntries = dirsNeedingLlm.reduce((sum, { stash }) => sum + stash.entries.filter((e) => e.quality === "generated" && !isEnrichmentComplete(e)).length, 0);
|
|
1418
1196
|
// P3 — wall-clock budget for the enrichment pass. Defaults to the resolved
|
|
1419
1197
|
// engine's timeoutMs (or 10 minutes if not set). Users can extend it via
|
|
1420
1198
|
// `index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`, or the
|
|
1421
1199
|
// engine's own `engines.<name>.timeoutMs`) — no separate knob needed.
|
|
1422
1200
|
const enrichDeadline = createEnrichmentDeadline(llmRunner.timeoutMs, totalEntries);
|
|
1423
|
-
|
|
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
|
-
})();
|
|
1201
|
+
const enrichSignal = AbortSignal.any([signal, enrichDeadline].filter((s) => s !== undefined));
|
|
1439
1202
|
if (totalEntries > 0) {
|
|
1440
|
-
onProgress
|
|
1203
|
+
onProgress({
|
|
1441
1204
|
phase: "llm",
|
|
1442
1205
|
message: `LLM enhancement starting for ${totalEntries} entr${totalEntries === 1 ? "y" : "ies"} ` +
|
|
1443
1206
|
`across ${totalDirs} director${totalDirs === 1 ? "y" : "ies"} (concurrency ${getDefaultLlmConcurrency(llmRunner.connection)}).`,
|
|
@@ -1449,7 +1212,7 @@ async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgr
|
|
|
1449
1212
|
let configFailure;
|
|
1450
1213
|
let lastProgressAt = Date.now();
|
|
1451
1214
|
let heartbeatTimer;
|
|
1452
|
-
if (totalEntries > 0
|
|
1215
|
+
if (totalEntries > 0) {
|
|
1453
1216
|
heartbeatTimer = setInterval(() => {
|
|
1454
1217
|
if (Date.now() - lastProgressAt < 15000)
|
|
1455
1218
|
return;
|
|
@@ -1483,7 +1246,7 @@ async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgr
|
|
|
1483
1246
|
if (entriesToEnhance.length === 0)
|
|
1484
1247
|
return undefined;
|
|
1485
1248
|
currentDirLabel = path.relative(currentStashDir, dirPath) || ".";
|
|
1486
|
-
onProgress
|
|
1249
|
+
onProgress({
|
|
1487
1250
|
phase: "llm",
|
|
1488
1251
|
message: `Enhancing ${currentDirLabel} ` +
|
|
1489
1252
|
`(${entriesToEnhance.length} entr${entriesToEnhance.length === 1 ? "y" : "ies"}).`,
|
|
@@ -1491,27 +1254,35 @@ async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgr
|
|
|
1491
1254
|
total: totalEntries,
|
|
1492
1255
|
});
|
|
1493
1256
|
lastProgressAt = Date.now();
|
|
1494
|
-
const
|
|
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
|
-
});
|
|
1257
|
+
const entryPathOf = (entry) => entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
1258
|
+
const itemRefs = entriesToEnhance.map((entry) => indexedProvenanceForFile(db, entryPathOf(entry)).itemRef);
|
|
1499
1259
|
let enhanced;
|
|
1500
1260
|
try {
|
|
1501
|
-
enhanced = await
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1261
|
+
enhanced = await enhanceEntriesWithLlm({
|
|
1262
|
+
llmRunner,
|
|
1263
|
+
entries: entriesToEnhance,
|
|
1264
|
+
itemRefs,
|
|
1265
|
+
files,
|
|
1266
|
+
db,
|
|
1267
|
+
config,
|
|
1268
|
+
summary,
|
|
1269
|
+
signal: enrichSignal,
|
|
1270
|
+
onNotices,
|
|
1271
|
+
onEntryDone: (event) => {
|
|
1272
|
+
completedEntries++;
|
|
1273
|
+
lastProgressAt = Date.now();
|
|
1274
|
+
onProgress({
|
|
1275
|
+
phase: "llm",
|
|
1276
|
+
message: `Enhanced ${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"}; ` +
|
|
1277
|
+
`${completedDirs}/${totalDirs} director${totalDirs === 1 ? "y" : "ies"} complete` +
|
|
1278
|
+
(event.entryName ? `; current ${event.entryName}` : "") +
|
|
1279
|
+
(currentDirLabel ? ` in ${currentDirLabel}` : "") +
|
|
1280
|
+
(event.outcome === "cache-hit" ? " (cache hit)" : ""),
|
|
1281
|
+
processed: completedEntries,
|
|
1282
|
+
total: totalEntries,
|
|
1283
|
+
});
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1515
1286
|
}
|
|
1516
1287
|
catch (err) {
|
|
1517
1288
|
if (err instanceof ConfigError) {
|
|
@@ -1523,16 +1294,15 @@ async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgr
|
|
|
1523
1294
|
// Re-upsert the enhanced entries in a single transaction so a crash
|
|
1524
1295
|
// cannot leave half the entries updated and the rest stale.
|
|
1525
1296
|
db.transaction(() => {
|
|
1526
|
-
for (const entry of enhanced
|
|
1527
|
-
const entryPath =
|
|
1528
|
-
const searchText = buildSearchText(entry);
|
|
1297
|
+
for (const entry of enhanced) {
|
|
1298
|
+
const entryPath = entryPathOf(entry);
|
|
1529
1299
|
const provenance = indexedProvenanceForFile(db, entryPath);
|
|
1530
|
-
upsertEntry(db, entryPath,
|
|
1300
|
+
upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance);
|
|
1531
1301
|
}
|
|
1532
1302
|
})();
|
|
1533
1303
|
completedDirs++;
|
|
1534
1304
|
lastProgressAt = Date.now();
|
|
1535
|
-
onProgress
|
|
1305
|
+
onProgress({
|
|
1536
1306
|
phase: "llm",
|
|
1537
1307
|
message: `Completed ${completedDirs}/${totalDirs} director${totalDirs === 1 ? "y" : "ies"}; ` +
|
|
1538
1308
|
`${completedEntries}/${totalEntries} entr${totalEntries === 1 ? "y" : "ies"} processed.`,
|
|
@@ -1553,7 +1323,7 @@ async function enhanceDirsWithLlm(db, config, execution, dirsNeedingLlm, onProgr
|
|
|
1553
1323
|
if (heartbeatTimer)
|
|
1554
1324
|
clearInterval(heartbeatTimer);
|
|
1555
1325
|
}
|
|
1556
|
-
if (
|
|
1326
|
+
if (enrichDeadline?.aborted) {
|
|
1557
1327
|
warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase index.enrichment.timeoutMs for a larger budget.");
|
|
1558
1328
|
}
|
|
1559
1329
|
// Gate-closed (`skipped`) entries are not failures — exclude them so a
|
|
@@ -1574,119 +1344,70 @@ export function createEnrichmentDeadline(timeoutMs, totalEntries) {
|
|
|
1574
1344
|
return perEntryTimeoutMs === null ? undefined : AbortSignal.timeout(perEntryTimeoutMs * Math.max(totalEntries, 1));
|
|
1575
1345
|
}
|
|
1576
1346
|
// ── 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
1347
|
function buildIndexSummaryMessage(options) {
|
|
1589
1348
|
const stashSourceLabel = options.sourcesCount === 1 ? "stash source" : "stash sources";
|
|
1590
|
-
const semanticDetail =
|
|
1349
|
+
const semanticDetail = options.semanticSearchMode === "off" ? "disabled" : `${options.embeddingProvider} embeddings`;
|
|
1591
1350
|
return `Starting ${options.mode} index (${options.sourcesCount} ${stashSourceLabel}, semantic search: ${semanticDetail}, LLM: ${options.llmEnabled ? "enabled" : "disabled"}).`;
|
|
1592
1351
|
}
|
|
1593
1352
|
function getEmbeddingProvider(embedding) {
|
|
1594
1353
|
return isHttpUrl(embedding?.endpoint) ? "remote" : "local";
|
|
1595
1354
|
}
|
|
1596
|
-
function getSemanticSearchLabel(semanticSearchMode, embeddingProvider, vecAvailable) {
|
|
1597
|
-
if (semanticSearchMode === "off")
|
|
1598
|
-
return "disabled";
|
|
1599
|
-
return `${embeddingProvider} embeddings, ${vecAvailable ? "sqlite-vec" : "JS fallback"}`;
|
|
1600
|
-
}
|
|
1601
1355
|
function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
|
|
1602
1356
|
const embeddingCount = getEmbeddingCount(db);
|
|
1603
|
-
const vecAvailable = isVecAvailable(db);
|
|
1604
1357
|
const embeddingProvider = getEmbeddingProvider(config.embedding);
|
|
1358
|
+
const verification = (ok, message, semanticSearchEnabled, semanticStatus, guidance) => ({
|
|
1359
|
+
ok,
|
|
1360
|
+
message,
|
|
1361
|
+
...(guidance !== undefined ? { guidance } : {}),
|
|
1362
|
+
semanticSearchEnabled,
|
|
1363
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
1364
|
+
semanticStatus,
|
|
1365
|
+
embeddingProvider,
|
|
1366
|
+
entryCount: embeddableEntries,
|
|
1367
|
+
embeddingCount,
|
|
1368
|
+
});
|
|
1369
|
+
const pendingStatus = config.semanticSearchMode === "off" ? "disabled" : "pending";
|
|
1605
1370
|
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
|
-
};
|
|
1371
|
+
return verification(true, "Index ready. No assets were found yet.", config.semanticSearchMode === "auto", pendingStatus);
|
|
1617
1372
|
}
|
|
1618
1373
|
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
|
-
};
|
|
1374
|
+
return verification(true, "Keyword index ready. Semantic search is disabled.", false, "disabled");
|
|
1630
1375
|
}
|
|
1631
1376
|
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));
|
|
1377
|
+
return verification(true, `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings).`, true, "ready-js");
|
|
1681
1378
|
}
|
|
1682
|
-
return
|
|
1379
|
+
return verification(false, embeddingResult.message ??
|
|
1380
|
+
`Semantic search verification failed (${embeddingCount}/${embeddableEntries} embeddings available).`, true, "blocked", embeddingProvider === "remote"
|
|
1381
|
+
? "Check your embedding endpoint and credentials, then retry `akm index --full --verbose`."
|
|
1382
|
+
: "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.");
|
|
1683
1383
|
}
|
|
1684
|
-
|
|
1384
|
+
/**
|
|
1385
|
+
* Enrich each entry through the LLM. `llm_enrichment_cache` (keyed by the
|
|
1386
|
+
* canonical item ref + file body hash) skips the call when the body is
|
|
1387
|
+
* unchanged.
|
|
1388
|
+
*/
|
|
1389
|
+
async function enhanceEntriesWithLlm(args) {
|
|
1390
|
+
const { llmRunner, entries, itemRefs, files, db, config, summary, signal, onEntryDone, onNotices } = args;
|
|
1685
1391
|
const { enhanceMetadata } = await import("../llm/metadata-enhance.js");
|
|
1686
|
-
|
|
1392
|
+
// failureSamples is bounded to 3 items, so a linear scan is cheaper than a
|
|
1393
|
+
// parallel Set for membership checks (#177 review).
|
|
1394
|
+
const recordFailure = (message) => {
|
|
1395
|
+
if (summary.failureSamples.length < 3 && !summary.failureSamples.includes(message)) {
|
|
1396
|
+
summary.failureSamples.push(message);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
// The non-empty enrichment fields replace the entry's, and the entry is
|
|
1400
|
+
// marked enriched so later runs skip it (P2).
|
|
1401
|
+
const withEnrichment = (entry, metadata) => ({
|
|
1402
|
+
...entry,
|
|
1403
|
+
...(metadata.description ? { description: metadata.description } : {}),
|
|
1404
|
+
...(metadata.searchHints?.length ? { searchHints: metadata.searchHints } : {}),
|
|
1405
|
+
...(metadata.tags?.length ? { tags: metadata.tags } : {}),
|
|
1406
|
+
quality: "enriched",
|
|
1407
|
+
});
|
|
1687
1408
|
let configFailure;
|
|
1688
|
-
const results = await concurrentMap(
|
|
1689
|
-
if (signal
|
|
1409
|
+
const results = await concurrentMap(entries, async (entry, idx) => {
|
|
1410
|
+
if (signal.aborted)
|
|
1690
1411
|
return entry;
|
|
1691
1412
|
summary.attempted++;
|
|
1692
1413
|
try {
|
|
@@ -1702,36 +1423,23 @@ async function enhanceStashWithLlm(llmRunner, stash, files, summary, signal, db,
|
|
|
1702
1423
|
warn(`Could not read file for LLM enrichment: ${entry.filename ?? entry.name}`);
|
|
1703
1424
|
}
|
|
1704
1425
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const cacheBody = fileContent ?? `${entry.name}\n${entry.description ?? ""}`;
|
|
1708
|
-
const bodyHash = computeBodyHash(cacheBody);
|
|
1709
|
-
const cacheKey = itemRefs?.[idx];
|
|
1426
|
+
const bodyHash = enrichmentBodyHash(entry, fileContent);
|
|
1427
|
+
const cacheKey = itemRefs[idx];
|
|
1710
1428
|
if (!cacheKey)
|
|
1711
1429
|
throw new Error(`Missing canonical item ref for enrichment entry ${entry.name}.`);
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
-
}
|
|
1430
|
+
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
1431
|
+
if (cached) {
|
|
1432
|
+
try {
|
|
1433
|
+
const updated = withEnrichment(entry, JSON.parse(cached.resultJson));
|
|
1434
|
+
summary.succeeded++;
|
|
1435
|
+
onEntryDone({ entryName: entry.name, outcome: "cache-hit" });
|
|
1436
|
+
return updated;
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
warn(`LLM enrichment cache entry corrupt for ${entry.name}; re-running enrichment`);
|
|
1732
1440
|
}
|
|
1733
1441
|
}
|
|
1734
|
-
const outcome = await enhanceMetadata(llmRunner, entry, fileContent, signal,
|
|
1442
|
+
const outcome = await enhanceMetadata(llmRunner, entry, fileContent, signal, config, onNotices);
|
|
1735
1443
|
if (outcome.status !== "enriched") {
|
|
1736
1444
|
// Not a genuine LLM success: the gate was closed (`skipped`) or the
|
|
1737
1445
|
// call errored/timed out (`failed`). Do NOT mark the entry enriched
|
|
@@ -1739,57 +1447,35 @@ async function enhanceStashWithLlm(llmRunner, stash, files, summary, signal, db,
|
|
|
1739
1447
|
// entry into a permanent enrichment skip even though nothing was
|
|
1740
1448
|
// enhanced. Surface failures honestly; stay silent on gated-off skips.
|
|
1741
1449
|
if (outcome.status === "failed") {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
summary.failureSamples.push(msg);
|
|
1745
|
-
}
|
|
1746
|
-
onEntryDone?.({ entryName: entry.name, outcome: "failed" });
|
|
1450
|
+
recordFailure(outcome.error ?? "metadata enrichment failed");
|
|
1451
|
+
onEntryDone({ entryName: entry.name, outcome: "failed" });
|
|
1747
1452
|
}
|
|
1748
1453
|
else {
|
|
1749
1454
|
summary.skipped++;
|
|
1750
|
-
onEntryDone
|
|
1455
|
+
onEntryDone({ entryName: entry.name, outcome: "skipped" });
|
|
1751
1456
|
}
|
|
1752
1457
|
return entry;
|
|
1753
1458
|
}
|
|
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
1459
|
// An empty-but-successful response is still cached: the LLM was paid
|
|
1764
1460
|
// for this body_hash and produced no improvements, so re-running would
|
|
1765
|
-
// only re-pay for the same no-op.
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
description: improvements.description,
|
|
1773
|
-
searchHints: improvements.searchHints,
|
|
1774
|
-
tags: improvements.tags,
|
|
1775
|
-
}));
|
|
1776
|
-
}
|
|
1461
|
+
// only re-pay for the same no-op.
|
|
1462
|
+
const improvements = outcome.metadata;
|
|
1463
|
+
upsertLlmCacheEntry(db, cacheKey, bodyHash, JSON.stringify({
|
|
1464
|
+
description: improvements.description,
|
|
1465
|
+
searchHints: improvements.searchHints,
|
|
1466
|
+
tags: improvements.tags,
|
|
1467
|
+
}));
|
|
1777
1468
|
summary.succeeded++;
|
|
1778
|
-
onEntryDone
|
|
1779
|
-
return
|
|
1469
|
+
onEntryDone({ entryName: entry.name, outcome: "llm" });
|
|
1470
|
+
return withEnrichment(entry, improvements);
|
|
1780
1471
|
}
|
|
1781
1472
|
catch (err) {
|
|
1782
1473
|
if (err instanceof ConfigError) {
|
|
1783
1474
|
configFailure ??= err;
|
|
1784
1475
|
return entry;
|
|
1785
1476
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
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" });
|
|
1477
|
+
recordFailure(toErrorMessage(err));
|
|
1478
|
+
onEntryDone({ entryName: entry.name, outcome: "failed" });
|
|
1793
1479
|
return entry;
|
|
1794
1480
|
}
|
|
1795
1481
|
},
|
|
@@ -1798,11 +1484,7 @@ async function enhanceStashWithLlm(llmRunner, stash, files, summary, signal, db,
|
|
|
1798
1484
|
getDefaultLlmConcurrency(llmRunner.connection));
|
|
1799
1485
|
if (configFailure)
|
|
1800
1486
|
throw configFailure;
|
|
1801
|
-
|
|
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 };
|
|
1487
|
+
return results.map((result, i) => result ?? entries[i]);
|
|
1806
1488
|
}
|
|
1807
1489
|
// ── lookup ─────────────────────────────────────────────────────────────────
|
|
1808
1490
|
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
@@ -1812,17 +1494,13 @@ async function resolveLookupSources() {
|
|
|
1812
1494
|
const { resolveSourceEntries } = await import("./search/search-source.js");
|
|
1813
1495
|
return resolveSourceEntries(undefined, loadConfig());
|
|
1814
1496
|
}
|
|
1815
|
-
function resolveLookupScope(bundle, sources) {
|
|
1816
|
-
if (!bundle)
|
|
1817
|
-
return { candidateSources: sources, qualified: false };
|
|
1818
|
-
return { candidateSources: resolveSourcesForOrigin(bundle, sources), qualified: true };
|
|
1819
|
-
}
|
|
1820
1497
|
async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
1821
1498
|
const sources = await resolveLookupSources();
|
|
1822
1499
|
if (sources.length === 0)
|
|
1823
1500
|
return { entry: null };
|
|
1824
|
-
const bundleBySourcePath = new Map(
|
|
1825
|
-
const
|
|
1501
|
+
const bundleBySourcePath = new Map(sourceComponents(sources).map((component, index) => [path.resolve(sources[index].path), component.id]));
|
|
1502
|
+
const qualified = Boolean(ref.bundle);
|
|
1503
|
+
const candidateSources = ref.bundle ? resolveSourcesForOrigin(ref.bundle, sources) : sources;
|
|
1826
1504
|
if (candidateSources.length === 0)
|
|
1827
1505
|
return { entry: null };
|
|
1828
1506
|
let db;
|
|
@@ -1833,6 +1511,11 @@ async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
|
1833
1511
|
catch (error) {
|
|
1834
1512
|
indexError = error;
|
|
1835
1513
|
}
|
|
1514
|
+
const resolved = (entry, owner) => ({
|
|
1515
|
+
entry,
|
|
1516
|
+
...(owner ? { owner } : {}),
|
|
1517
|
+
...(indexError === undefined ? {} : { indexError }),
|
|
1518
|
+
});
|
|
1836
1519
|
try {
|
|
1837
1520
|
for (const source of candidateSources) {
|
|
1838
1521
|
const adapterId = source.adapterId ?? detectAdapterId(source.path);
|
|
@@ -1855,21 +1538,21 @@ async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
|
1855
1538
|
if (!(error instanceof WorkflowSourceIdentityError))
|
|
1856
1539
|
throw error;
|
|
1857
1540
|
warn(`${error.message} Falling back to the physical owner.`);
|
|
1858
|
-
return
|
|
1541
|
+
return resolved(null, owner);
|
|
1859
1542
|
}
|
|
1860
1543
|
}
|
|
1861
1544
|
else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
|
|
1862
|
-
return
|
|
1545
|
+
return resolved(null, owner);
|
|
1863
1546
|
}
|
|
1864
|
-
return
|
|
1547
|
+
return resolved(entry, owner);
|
|
1865
1548
|
}
|
|
1866
1549
|
}
|
|
1867
1550
|
// A physical owner with a missing/incomplete index row still owns this
|
|
1868
1551
|
// unqualified concept. Stop here so a later source cannot retarget it.
|
|
1869
1552
|
if (owner)
|
|
1870
|
-
return
|
|
1553
|
+
return resolved(null, owner);
|
|
1871
1554
|
}
|
|
1872
|
-
return
|
|
1555
|
+
return resolved(null);
|
|
1873
1556
|
}
|
|
1874
1557
|
finally {
|
|
1875
1558
|
if (db)
|
|
@@ -1959,16 +1642,11 @@ const USAGE_EVENT_RETENTION_DAYS = 90;
|
|
|
1959
1642
|
*
|
|
1960
1643
|
* Called during `akm index` after FTS rebuild.
|
|
1961
1644
|
*/
|
|
1962
|
-
export function recomputeUtilityScores(db, stateDb
|
|
1645
|
+
export function recomputeUtilityScores(db, stateDb) {
|
|
1963
1646
|
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
1647
|
// Purge stale usage events (90-day retention). usage_events lives in state.db
|
|
1970
1648
|
// (Chunk-8 WI-8.3); its table is created by state migration 020.
|
|
1971
|
-
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS
|
|
1649
|
+
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
|
|
1972
1650
|
// Time-proportional decay: apply one round of EMA per elapsed day so
|
|
1973
1651
|
// indexing frequency doesn't affect how fast scores decay.
|
|
1974
1652
|
const lastComputedAt = getMeta(db, "last_utility_computed_at");
|
|
@@ -1999,7 +1677,7 @@ export function recomputeUtilityScores(db, stateDb, options) {
|
|
|
1999
1677
|
ELSE NULL
|
|
2000
1678
|
END
|
|
2001
1679
|
) AS last_used_at
|
|
2002
|
-
FROM
|
|
1680
|
+
FROM usage_events u
|
|
2003
1681
|
WHERE u.entry_id IS NOT NULL
|
|
2004
1682
|
AND u.source = 'user'
|
|
2005
1683
|
GROUP BY u.entry_id
|