akm-cli 0.9.0-rc.1 → 0.9.0-rc.13
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 +1190 -52
- package/README.md +62 -37
- package/SECURITY.md +46 -31
- package/dist/akm +162 -38
- package/dist/akm-migrate +44 -0
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/hints/cli-hints-full.md +268 -118
- package/dist/assets/hints/cli-hints-short.md +87 -24
- package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
- package/dist/assets/improve-strategies/frequent.json +15 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
- package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
- package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
- package/dist/assets/improve-strategies/reflect-distill.json +30 -0
- package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +5 -5
- package/dist/assets/prompts/extract-session.md +2 -6
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
- package/dist/assets/prompts/reflect-output-repair.md +3 -0
- package/dist/assets/stash-skeleton/README.md +38 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
- package/dist/assets/tasks/core/extract.yml +3 -2
- package/dist/assets/tasks/core/improve.yml +2 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +2 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
- package/dist/assets/templates/html/health.html +5 -4
- package/dist/assets/workflows/workflow-template.md +31 -15
- package/dist/cli/invocation.js +279 -0
- package/dist/cli/parse-args.js +5 -90
- package/dist/cli/retired-commands.js +78 -0
- package/dist/cli/shared.js +158 -48
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +747 -293
- package/dist/commands/agent/agent-dispatch.js +19 -18
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +43 -97
- package/dist/commands/completions.js +80 -23
- package/dist/commands/config-cli.js +44 -281
- package/dist/commands/env/env-binding.js +13 -9
- package/dist/commands/env/env-cli.js +76 -159
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +45 -61
- package/dist/commands/env/secret.js +32 -62
- package/dist/commands/feedback-cli.js +179 -85
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +7 -8
- package/dist/commands/health/checks.js +279 -94
- package/dist/commands/health/html-report.js +197 -578
- package/dist/commands/health/improve-metrics.js +277 -246
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +16 -7
- package/dist/commands/health/metrics.js +67 -32
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +508 -0
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +16 -56
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
- package/dist/commands/health/types-improve.js +29 -0
- package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
- package/dist/commands/health/types-result.js +7 -0
- package/dist/commands/health/types-runs.js +4 -0
- package/dist/commands/health/types-session-log.js +4 -0
- package/dist/commands/health/types-windows.js +4 -0
- package/dist/commands/health/types.js +26 -21
- package/dist/commands/health/windows.js +2 -3
- package/dist/commands/health.js +296 -167
- package/dist/commands/improve/anti-collapse.js +5 -5
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +65 -52
- package/dist/commands/improve/consolidate/chunking.js +9 -7
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +454 -1354
- package/dist/commands/improve/content-hash.js +39 -0
- package/dist/commands/improve/distill/content-repair.js +4 -10
- package/dist/commands/improve/distill/promote-memory.js +89 -64
- package/dist/commands/improve/distill/quality-gate.js +118 -42
- package/dist/commands/improve/distill-guards.js +1 -1
- package/dist/commands/improve/distill-promotion-policy.js +33 -888
- package/dist/commands/improve/distill.js +607 -363
- package/dist/commands/improve/eligibility.js +165 -79
- package/dist/commands/improve/extract-cli.js +35 -126
- package/dist/commands/improve/extract-prompt.js +6 -35
- package/dist/commands/improve/extract.js +640 -391
- package/dist/commands/improve/feedback-valence.js +2 -12
- package/dist/commands/improve/improve-cli.js +134 -135
- package/dist/commands/improve/improve-result-file.js +30 -50
- package/dist/commands/improve/improve-run-types.js +4 -0
- package/dist/commands/improve/improve-strategies.js +135 -0
- package/dist/commands/improve/improve.js +904 -701
- package/dist/commands/improve/locks.js +64 -111
- package/dist/commands/improve/loop-stages.js +1110 -923
- package/dist/commands/improve/memory/derived-ref.js +124 -0
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
- package/dist/commands/improve/memory/memory-improve.js +25 -37
- package/dist/commands/improve/outcome-loop.js +25 -88
- package/dist/commands/improve/preparation.js +1034 -813
- package/dist/commands/improve/proactive-maintenance.js +34 -9
- package/dist/commands/improve/proposal-envelope.js +31 -0
- package/dist/commands/improve/reflect.js +983 -794
- package/dist/commands/improve/run-context.js +119 -0
- package/dist/commands/improve/salience.js +24 -127
- package/dist/commands/improve/session-asset.js +7 -3
- package/dist/commands/improve/shared.js +14 -34
- package/dist/commands/improve/source-identity.js +28 -0
- package/dist/commands/improve/triage.js +20 -17
- package/dist/commands/lint/base-linter.js +340 -313
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +185 -30
- package/dist/commands/{events.js → log.js} +28 -38
- package/dist/commands/migrate-cli.js +54 -0
- package/dist/commands/migration-tool.js +55 -0
- package/dist/commands/observability-cli.js +70 -208
- package/dist/commands/proposal/diff-format.js +50 -0
- package/dist/commands/proposal/drain-policies.js +0 -6
- package/dist/commands/proposal/drain.js +91 -40
- package/dist/commands/proposal/proposal-cli.js +134 -132
- package/dist/commands/proposal/proposal-types.js +56 -0
- package/dist/commands/proposal/proposal.js +83 -65
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +105 -88
- package/dist/commands/proposal/repository.js +1303 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
- package/dist/commands/proposal/validators/proposal-validators.js +61 -12
- package/dist/commands/proposal/validators/proposals.js +6 -8
- package/dist/commands/read/curate.js +78 -73
- package/dist/commands/read/knowledge.js +510 -13
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +84 -15
- package/dist/commands/read/search-cli.js +203 -96
- package/dist/commands/read/search.js +126 -94
- package/dist/commands/read/show.js +226 -250
- package/dist/commands/registry-cli.js +34 -60
- package/dist/commands/remember.js +18 -57
- package/dist/commands/sources/add-cli.js +104 -49
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +63 -0
- package/dist/commands/sources/info.js +27 -15
- package/dist/commands/sources/init.js +30 -40
- package/dist/commands/sources/installed-stashes.js +469 -172
- package/dist/commands/sources/schema-repair.js +10 -9
- package/dist/commands/sources/self-update.js +182 -121
- package/dist/commands/sources/source-add.js +169 -178
- package/dist/commands/sources/source-clone.js +144 -41
- package/dist/commands/sources/source-manage.js +94 -59
- package/dist/commands/sources/sources-cli.js +64 -205
- package/dist/commands/sources/stash-cli.js +91 -54
- package/dist/commands/sources/stash-skeleton.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +106 -104
- package/dist/commands/tasks/tasks.js +445 -262
- package/dist/commands/workflow-cli.js +75 -228
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/activation-policy.js +49 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
- package/dist/core/adapter/adapters/akm-adapter.js +528 -0
- package/dist/core/adapter/adapters/akm-lint.js +392 -0
- package/dist/core/adapter/adapters/akm-metadata.js +387 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
- package/dist/core/adapter/adapters/claude-adapter.js +61 -0
- package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
- package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
- package/dist/core/adapter/adapters/index.js +80 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
- package/dist/core/adapter/adapters/okf-adapter.js +391 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
- package/dist/core/adapter/adapters/shared.js +286 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
- package/dist/core/adapter/bundle-adapter.js +4 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +44 -0
- package/dist/core/adapter/registry.js +56 -0
- package/dist/core/adapter/types.js +4 -0
- package/dist/core/asset/akm-markdown.js +30 -0
- package/dist/core/asset/asset-placement.js +243 -0
- package/dist/core/asset/asset-ref.js +110 -79
- package/dist/core/asset/asset-serialize.js +20 -0
- package/dist/core/asset/frontmatter.js +28 -12
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +274 -0
- package/dist/core/asset/stash-meta.js +2 -2
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +281 -86
- package/dist/core/config/config-io.js +42 -128
- package/dist/core/config/config-schema.js +233 -855
- package/dist/core/config/config-sources.js +162 -39
- package/dist/core/config/config-types.js +16 -11
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +126 -37
- package/dist/core/config/config.js +154 -331
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +28 -0
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +38 -0
- package/dist/core/config/schema/engines.js +116 -0
- package/dist/core/config/schema/experimental.js +47 -0
- package/dist/core/config/schema/feedback.js +31 -0
- package/dist/core/config/schema/improve-processes.js +389 -0
- package/dist/core/config/schema/improve.js +94 -0
- package/dist/core/config/schema/index-config.js +176 -0
- package/dist/core/config/schema/output.js +18 -0
- package/dist/core/config/schema/primitives.js +94 -0
- package/dist/core/config/schema/search.js +30 -0
- package/dist/core/config/schema/setup.js +18 -0
- package/dist/core/config/schema/sources-bundles.js +169 -0
- package/dist/core/config/schema/workflow.js +29 -0
- package/dist/core/env-secret-ref.js +155 -20
- package/dist/core/errors.js +17 -15
- package/dist/core/events-types.js +4 -0
- package/dist/core/events.js +46 -128
- package/dist/core/extra-params.js +62 -0
- package/dist/core/file-change.js +17 -0
- package/dist/core/file-lock.js +202 -57
- package/dist/core/fs-txn.js +392 -0
- package/dist/core/git-message.js +59 -0
- package/dist/core/improve-result.js +167 -0
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +1 -1
- package/dist/core/maintenance-barrier.js +135 -0
- package/dist/core/migration-operation.js +44 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/paths.js +22 -25
- package/dist/core/platform.js +10 -0
- package/dist/core/recognition-util.js +128 -0
- package/dist/core/redaction.js +392 -0
- package/dist/core/standards/resolve-standards-context.js +36 -65
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +5 -5
- package/dist/core/state/migrations.js +242 -11
- package/dist/core/state-db.js +98 -10
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +303 -0
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/time.js +20 -0
- package/dist/core/type-presentation.js +130 -0
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +834 -118
- package/dist/indexer/bundle-identity-guard.js +92 -0
- package/dist/indexer/db/graph-db.js +1 -25
- package/dist/indexer/db/llm-cache.js +1 -1
- package/dist/indexer/ensure-index.js +30 -9
- package/dist/indexer/graph/graph-boost.js +9 -30
- package/dist/indexer/graph/graph-extraction.js +41 -27
- package/dist/indexer/graph/graph-types.js +4 -0
- package/dist/indexer/index-writer-lock.js +93 -49
- package/dist/indexer/index-written-assets.js +100 -53
- package/dist/indexer/indexer.js +746 -329
- package/dist/indexer/init.js +18 -25
- package/dist/indexer/installations.js +142 -0
- package/dist/indexer/passes/dir-staleness.js +18 -10
- package/dist/indexer/passes/memory-inference.js +25 -15
- package/dist/indexer/passes/metadata.js +412 -243
- package/dist/indexer/scan/doc-to-entry.js +160 -0
- package/dist/indexer/scan/drain-dir.js +134 -0
- package/dist/indexer/search/db-search.js +292 -108
- package/dist/indexer/search/fts-query.js +64 -0
- package/dist/indexer/search/ranking-contributors.js +145 -25
- package/dist/indexer/search/ranking-types.js +4 -0
- package/dist/indexer/search/ranking.js +28 -71
- package/dist/indexer/search/search-attribution.js +67 -0
- package/dist/indexer/search/search-fields.js +18 -3
- package/dist/indexer/search/search-hit-enrichers.js +30 -40
- package/dist/indexer/search/search-source.js +157 -111
- package/dist/indexer/search/semantic-status.js +4 -1
- package/dist/indexer/usage/usage-events.js +10 -30
- package/dist/indexer/walk/file-context.js +3 -45
- package/dist/indexer/walk/matchers.js +42 -73
- package/dist/indexer/walk/path-resolver.js +11 -5
- package/dist/indexer/walk/walker.js +42 -14
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -58
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +231 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +8 -3
- package/dist/integrations/agent/profiles.js +6 -99
- package/dist/integrations/agent/prompts.js +46 -18
- package/dist/integrations/agent/runner-dispatch.js +78 -13
- package/dist/integrations/agent/runner.js +76 -208
- package/dist/integrations/agent/spawn.js +48 -279
- package/dist/integrations/harnesses/aider/agent-builder.js +9 -8
- package/dist/integrations/harnesses/aider/index.js +2 -12
- package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -16
- package/dist/integrations/harnesses/amazonq/index.js +3 -17
- package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
- package/dist/integrations/harnesses/claude/config-import.js +1 -3
- package/dist/integrations/harnesses/claude/index.js +1 -14
- package/dist/integrations/harnesses/claude/session-log.js +27 -75
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -7
- package/dist/integrations/harnesses/codex/index.js +2 -13
- package/dist/integrations/harnesses/copilot/agent-builder.js +9 -9
- package/dist/integrations/harnesses/copilot/index.js +1 -13
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -9
- package/dist/integrations/harnesses/gemini/index.js +1 -13
- package/dist/integrations/harnesses/ids.js +24 -0
- package/dist/integrations/harnesses/index.js +31 -33
- package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
- package/dist/integrations/harnesses/opencode/config-import.js +1 -3
- package/dist/integrations/harnesses/opencode/index.js +1 -18
- package/dist/integrations/harnesses/opencode/session-log.js +67 -125
- package/dist/integrations/harnesses/opencode-sdk/harness.js +3 -17
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
- package/dist/integrations/harnesses/openhands/agent-builder.js +12 -10
- package/dist/integrations/harnesses/openhands/index.js +2 -12
- package/dist/integrations/harnesses/pi/agent-builder.js +11 -18
- package/dist/integrations/harnesses/pi/index.js +3 -16
- package/dist/integrations/harnesses/shared.js +17 -0
- package/dist/integrations/harnesses/types.js +38 -34
- package/dist/integrations/lockfile.js +211 -24
- package/dist/integrations/session-logs/index.js +24 -40
- package/dist/integrations/session-logs/provider-base.js +113 -0
- package/dist/llm/client.js +182 -110
- package/dist/llm/embedders/deterministic.js +2 -2
- package/dist/llm/embedders/remote.js +21 -9
- package/dist/llm/feature-gate.js +17 -57
- package/dist/llm/graph-extract.js +12 -13
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +45 -30
- package/dist/llm/structured-call.js +16 -8
- package/dist/llm/usage-persist.js +30 -5
- package/dist/llm/usage-telemetry.js +59 -6
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/command-registry.js +27 -0
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +251 -0
- package/dist/output/html-render.js +11 -16
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +15 -281
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +58 -84
- package/dist/output/shapes/passthrough.js +8 -40
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes/registry.js +12 -6
- package/dist/output/shapes.js +0 -9
- package/dist/output/text/{init.js → bundle-create.js} +3 -1
- package/dist/output/text/bundle-show.js +7 -0
- package/dist/output/text/command-format.js +562 -0
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/helpers.js +15 -1375
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +202 -0
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/registry.js +12 -6
- package/dist/output/text/show-directives.js +117 -0
- package/dist/output/text/show-format.js +103 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +332 -0
- package/dist/output/text/workflow.js +1 -2
- package/dist/output/text.js +10 -19
- package/dist/registry/factory.js +4 -6
- package/dist/registry/origin-resolve.js +16 -27
- package/dist/registry/providers/skills-sh.js +3 -3
- package/dist/registry/providers/static-index.js +15 -25
- package/dist/registry/resolve.js +43 -94
- package/dist/registry/semver.js +43 -0
- package/dist/runtime.js +81 -12
- package/dist/scripts/akm-migrate.js +35529 -0
- package/dist/setup/detect.js +5 -7
- package/dist/setup/detected-engines.js +136 -0
- package/dist/setup/engine-config.js +100 -0
- package/dist/setup/registry-stash-loader.js +3 -3
- package/dist/setup/semantic-assets.js +12 -9
- package/dist/setup/setup.js +444 -208
- package/dist/setup/steps/connection-shared.js +120 -0
- package/dist/setup/steps/connection.js +108 -305
- package/dist/setup/steps/platforms.js +13 -12
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +21 -15
- package/dist/setup/steps/stashdir.js +6 -4
- package/dist/setup/steps/tasks.js +236 -119
- package/dist/setup/steps.js +3 -2
- package/dist/sources/freshness.js +39 -0
- package/dist/sources/provider-factory.js +11 -17
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +278 -34
- package/dist/sources/providers/git-provider.js +54 -56
- package/dist/sources/providers/git-stash.js +420 -91
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +47 -22
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +11 -10
- package/dist/sources/snapshot-fetchers/types.js +4 -0
- package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
- package/dist/storage/database.js +60 -4
- package/dist/storage/engines/sqlite-migrations.js +156 -5
- package/dist/storage/locations.js +1 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +51 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -32
- package/dist/storage/repositories/index-connection.js +79 -0
- package/dist/storage/repositories/index-db.js +4 -3
- package/dist/storage/repositories/index-entries-repository.js +863 -0
- package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
- package/dist/storage/repositories/index-entry-types.js +4 -0
- package/dist/storage/repositories/index-fts-repository.js +167 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
- package/dist/storage/repositories/index-meta-repository.js +49 -0
- package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
- package/dist/storage/repositories/index-sql.js +12 -0
- package/dist/storage/repositories/index-utility-repository.js +356 -0
- package/dist/storage/repositories/index-vec-repository.js +250 -0
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +317 -75
- package/dist/storage/repositories/registry-cache.js +1 -1
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +110 -3
- package/dist/storage/repositories/workflow-runs-repository.js +68 -35
- package/dist/tasks/backends/cron.js +169 -46
- package/dist/tasks/backends/exec-utils.js +76 -3
- package/dist/tasks/backends/index.js +6 -9
- package/dist/tasks/backends/launchd.js +292 -55
- package/dist/tasks/backends/schtasks.js +557 -70
- package/dist/tasks/backends/types.js +4 -0
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/embedded.js +56 -38
- package/dist/tasks/parser.js +156 -64
- package/dist/tasks/resolve-akm-bin.js +144 -51
- package/dist/tasks/runner.js +377 -209
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +296 -0
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/task-id.js +35 -0
- package/dist/tasks/validator.js +30 -16
- package/dist/workflows/authoring/authoring.js +96 -148
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/cli.js +0 -20
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/exec/brief.js +25 -59
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +157 -94
- package/dist/workflows/exec/report.js +365 -200
- package/dist/workflows/exec/run-workflow.js +46 -40
- package/dist/workflows/exec/scheduler.js +12 -41
- package/dist/workflows/exec/step-work.js +239 -205
- package/dist/workflows/exec/workflow-engine-gate.js +67 -0
- package/dist/workflows/ir/compile.js +141 -283
- package/dist/workflows/ir/freeze.js +233 -0
- package/dist/workflows/ir/plan-hash.js +40 -5
- package/dist/workflows/ir/schema.js +537 -1
- package/dist/workflows/parser.js +878 -306
- package/dist/workflows/program/expressions.js +20 -208
- package/dist/workflows/program/schema.js +7 -10
- package/dist/workflows/renderer.js +99 -121
- package/dist/workflows/resource-limits.js +22 -0
- package/dist/workflows/runtime/checkin.js +1 -1
- package/dist/workflows/runtime/plan-classifier.js +131 -0
- package/dist/workflows/runtime/runs.js +200 -113
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +20 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +235 -97
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +4 -26
- package/dist/workflows/validator.js +52 -30
- package/docs/README.md +42 -78
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +9 -8
- package/docs/migration/release-notes/0.9.0.md +158 -14
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +844 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/data-and-telemetry.md +333 -0
- package/package.json +21 -17
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +4636 -0
- package/schemas/akm-task.json +87 -0
- package/{dist/schemas → schemas}/akm-workflow.json +127 -82
- package/dist/akm-migrate-storage +0 -38
- package/dist/assets/help/help-accept.md +0 -12
- package/dist/assets/help/help-improve.md +0 -84
- package/dist/assets/help/help-proposals.md +0 -17
- package/dist/assets/help/help-propose.md +0 -17
- package/dist/assets/help/help-reject.md +0 -11
- package/dist/assets/profiles/frequent.json +0 -13
- package/dist/assets/profiles/recombine-only.json +0 -21
- package/dist/assets/profiles/reflect-distill.json +0 -30
- package/dist/assets/profiles/synthesize.json +0 -15
- package/dist/assets/prompts/procedural-system.md +0 -44
- package/dist/assets/prompts/recombine-system.md +0 -40
- package/dist/assets/prompts/staleness-detect-system.md +0 -6
- package/dist/assets/tasks/core/backup.yml +0 -4
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/assets/templates/html/default.html +0 -78
- package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
- package/dist/assets/wiki/index-template.md +0 -12
- package/dist/assets/wiki/ingest-workflow-template.md +0 -83
- package/dist/assets/wiki/log-template.md +0 -8
- package/dist/assets/wiki/schema-template.md +0 -61
- package/dist/cli/config-migrate.js +0 -150
- package/dist/cli/config-validate.js +0 -39
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -487
- package/dist/commands/improve/calibration.js +0 -161
- package/dist/commands/improve/dedup.js +0 -482
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/improve/hot-probation.js +0 -45
- package/dist/commands/improve/improve-auto-accept.js +0 -276
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/commands/improve/procedural.js +0 -398
- package/dist/commands/improve/recombine.js +0 -818
- package/dist/commands/improve/schema-similarity-gate.js +0 -168
- package/dist/commands/lint/agent-linter.js +0 -44
- package/dist/commands/lint/command-linter.js +0 -44
- package/dist/commands/lint/default-linter.js +0 -16
- package/dist/commands/lint/fact-linter.js +0 -39
- package/dist/commands/lint/knowledge-linter.js +0 -16
- package/dist/commands/lint/memory-linter.js +0 -61
- package/dist/commands/lint/registry.js +0 -41
- package/dist/commands/lint/skill-linter.js +0 -45
- package/dist/commands/lint/task-linter.js +0 -50
- package/dist/commands/lint/workflow-linter.js +0 -81
- package/dist/commands/proposal/legacy-import.js +0 -115
- package/dist/commands/sources/history.js +0 -196
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/commands/wiki-cli.js +0 -292
- package/dist/core/asset/asset-registry.js +0 -76
- package/dist/core/asset/asset-spec.js +0 -316
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/core/eval/rank-metrics.js +0 -113
- package/dist/core/ripgrep/install.js +0 -163
- package/dist/core/ripgrep/resolve.js +0 -81
- package/dist/indexer/db/db.js +0 -1414
- package/dist/indexer/manifest.js +0 -170
- package/dist/indexer/passes/metadata-contributors.js +0 -31
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
- package/dist/integrations/harnesses/opencode-sdk/index.js +0 -25
- package/dist/llm/call-ai.js +0 -62
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/output/shapes/distill.js +0 -14
- package/dist/output/shapes/history.js +0 -11
- package/dist/output/text/distill.js +0 -6
- package/dist/output/text/enable-disable.js +0 -8
- package/dist/output/text/history.js +0 -6
- package/dist/output/text/wiki.js +0 -16
- package/dist/registry/build-index.js +0 -386
- package/dist/schemas/akm-config.json +0 -14225
- package/dist/scripts/migrate-storage.js +0 -13169
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -10169
- package/dist/scripts/migrations/v16-to-v17.js +0 -141
- package/dist/setup/legacy-config.js +0 -106
- package/dist/storage/repositories/consolidation-repository.js +0 -38
- package/dist/storage/repositories/recombine-repository.js +0 -213
- package/dist/wiki/wiki-templates.js +0 -15
- package/dist/wiki/wiki.js +0 -1012
- package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
- package/dist/workflows/db.js +0 -350
- package/dist/workflows/exec/watch.js +0 -116
- package/dist/workflows/program/parser.js +0 -760
- package/dist/workflows/program/project.js +0 -105
- package/docs/data-and-telemetry.md +0 -227
- package/docs/migration/release-notes/0.9.0-beta.60.md +0 -19
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
package/dist/indexer/indexer.js
CHANGED
|
@@ -3,42 +3,64 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
7
|
+
import { adapterForId } from "../core/adapter/registry.js";
|
|
8
|
+
import { isHttpUrl, toErrorMessage } from "../core/common.js";
|
|
8
9
|
import { concurrentMap } from "../core/concurrent.js";
|
|
9
10
|
import { getDbPath } from "../core/paths.js";
|
|
11
|
+
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
12
|
+
import { withStateDb } from "../core/state-db.js";
|
|
10
13
|
import { isVerbose, warn, warnVerbose } from "../core/warn.js";
|
|
11
14
|
import { resolveIndexPassLLM } from "../llm/index-passes.js";
|
|
15
|
+
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
16
|
+
import { closeDatabase, openExistingDatabase, openIndexDatabase } from "../storage/repositories/index-connection.js";
|
|
17
|
+
import { deleteEntriesByDirAndStash, deleteEntriesByDirExceptKeys, deleteEntriesByIds, deleteEntriesByStashDir, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedDirPathsByStashDir, getIndexedStashDirsByDir, relinkUsageEvents, upsertEntry, upsertWorkflowDocument, } from "../storage/repositories/index-entries-repository.js";
|
|
18
|
+
import { rebuildFts } from "../storage/repositories/index-fts-repository.js";
|
|
19
|
+
import { clearStaleCacheEntries } from "../storage/repositories/index-llm-cache-repository.js";
|
|
20
|
+
import { deleteIndexDirState, deleteIndexDirStatesByStashDir, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
21
|
+
import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
22
|
+
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
12
23
|
import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
|
|
13
|
-
import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntryCount, getMeta, isVecAvailable, openExistingDatabase, openIndexDatabase, purgeEmbeddings, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
|
|
14
24
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
15
25
|
import { withIndexWriterLease } from "./index-writer-lock.js";
|
|
26
|
+
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
16
27
|
import { canUseIncrementalSkip, computeDirFingerprint, getCachedZeroRowDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
|
|
17
|
-
import {
|
|
28
|
+
import { isEnrichmentComplete, isWorkflowSkipWarning } from "./passes/metadata.js";
|
|
29
|
+
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
18
30
|
import { buildSearchText } from "./search/search-fields.js";
|
|
19
31
|
import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
32
|
+
import { purgeOldUsageEvents } from "./usage/usage-events.js";
|
|
33
|
+
import { walkStashFlatWithStatus } from "./walk/walker.js";
|
|
22
34
|
function throwIfAborted(signal) {
|
|
23
35
|
if (signal?.aborted) {
|
|
24
36
|
throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
|
-
function getDefaultLlmConcurrency(llmConfig) {
|
|
39
|
+
export function getDefaultLlmConcurrency(llmConfig) {
|
|
28
40
|
if (typeof llmConfig?.concurrency === "number")
|
|
29
41
|
return llmConfig.concurrency;
|
|
30
42
|
if (!llmConfig?.endpoint)
|
|
31
43
|
return 1;
|
|
32
44
|
try {
|
|
33
45
|
const url = new URL(llmConfig.endpoint);
|
|
34
|
-
|
|
46
|
+
// URL.hostname keeps IPv6 brackets ("[::1]") — strip them so the loopback
|
|
47
|
+
// comparison actually matches.
|
|
48
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
35
49
|
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost"))
|
|
36
50
|
return 1;
|
|
37
51
|
}
|
|
38
52
|
catch {
|
|
39
53
|
return 1;
|
|
40
54
|
}
|
|
41
|
-
|
|
55
|
+
// Remote endpoints default to a modest 2-wide pool (owner ruling 2026-07-21):
|
|
56
|
+
// enough to overlap request latency without hammering rate-limited APIs.
|
|
57
|
+
// Local model servers stay at 1 (single loaded model; parallel requests
|
|
58
|
+
// trigger reload thrash). The explicit-override branch above only fires for
|
|
59
|
+
// callers that put `concurrency` on the connection themselves —
|
|
60
|
+
// `engines.<name>.concurrency` is a valid schema field but `resolveLlmEngineUse`
|
|
61
|
+
// does NOT copy it into the resolved connection, so on the enrichment path the
|
|
62
|
+
// auto-derived 1/2 is what runs (see docs/architecture/internals/indexing.md).
|
|
63
|
+
return 2;
|
|
42
64
|
}
|
|
43
65
|
// ── Phase functions ──────────────────────────────────────────────────────────
|
|
44
66
|
/**
|
|
@@ -69,9 +91,7 @@ async function runSourceCachePhase(ctx) {
|
|
|
69
91
|
for (const dir of prevStashDirs) {
|
|
70
92
|
if (!currentSet.has(dir)) {
|
|
71
93
|
ctx.hadRemovedSources = true;
|
|
72
|
-
|
|
73
|
-
deleteIndexDirStatesByStashDir(db, dir);
|
|
74
|
-
deleteStoredGraph(db, dir);
|
|
94
|
+
ctx.removedSourceDirs.push(dir);
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
}
|
|
@@ -80,6 +100,15 @@ async function runSourceCachePhase(ctx) {
|
|
|
80
100
|
// further to do here. The flag is exposed on ctx for runWalkPhase().
|
|
81
101
|
void config;
|
|
82
102
|
}
|
|
103
|
+
function applyRemovedSources(ctx) {
|
|
104
|
+
if (!ctx.scanComplete)
|
|
105
|
+
return;
|
|
106
|
+
for (const dir of ctx.removedSourceDirs) {
|
|
107
|
+
deleteEntriesByStashDir(ctx.db, dir);
|
|
108
|
+
deleteIndexDirStatesByStashDir(ctx.db, dir);
|
|
109
|
+
deleteStoredGraph(ctx.db, dir);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
83
112
|
/**
|
|
84
113
|
* Walk phase: scan the filesystem, generate metadata, and persist entries to
|
|
85
114
|
* the database. Also kicks off LLM enrichment for directories that need it.
|
|
@@ -88,16 +117,17 @@ async function runSourceCachePhase(ctx) {
|
|
|
88
117
|
* `ctx.walkWarnings`, and `ctx.dirsNeedingLlm` for downstream phases.
|
|
89
118
|
*/
|
|
90
119
|
async function runWalkPhase(ctx) {
|
|
91
|
-
const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full,
|
|
120
|
+
const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full, clean, signal, onProgress, config } = ctx;
|
|
92
121
|
throwIfAborted(signal);
|
|
93
122
|
ctx.timing.tWalkStart = Date.now();
|
|
94
123
|
const doFullDelete = full || !isIncremental;
|
|
95
|
-
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress);
|
|
124
|
+
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings, complete } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress, !clean);
|
|
96
125
|
ctx.scannedDirs = scannedDirs;
|
|
97
126
|
ctx.skippedDirs = skippedDirs;
|
|
98
127
|
ctx.generatedCount = generatedCount;
|
|
99
128
|
ctx.walkWarnings = warnings;
|
|
100
129
|
ctx.dirsNeedingLlm = dirsNeedingLlm;
|
|
130
|
+
ctx.scanComplete = complete;
|
|
101
131
|
onProgress({
|
|
102
132
|
phase: "scan",
|
|
103
133
|
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
@@ -118,7 +148,7 @@ async function runWalkPhase(ctx) {
|
|
|
118
148
|
ctx.timing.tWalkEnd = Date.now();
|
|
119
149
|
throwIfAborted(signal);
|
|
120
150
|
// LLM enrichment for directories that need it
|
|
121
|
-
await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
151
|
+
await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal);
|
|
122
152
|
onProgress({
|
|
123
153
|
phase: "llm",
|
|
124
154
|
message: resolveIndexPassLLM("enrichment", config)
|
|
@@ -153,11 +183,20 @@ async function runFinalizePhase(ctx) {
|
|
|
153
183
|
message: isIncremental ? "Rebuilt full-text search index (dirty rows only)." : "Rebuilt full-text search index.",
|
|
154
184
|
});
|
|
155
185
|
ctx.timing.tFtsEnd = Date.now();
|
|
156
|
-
// Re-link detached usage_events and recompute utility scores.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
186
|
+
// Re-link detached usage_events and recompute utility scores. The one-time
|
|
187
|
+
// §11.4 legacy→item_ref re-key is owned by the migration cutover
|
|
188
|
+
// (020-three-db-cutover) now, so index finalize only re-resolves entry_ids
|
|
189
|
+
// (idempotent) — every stored `entry_ref` is already the item_ref spelling.
|
|
190
|
+
//
|
|
191
|
+
// Chunk-8 WI-8.3: usage_events lives in state.db now (index.db no longer holds
|
|
192
|
+
// it), so these cross-DB passes take both handles — entries in `db` (index.db),
|
|
193
|
+
// usage_events in the loaned state.db.
|
|
194
|
+
withStateDb((stateDb) => {
|
|
195
|
+
onProgress({ phase: "finalize", message: "Relinking usage events." });
|
|
196
|
+
relinkUsageEvents(db, stateDb, { sources, defaultStashDir: stashDir });
|
|
197
|
+
onProgress({ phase: "finalize", message: "Recomputing utility scores." });
|
|
198
|
+
recomputeUtilityScores(db, stateDb);
|
|
199
|
+
});
|
|
161
200
|
// Purge LLM cache entries for assets that no longer exist in the index.
|
|
162
201
|
try {
|
|
163
202
|
onProgress({ phase: "finalize", message: "Clearing stale LLM cache entries." });
|
|
@@ -166,22 +205,22 @@ async function runFinalizePhase(ctx) {
|
|
|
166
205
|
catch {
|
|
167
206
|
/* ignore */
|
|
168
207
|
}
|
|
169
|
-
// Regenerate each wiki's index.md from its pages' frontmatter. Best-effort.
|
|
170
|
-
try {
|
|
171
|
-
onProgress({ phase: "finalize", message: "Regenerating wiki indexes." });
|
|
172
|
-
const { regenerateAllWikiIndexes } = await import("../wiki/wiki.js");
|
|
173
|
-
regenerateAllWikiIndexes(stashDir);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
/* best-effort */
|
|
177
|
-
}
|
|
178
208
|
throwIfAborted(signal);
|
|
179
|
-
//
|
|
209
|
+
// An incomplete run preserves the prior freshness watermark. Advancing it
|
|
210
|
+
// could make a recovered source look unchanged even though this run never
|
|
211
|
+
// persisted its files.
|
|
180
212
|
const embeddingResult = ctx.embeddingResult ?? { success: false };
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
213
|
+
if (ctx.scanComplete) {
|
|
214
|
+
setMeta(db, "builtAt", new Date().toISOString());
|
|
215
|
+
setMeta(db, "stashDir", stashDir);
|
|
216
|
+
setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
|
|
217
|
+
}
|
|
184
218
|
setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
|
|
219
|
+
// Stash-organization conventions (SPEC-8): track which `index.indexBodyOpening`
|
|
220
|
+
// state the index was built with, and warn while the flag diverges from it.
|
|
221
|
+
const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, (ctx.full || !isIncremental) && ctx.scanComplete);
|
|
222
|
+
if (bodyOpeningWarning)
|
|
223
|
+
warn(bodyOpeningWarning);
|
|
185
224
|
warnIfVecMissing(db);
|
|
186
225
|
const totalEntries = getEntryCount(db);
|
|
187
226
|
const semanticEntryCount = getEmbeddableEntryCount(db);
|
|
@@ -209,6 +248,41 @@ async function runFinalizePhase(ctx) {
|
|
|
209
248
|
// suppress unused warning — sources was previously used inline
|
|
210
249
|
void sources;
|
|
211
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Stash-organization conventions (SPEC-8): reconcile the `index.indexBodyOpening`
|
|
253
|
+
* flag with the state the index was last FULLY built with (index_meta key
|
|
254
|
+
* `indexBodyOpening`), returning a warning message while they diverge.
|
|
255
|
+
*
|
|
256
|
+
* Incremental runs re-extract only changed files (and embeddings are only
|
|
257
|
+
* generated for rows lacking one), so a flag toggle leaves the index MIXED
|
|
258
|
+
* until a full rebuild — `akm index --full` re-extracts every entry and wipes
|
|
259
|
+
* embeddings so they regenerate from the new text. The warning therefore
|
|
260
|
+
* repeats on every incremental run until a full walk records the flag state
|
|
261
|
+
* as applied.
|
|
262
|
+
*
|
|
263
|
+
* A missing meta key on an incremental run means the index predates this
|
|
264
|
+
* feature, i.e. it was necessarily built with the flag OFF — so the absent
|
|
265
|
+
* key reads (and is seeded) as "0", never as the current flag value. This
|
|
266
|
+
* keeps the most likely real toggle scenario — upgrade, enable the flag, run
|
|
267
|
+
* a plain `akm index` — warning until `--full` runs (review finding).
|
|
268
|
+
*
|
|
269
|
+
* Exported for tests; production's only caller is the finalize phase above.
|
|
270
|
+
*/
|
|
271
|
+
export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
|
|
272
|
+
const bodyOpeningFlag = flagEnabled ? "1" : "0";
|
|
273
|
+
const prevBodyOpeningFlag = getMeta(db, "indexBodyOpening") ?? "0";
|
|
274
|
+
// Only a full walk (which includes the first build ever) may record the
|
|
275
|
+
// current flag as the applied state; incremental runs preserve — or, for a
|
|
276
|
+
// pre-feature index, seed — the state of the last full build.
|
|
277
|
+
setMeta(db, "indexBodyOpening", isFullWalk ? bodyOpeningFlag : prevBodyOpeningFlag);
|
|
278
|
+
if (isFullWalk || prevBodyOpeningFlag === bodyOpeningFlag)
|
|
279
|
+
return undefined;
|
|
280
|
+
return (`index.indexBodyOpening is ${flagEnabled ? "enabled" : "disabled"} but the index was built with it ` +
|
|
281
|
+
`${flagEnabled ? "disabled" : "enabled"}. Incremental runs only re-extract changed files, so ` +
|
|
282
|
+
"indexed text and embeddings are stale for unchanged entries. Run `akm index --full` to apply the new " +
|
|
283
|
+
"setting everywhere (embeddings regenerate), and re-mint collapse-detector canary baselines via " +
|
|
284
|
+
"`bun scripts/refresh-canary-set.ts --refresh` if you use them.");
|
|
285
|
+
}
|
|
212
286
|
// ── Clean pass ───────────────────────────────────────────────────────────────
|
|
213
287
|
/**
|
|
214
288
|
* Post-index clean pass: scan the `entries` table for rows whose source file
|
|
@@ -245,7 +319,82 @@ export async function akmIndex(options) {
|
|
|
245
319
|
return akmIndexOverride(options);
|
|
246
320
|
return akmIndexReal(options);
|
|
247
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Detect an adapter for every resolvable source that does not declare one, and
|
|
324
|
+
* persist each detection into `config.json`.
|
|
325
|
+
*
|
|
326
|
+
* R-056: this config write previously had zero disclosure — it appeared in no
|
|
327
|
+
* result, on no stream, and in no doc. The returned `persistedAdapters` records
|
|
328
|
+
* exactly which bundle→adapter pairs the mutate callback actually applied, so
|
|
329
|
+
* the caller can surface them in the result envelope; a stderr notice is
|
|
330
|
+
* emitted here. The map is cleared at the top of every callback invocation
|
|
331
|
+
* because `mutateConfig` may retry optimistically, and a retry must not report
|
|
332
|
+
* a superseded attempt.
|
|
333
|
+
*
|
|
334
|
+
* Extracted from `akmIndexReal` as one self-contained named pass, both to keep
|
|
335
|
+
* that function under the src-wide function-size bar and because the detection
|
|
336
|
+
* and its disclosure belong together.
|
|
337
|
+
*/
|
|
338
|
+
function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, opts) {
|
|
339
|
+
const detectedByBundle = new Map();
|
|
340
|
+
for (const source of allSourceEntries) {
|
|
341
|
+
if (source.adapterId || source.unresolved)
|
|
342
|
+
continue;
|
|
343
|
+
if (allSourceRootsReadable([source.path])) {
|
|
344
|
+
source.adapterId = detectAdapterId(source.path);
|
|
345
|
+
if (source.registryId)
|
|
346
|
+
detectedByBundle.set(source.registryId, source.adapterId);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const persistedAdapters = {};
|
|
350
|
+
if (detectedByBundle.size === 0)
|
|
351
|
+
return { config, persistedAdapters };
|
|
352
|
+
const nextConfig = mutateConfig((current) => {
|
|
353
|
+
if (!current.bundles)
|
|
354
|
+
return current;
|
|
355
|
+
let changed = false;
|
|
356
|
+
const bundles = { ...current.bundles };
|
|
357
|
+
for (const key of Object.keys(persistedAdapters))
|
|
358
|
+
delete persistedAdapters[key];
|
|
359
|
+
for (const [bundleId, adapter] of detectedByBundle) {
|
|
360
|
+
const bundle = bundles[bundleId];
|
|
361
|
+
if (!bundle)
|
|
362
|
+
continue;
|
|
363
|
+
const componentEntries = Object.entries(bundle.components ?? {});
|
|
364
|
+
const [componentId, component] = componentEntries[0] ?? ["main", {}];
|
|
365
|
+
if (component.adapter)
|
|
366
|
+
continue;
|
|
367
|
+
bundles[bundleId] = { ...bundle, components: { [componentId]: { ...component, adapter } } };
|
|
368
|
+
changed = true;
|
|
369
|
+
persistedAdapters[bundleId] = adapter;
|
|
370
|
+
}
|
|
371
|
+
return changed ? { ...current, bundles } : current;
|
|
372
|
+
}, { absentNoop: true }).config;
|
|
373
|
+
const persistedCount = Object.keys(persistedAdapters).length;
|
|
374
|
+
if (persistedCount > 0 && opts.announce) {
|
|
375
|
+
const summary = Object.entries(persistedAdapters)
|
|
376
|
+
.map(([bundleId, adapter]) => `${bundleId} → ${adapter}`)
|
|
377
|
+
.join(", ");
|
|
378
|
+
warn(`[index] Detected adapter${persistedCount === 1 ? "" : "s"} for ${summary}; persisted to config.json ` +
|
|
379
|
+
"(bundles.<id>.components.<component>.adapter).");
|
|
380
|
+
}
|
|
381
|
+
return { config: nextConfig, persistedAdapters };
|
|
382
|
+
}
|
|
248
383
|
async function akmIndexReal(options) {
|
|
384
|
+
// R-022: `dryRun` only ever gated the `--clean` stale-entry removal pass
|
|
385
|
+
// (see `runCleanPass` below) — every other phase (walk, LLM enrichment,
|
|
386
|
+
// embeddings, FTS, the adapter-detection config write) ran for real
|
|
387
|
+
// regardless, so `akm index --dry-run` alone silently performed a full,
|
|
388
|
+
// real index. The flag's own docs (`IndexOptions.dryRun` above, and the
|
|
389
|
+
// CLI help in stash-cli.ts) already scope it to `--clean`; reject the
|
|
390
|
+
// combination that was never implemented instead of quietly doing
|
|
391
|
+
// something other than what "dry run" promised. Checked before the writer
|
|
392
|
+
// lease is even requested so a bad invocation fails instantly.
|
|
393
|
+
if (options?.dryRun === true && options?.clean !== true) {
|
|
394
|
+
const { UsageError } = await import("../core/errors.js");
|
|
395
|
+
throw new UsageError("`--dry-run` only applies together with `--clean` (it previews which stale entries `--clean` would remove). " +
|
|
396
|
+
"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.");
|
|
397
|
+
}
|
|
249
398
|
const requestedAt = Date.now();
|
|
250
399
|
let acquiredAt = requestedAt;
|
|
251
400
|
return withIndexWriterLease({
|
|
@@ -261,30 +410,28 @@ async function akmIndexReal(options) {
|
|
|
261
410
|
acquiredAt = requestedAt + waitedMs;
|
|
262
411
|
},
|
|
263
412
|
}, async () => {
|
|
264
|
-
const stashDir = options
|
|
413
|
+
const stashDir = options.stashDir;
|
|
265
414
|
const onProgress = options?.onProgress ?? (() => { });
|
|
266
415
|
const signal = options?.signal;
|
|
267
|
-
const reEnrich = options?.reEnrich === true;
|
|
268
416
|
const full = options?.full === true;
|
|
269
417
|
const clean = options?.clean === true;
|
|
270
418
|
const dryRun = options?.dryRun === true;
|
|
271
419
|
// Load config and resolve all stash sources
|
|
272
|
-
const { loadConfig } = await import("../core/config/config.js");
|
|
273
|
-
|
|
274
|
-
// One-time, read-only guard: warn if the writable stash still holds an
|
|
275
|
-
// un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
|
|
276
|
-
// entirely, so an unmigrated vault's `.env` data would silently never be
|
|
277
|
-
// indexed. Non-destructive — only stats, never reads/writes/deletes.
|
|
278
|
-
const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
|
|
279
|
-
warnOnUnmigratedVaults(stashDir);
|
|
420
|
+
const { loadConfig, mutateConfig } = await import("../core/config/config.js");
|
|
421
|
+
let config = loadConfig();
|
|
280
422
|
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
281
423
|
// so their content directories exist on disk for the walker to discover.
|
|
282
424
|
const sourceCacheStart = Date.now();
|
|
283
425
|
onProgress({ phase: "preflight", message: "Hydrating source caches." });
|
|
284
426
|
const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
|
|
285
|
-
await ensureSourceCaches(config, { force: full });
|
|
427
|
+
await ensureSourceCaches(config, { force: full, materialize: options.hydrateSources !== false });
|
|
286
428
|
const sourceCacheEnd = Date.now();
|
|
287
429
|
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
430
|
+
const detected = detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, {
|
|
431
|
+
announce: options.implicit !== true,
|
|
432
|
+
});
|
|
433
|
+
config = detected.config;
|
|
434
|
+
const persistedAdapters = detected.persistedAdapters;
|
|
288
435
|
const allSourceDirs = allSourceEntries.map((s) => s.path);
|
|
289
436
|
onProgress({
|
|
290
437
|
phase: "preflight",
|
|
@@ -308,7 +455,7 @@ async function akmIndexReal(options) {
|
|
|
308
455
|
sources: allSourceEntries,
|
|
309
456
|
sourceDirs: allSourceDirs,
|
|
310
457
|
full,
|
|
311
|
-
|
|
458
|
+
clean,
|
|
312
459
|
stashDir,
|
|
313
460
|
onProgress,
|
|
314
461
|
signal,
|
|
@@ -325,6 +472,8 @@ async function akmIndexReal(options) {
|
|
|
325
472
|
isIncremental,
|
|
326
473
|
builtAtMs,
|
|
327
474
|
hadRemovedSources: false,
|
|
475
|
+
removedSourceDirs: [],
|
|
476
|
+
scanComplete: true,
|
|
328
477
|
scannedDirs: 0,
|
|
329
478
|
skippedDirs: 0,
|
|
330
479
|
generatedCount: 0,
|
|
@@ -346,6 +495,7 @@ async function akmIndexReal(options) {
|
|
|
346
495
|
// ── Phase sequence ───────────────────────────────────────────────────────
|
|
347
496
|
await runSourceCachePhase(ctx);
|
|
348
497
|
await runWalkPhase(ctx);
|
|
498
|
+
applyRemovedSources(ctx);
|
|
349
499
|
await runEmbeddingPhase(ctx);
|
|
350
500
|
await runFinalizePhase(ctx);
|
|
351
501
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -363,7 +513,13 @@ async function akmIndexReal(options) {
|
|
|
363
513
|
phase: "finalize",
|
|
364
514
|
message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
|
|
365
515
|
});
|
|
366
|
-
|
|
516
|
+
if (ctx.scanComplete) {
|
|
517
|
+
cleanResult = runCleanPass(db, dryRun);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
warn("[index] --clean skipped because one or more configured sources were not scanned completely.");
|
|
521
|
+
cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
|
|
522
|
+
}
|
|
367
523
|
}
|
|
368
524
|
const cleanEnd = Date.now();
|
|
369
525
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -376,6 +532,9 @@ async function akmIndexReal(options) {
|
|
|
376
532
|
directoriesScanned: ctx.scannedDirs,
|
|
377
533
|
directoriesSkipped: ctx.skippedDirs,
|
|
378
534
|
...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
|
|
535
|
+
...(Object.keys(persistedAdapters).length > 0
|
|
536
|
+
? { configUpdated: { detectedAdapters: persistedAdapters } }
|
|
537
|
+
: {}),
|
|
379
538
|
verification,
|
|
380
539
|
timing: {
|
|
381
540
|
totalMs: Date.now() - timing.t0,
|
|
@@ -398,32 +557,183 @@ async function akmIndexReal(options) {
|
|
|
398
557
|
}
|
|
399
558
|
});
|
|
400
559
|
}
|
|
560
|
+
function removalsFirst(records) {
|
|
561
|
+
return [...records.filter((record) => record.remove), ...records.filter((record) => !record.remove)];
|
|
562
|
+
}
|
|
563
|
+
function addEntryIds(target, ids) {
|
|
564
|
+
for (const id of ids)
|
|
565
|
+
target.add(id);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Map each source root → its durable `BundleComponent` (`deriveInstallations`,
|
|
569
|
+
* batch-unique bundle ids, source order preserved). The per-dir document drain
|
|
570
|
+
* dispatches `adapterForId(component.adapter).recognize` for this component. The
|
|
571
|
+
* component id only surfaces on `IndexDocument.ref`, which the persist layer
|
|
572
|
+
* re-derives independently — so a source missing from the map (never happens: the
|
|
573
|
+
* map is built from the same sources) is harmless.
|
|
574
|
+
*/
|
|
575
|
+
function buildComponentBySource(sources) {
|
|
576
|
+
const map = new Map();
|
|
577
|
+
const installations = deriveInstallations(sources);
|
|
578
|
+
sources.forEach((source, i) => {
|
|
579
|
+
const component = installations[i]?.components[0];
|
|
580
|
+
if (component)
|
|
581
|
+
map.set(source.path, component);
|
|
582
|
+
});
|
|
583
|
+
return map;
|
|
584
|
+
}
|
|
585
|
+
function componentForSource(components, sourcePath) {
|
|
586
|
+
return (components.get(sourcePath) ?? {
|
|
587
|
+
id: sourcePath,
|
|
588
|
+
adapter: "akm",
|
|
589
|
+
root: sourcePath,
|
|
590
|
+
writable: false,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function groupFileContextsByDir(fileContexts) {
|
|
594
|
+
const groups = new Map();
|
|
595
|
+
for (const ctx of fileContexts) {
|
|
596
|
+
const group = groups.get(ctx.parentDirAbs);
|
|
597
|
+
if (group)
|
|
598
|
+
group.push(ctx);
|
|
599
|
+
else
|
|
600
|
+
groups.set(ctx.parentDirAbs, [ctx]);
|
|
601
|
+
}
|
|
602
|
+
return groups;
|
|
603
|
+
}
|
|
604
|
+
function sourceSnapshotRemovals(db, currentStashDir, currentDirs, allIndexedDirsBySource) {
|
|
605
|
+
const indexedDirs = allIndexedDirsBySource?.get(path.resolve(currentStashDir)) ?? getIndexedDirPathsByStashDir(db, currentStashDir);
|
|
606
|
+
return [...indexedDirs]
|
|
607
|
+
.map((dirPath) => path.resolve(dirPath))
|
|
608
|
+
.filter((dirPath) => !currentDirs.has(dirPath))
|
|
609
|
+
.map((dirPath) => ({
|
|
610
|
+
dirPath,
|
|
611
|
+
currentStashDir,
|
|
612
|
+
files: [],
|
|
613
|
+
stash: null,
|
|
614
|
+
skip: false,
|
|
615
|
+
remove: true,
|
|
616
|
+
reason: { kind: "not-in-source-snapshot" },
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs) {
|
|
620
|
+
const componentBySource = buildComponentBySource(allSourceEntries);
|
|
621
|
+
const handoffDirs = new Set();
|
|
622
|
+
const plans = allSourceEntries.map((sourceAdded) => {
|
|
623
|
+
const currentStashDir = sourceAdded.path;
|
|
624
|
+
const component = componentForSource(componentBySource, currentStashDir);
|
|
625
|
+
if (sourceAdded.unresolved) {
|
|
626
|
+
return {
|
|
627
|
+
currentStashDir,
|
|
628
|
+
component,
|
|
629
|
+
adapter: undefined,
|
|
630
|
+
indexVariant: undefined,
|
|
631
|
+
dirGroups: new Map(),
|
|
632
|
+
removals: [],
|
|
633
|
+
walkComplete: false,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const walked = walkStashFlatWithStatus(currentStashDir, {
|
|
637
|
+
includeAllDirectories: component.adapter === "okf",
|
|
638
|
+
});
|
|
639
|
+
const dirGroups = groupFileContextsByDir(walked.files);
|
|
640
|
+
const adapter = adapterForId(component.adapter);
|
|
641
|
+
return {
|
|
642
|
+
currentStashDir,
|
|
643
|
+
component,
|
|
644
|
+
adapter,
|
|
645
|
+
indexVariant: adapter ? `${adapter.id}@${adapter.version}` : undefined,
|
|
646
|
+
dirGroups,
|
|
647
|
+
removals: [],
|
|
648
|
+
walkComplete: walked.complete,
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
const removalKeys = new Set();
|
|
652
|
+
const addRemoval = (plan, dirPath, stashDir) => {
|
|
653
|
+
const resolvedDir = path.resolve(dirPath);
|
|
654
|
+
const key = `${resolvedDir}\0${path.resolve(stashDir)}`;
|
|
655
|
+
if (removalKeys.has(key))
|
|
656
|
+
return;
|
|
657
|
+
removalKeys.add(key);
|
|
658
|
+
plan.removals.push({
|
|
659
|
+
dirPath,
|
|
660
|
+
currentStashDir: stashDir,
|
|
661
|
+
files: [],
|
|
662
|
+
stash: null,
|
|
663
|
+
skip: false,
|
|
664
|
+
remove: true,
|
|
665
|
+
reason: { kind: "not-in-source-snapshot" },
|
|
666
|
+
});
|
|
667
|
+
handoffDirs.add(resolvedDir);
|
|
668
|
+
};
|
|
669
|
+
const allComplete = plans.every((plan) => plan.walkComplete && plan.adapter !== undefined);
|
|
670
|
+
// A full, globally-complete run uses the atomic table wipe below. Every
|
|
671
|
+
// other run reconciles only sources that produced trustworthy snapshots.
|
|
672
|
+
if (reconcileMissingDirs && (isIncremental || !allComplete)) {
|
|
673
|
+
const allIndexedDirsBySource = !isIncremental ? new Map() : undefined;
|
|
674
|
+
if (allIndexedDirsBySource) {
|
|
675
|
+
for (const entry of getAllEntries(db)) {
|
|
676
|
+
const sourceRoot = path.resolve(entry.stashDir);
|
|
677
|
+
const dirs = allIndexedDirsBySource.get(sourceRoot) ?? new Set();
|
|
678
|
+
dirs.add(path.resolve(entry.dirPath));
|
|
679
|
+
allIndexedDirsBySource.set(sourceRoot, dirs);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
for (const plan of plans) {
|
|
683
|
+
if (!plan.walkComplete || !plan.adapter)
|
|
684
|
+
continue;
|
|
685
|
+
const currentDirs = new Set([...plan.dirGroups.keys()].map((dirPath) => path.resolve(dirPath)));
|
|
686
|
+
for (const removal of sourceSnapshotRemovals(db, plan.currentStashDir, currentDirs, allIndexedDirsBySource)) {
|
|
687
|
+
addRemoval(plan, removal.dirPath, removal.currentStashDir);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Cross-source ownership handoffs can delete another source's rows, so they
|
|
692
|
+
// still require every possible owner to have completed its scan.
|
|
693
|
+
if (!allComplete)
|
|
694
|
+
return { plans, handoffDirs };
|
|
695
|
+
// The first configured source that exposes a physical directory owns it.
|
|
696
|
+
// Remove rows left by a prior owner even when both adapters are identical.
|
|
697
|
+
const claimedDirs = new Set();
|
|
698
|
+
for (const plan of plans) {
|
|
699
|
+
for (const dirPath of plan.dirGroups.keys()) {
|
|
700
|
+
const resolvedDir = path.resolve(dirPath);
|
|
701
|
+
if (claimedDirs.has(resolvedDir))
|
|
702
|
+
continue;
|
|
703
|
+
claimedDirs.add(resolvedDir);
|
|
704
|
+
for (const priorOwner of getIndexedStashDirsByDir(db, dirPath)) {
|
|
705
|
+
if (path.resolve(priorOwner) !== path.resolve(plan.currentStashDir)) {
|
|
706
|
+
addRemoval(plan, dirPath, priorOwner);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return { plans, handoffDirs };
|
|
712
|
+
}
|
|
401
713
|
/**
|
|
402
714
|
* Phase 1 (async): walk every source directory and pre-generate all metadata
|
|
403
715
|
* outside any transaction, producing the per-directory scan records that
|
|
404
716
|
* {@link persistDirRecords} later writes.
|
|
405
717
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
718
|
+
* The per-dir document drain (`drainDirDocuments` × the component's dispatched
|
|
719
|
+
* `adapter.recognize`, F4a M-core-2) is synchronous, but the walk still runs
|
|
720
|
+
* outside `db.transaction()` so the persist pass can be a single synchronous
|
|
721
|
+
* transaction.
|
|
409
722
|
*/
|
|
410
|
-
|
|
723
|
+
function reportSourceScanProgress(onProgress, processed, total, message) {
|
|
724
|
+
onProgress?.({ phase: "scan", message, processed, total });
|
|
725
|
+
}
|
|
726
|
+
async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs = true) {
|
|
411
727
|
let scannedDirs = 0;
|
|
412
728
|
let skippedDirs = 0;
|
|
413
729
|
let generatedCount = 0;
|
|
414
730
|
const warnings = [];
|
|
415
731
|
const seenPaths = new Set();
|
|
732
|
+
const { plans, handoffDirs } = buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs);
|
|
416
733
|
const dirRecords = [];
|
|
417
734
|
let processedDirs = 0;
|
|
418
735
|
let priorDirsChanged = hadRemovedSources;
|
|
419
|
-
const reportScanProgress = (message) =>
|
|
420
|
-
onProgress?.({
|
|
421
|
-
phase: "scan",
|
|
422
|
-
message,
|
|
423
|
-
processed: processedDirs,
|
|
424
|
-
total: allSourceEntries.length,
|
|
425
|
-
});
|
|
426
|
-
};
|
|
736
|
+
const reportScanProgress = (message) => reportSourceScanProgress(onProgress, processedDirs, allSourceEntries.length, message);
|
|
427
737
|
const reportDirDecision = (kind, dirPath, currentStashDir, reason, persistedRowCount) => {
|
|
428
738
|
if (!isVerbose())
|
|
429
739
|
return;
|
|
@@ -432,10 +742,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
432
742
|
reportScanProgress(`${kind === "scan" ? "Rescanning" : "Skipping"} ${path.relative(currentStashDir, dirPath) || "."} ` +
|
|
433
743
|
`from ${currentStashDir}: ${reason.kind}${detail}${rowInfo}`);
|
|
434
744
|
};
|
|
435
|
-
//
|
|
436
|
-
// dir may surface via multiple stash roots; only the first occurrence is
|
|
437
|
-
// indexed, and the later ones are recorded as skips. Returns true when the
|
|
438
|
-
// dir was already seen (caller should skip further processing).
|
|
745
|
+
// Only the first source that exposes a physical directory may index it.
|
|
439
746
|
const markSeenOrSkipDuplicate = (dirPath, currentStashDir, files) => {
|
|
440
747
|
const resolved = path.resolve(dirPath);
|
|
441
748
|
if (seenPaths.has(resolved)) {
|
|
@@ -450,9 +757,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
450
757
|
// Incremental freshness gate shared by both branches: consult the persisted
|
|
451
758
|
// dir state and record either a skip (unchanged + eligible for incremental
|
|
452
759
|
// skip) or a scan record carrying the candidate stash.
|
|
453
|
-
const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash) => {
|
|
454
|
-
const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs);
|
|
455
|
-
if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
760
|
+
const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash, hashByFile, conceptIdByFile, indexVariant, forceScan, pruneMissing) => {
|
|
761
|
+
const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs, indexVariant);
|
|
762
|
+
if (isIncremental && !forceScan && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
456
763
|
skippedDirs++;
|
|
457
764
|
dirRecords.push({
|
|
458
765
|
dirPath,
|
|
@@ -461,6 +768,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
461
768
|
stash: null,
|
|
462
769
|
skip: true,
|
|
463
770
|
reason: previousState.reason,
|
|
771
|
+
indexVariant,
|
|
464
772
|
});
|
|
465
773
|
reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
|
|
466
774
|
return;
|
|
@@ -476,63 +784,46 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
476
784
|
skip: false,
|
|
477
785
|
reason,
|
|
478
786
|
persistedRowCount: previousState.persistedRowCount,
|
|
787
|
+
hashByFile,
|
|
788
|
+
conceptIdByFile,
|
|
789
|
+
indexVariant,
|
|
790
|
+
pruneMissing,
|
|
479
791
|
});
|
|
480
792
|
reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
|
|
481
793
|
};
|
|
482
|
-
for (const
|
|
483
|
-
const currentStashDir =
|
|
484
|
-
const fileContexts = walkStashFlat(currentStashDir);
|
|
794
|
+
for (const plan of plans) {
|
|
795
|
+
const { currentStashDir, component, adapter, dirGroups, removals, walkComplete } = plan;
|
|
485
796
|
processedDirs++;
|
|
486
797
|
reportScanProgress(`Processed ${processedDirs}/${allSourceEntries.length} source${allSourceEntries.length === 1 ? "" : "s"}.`);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
for (const ctx of fileContexts) {
|
|
492
|
-
if (ctx.ext !== ".md")
|
|
493
|
-
continue;
|
|
494
|
-
if (!shouldIndexStashFile(currentStashDir, ctx.absPath, { treatStashRootAsWikiRoot: true }))
|
|
495
|
-
continue;
|
|
496
|
-
const relNoExt = ctx.relPath.replace(/\.md$/, "");
|
|
497
|
-
const frontmatter = ctx.frontmatter() ?? {};
|
|
498
|
-
const entry = {
|
|
499
|
-
name: `${wikiName}/${relNoExt}`,
|
|
500
|
-
type: "wiki",
|
|
501
|
-
filename: ctx.fileName,
|
|
502
|
-
quality: "generated",
|
|
503
|
-
confidence: 0.55,
|
|
504
|
-
source: "filename",
|
|
505
|
-
};
|
|
506
|
-
applyCuratedFrontmatter(entry, frontmatter);
|
|
507
|
-
applyWikiFrontmatter(entry, frontmatter);
|
|
508
|
-
const dir = ctx.parentDirAbs;
|
|
509
|
-
const group = wikiDirGroups.get(dir);
|
|
510
|
-
if (group) {
|
|
511
|
-
group.files.push(ctx.absPath);
|
|
512
|
-
group.entries.push(entry);
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
wikiDirGroups.set(dir, { files: [ctx.absPath], entries: [entry] });
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
for (const [dirPath, { files, entries }] of wikiDirGroups) {
|
|
519
|
-
if (markSeenOrSkipDuplicate(dirPath, currentStashDir, files))
|
|
520
|
-
continue;
|
|
521
|
-
recordFreshnessDecision(dirPath, currentStashDir, files, { entries });
|
|
522
|
-
}
|
|
798
|
+
if (!walkComplete) {
|
|
799
|
+
for (const dirPath of dirGroups.keys())
|
|
800
|
+
seenPaths.add(path.resolve(dirPath));
|
|
801
|
+
warn(`[index] source "${component.id}" was not scanned completely; preserving its last-known-good rows.`);
|
|
523
802
|
continue;
|
|
524
803
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
804
|
+
// Owner ruling 2026-07-21: dispatch each component's DETECTED adapter (§4).
|
|
805
|
+
// An unknown adapter id has no `adapterForId` match → skip the whole
|
|
806
|
+
// component with a warning (one bundle = one component = one adapter).
|
|
807
|
+
if (!adapter) {
|
|
808
|
+
for (const dirPath of dirGroups.keys())
|
|
809
|
+
seenPaths.add(path.resolve(dirPath));
|
|
810
|
+
warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const indexVariant = plan.indexVariant ?? `${adapter.id}@${adapter.version}`;
|
|
814
|
+
for (const removal of removals) {
|
|
815
|
+
dirRecords.push(removal);
|
|
816
|
+
scannedDirs++;
|
|
817
|
+
priorDirsChanged = true;
|
|
818
|
+
reportDirDecision("scan", removal.dirPath, currentStashDir, removal.reason);
|
|
533
819
|
}
|
|
534
|
-
for (const [dirPath,
|
|
535
|
-
|
|
820
|
+
for (const [dirPath, ctxs] of dirGroups) {
|
|
821
|
+
// Adapter-owned filtering (owner ruling 2026-07-21): the drain no longer
|
|
822
|
+
// pre-filters with AKM-stash policy — each adapter's `recognize` claims or
|
|
823
|
+
// abstains on its own bundle's walked files. The core walk keeps only the
|
|
824
|
+
// universal hygiene `walkStashFlat` already applies (.git/dot-dirs/etc.).
|
|
825
|
+
const indexableFiles = ctxs.map((ctx) => ctx.absPath);
|
|
826
|
+
const forceScan = handoffDirs.has(path.resolve(dirPath));
|
|
536
827
|
if (markSeenOrSkipDuplicate(dirPath, currentStashDir, indexableFiles))
|
|
537
828
|
continue;
|
|
538
829
|
if (indexableFiles.length === 0) {
|
|
@@ -542,7 +833,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
542
833
|
reportDirDecision("skip", dirPath, currentStashDir, reason);
|
|
543
834
|
continue;
|
|
544
835
|
}
|
|
545
|
-
const cachedZeroRowState = isIncremental &&
|
|
836
|
+
const cachedZeroRowState = isIncremental &&
|
|
837
|
+
!forceScan &&
|
|
838
|
+
getCachedZeroRowDirState(db, dirPath, indexableFiles, builtAtMs, priorDirsChanged, indexVariant);
|
|
546
839
|
if (cachedZeroRowState) {
|
|
547
840
|
skippedDirs++;
|
|
548
841
|
dirRecords.push({
|
|
@@ -552,40 +845,100 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
552
845
|
stash: null,
|
|
553
846
|
skip: true,
|
|
554
847
|
reason: cachedZeroRowState.reason,
|
|
848
|
+
indexVariant,
|
|
555
849
|
});
|
|
556
850
|
reportDirDecision("skip", dirPath, currentStashDir, cachedZeroRowState.reason, cachedZeroRowState.persistedRowCount);
|
|
557
851
|
continue;
|
|
558
852
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
853
|
+
// F4a M-core-2 (the flip): drain the dir's `IndexDocument` stream via the
|
|
854
|
+
// component's dispatched `adapter.recognize` (broken workflows dropped-with-
|
|
855
|
+
// warning at the drain layer) and reconstruct the durable `IndexDocument`s.
|
|
856
|
+
const drained = drainDirDocuments(adapter, component, ctxs);
|
|
857
|
+
if (drained.warnings.length)
|
|
858
|
+
warnings.push(...drained.warnings);
|
|
859
|
+
const generated = drained.warnings.length
|
|
860
|
+
? { entries: drained.entries, warnings: drained.warnings }
|
|
861
|
+
: { entries: drained.entries };
|
|
862
|
+
// `.stash.json` sidecar overrides retired (#39): the cutover's content
|
|
863
|
+
// migration folded sidecar metadata into frontmatter and deleted the
|
|
864
|
+
// files; the runtime no longer reads them.
|
|
865
|
+
const { stash, staleFiles } = buildIndexedDirCandidate(dirPath, indexableFiles, generated);
|
|
564
866
|
if (generated.entries.length > 0) {
|
|
565
867
|
generatedCount += generated.entries.length;
|
|
566
868
|
}
|
|
567
|
-
recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash);
|
|
869
|
+
recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash, drained.hashByFile, drained.conceptIdByFile, indexVariant, forceScan, walkComplete);
|
|
568
870
|
}
|
|
569
871
|
}
|
|
570
|
-
return {
|
|
872
|
+
return {
|
|
873
|
+
dirRecords: removalsFirst(dirRecords),
|
|
874
|
+
scannedDirs,
|
|
875
|
+
skippedDirs,
|
|
876
|
+
generatedCount,
|
|
877
|
+
warnings,
|
|
878
|
+
complete: plans.every((plan) => plan.walkComplete && plan.adapter !== undefined),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function preserveExistingIndex(doFullDelete, dirRecords, sourceRoots) {
|
|
882
|
+
if (!doFullDelete)
|
|
883
|
+
return false;
|
|
884
|
+
const incomingDocCount = dirRecords.reduce((n, record) => n + (record.skip ? 0 : (record.stash?.entries.length ?? 0)), 0);
|
|
885
|
+
if (incomingDocCount > 0 || allSourceRootsReadable(sourceRoots))
|
|
886
|
+
return false;
|
|
887
|
+
warn("[index] --full produced zero documents while one or more source roots are missing or unreadable — " +
|
|
888
|
+
"preserving the existing index (last-known-good) rather than wiping it. Re-run once the sources are available.");
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* #624-P1 zero-document preflight probe. A source root counts as "readable"
|
|
893
|
+
* when it exists on disk as a directory whose listing can be read. A root that
|
|
894
|
+
* is missing or unreadable (a transient mount failure, a permission race, or a
|
|
895
|
+
* source that vanished mid-run) makes a zero-document scan untrustworthy: the
|
|
896
|
+
* walk saw nothing not because the stash is empty but because it could not be
|
|
897
|
+
* read. Returns true only when EVERY root is readable, so a single unreadable
|
|
898
|
+
* root blocks the full-rebuild wipe.
|
|
899
|
+
*/
|
|
900
|
+
function allSourceRootsReadable(roots) {
|
|
901
|
+
for (const root of roots) {
|
|
902
|
+
try {
|
|
903
|
+
const st = fs.statSync(root);
|
|
904
|
+
if (!st.isDirectory())
|
|
905
|
+
return false;
|
|
906
|
+
fs.readdirSync(root); // probe readability, not just existence
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return true;
|
|
571
913
|
}
|
|
572
914
|
/**
|
|
573
915
|
* Phase 2 (sync): write all pre-generated scan records inside a single
|
|
574
916
|
* transaction, returning the directories that still need LLM enrichment.
|
|
575
917
|
*/
|
|
576
|
-
function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
|
|
918
|
+
function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, scanComplete, bundleByRoot) {
|
|
577
919
|
const dirsNeedingLlm = [];
|
|
578
|
-
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
920
|
+
const fullDelete = doFullDelete && scanComplete;
|
|
921
|
+
// #624-P1 zero-document preflight (spec §4). A full-rebuild wipe is a
|
|
922
|
+
// legitimate mass-delete ONLY when the scan legitimately found nothing. If
|
|
923
|
+
// the walk produced zero documents AND any configured source root is missing
|
|
924
|
+
// or unreadable, the empty result is almost certainly a transient scan
|
|
925
|
+
// failure, not an emptied stash — wiping here would cascade-destroy the
|
|
926
|
+
// last-known-good index (entries + embeddings + utility/usage). Preserve it
|
|
927
|
+
// and warn instead; the next successful run reconciles. A genuinely empty
|
|
928
|
+
// stash whose roots ARE readable still wipes, as before.
|
|
929
|
+
if (preserveExistingIndex(fullDelete, dirRecords, sourceRoots))
|
|
930
|
+
return { dirsNeedingLlm };
|
|
931
|
+
// Per-source dedup: the same logical asset can appear more than once within
|
|
932
|
+
// one owning source, where source order still makes the first occurrence win.
|
|
933
|
+
// The owner is part of the key so identical concepts in different bundles
|
|
934
|
+
// remain distinct indexed rows.
|
|
583
935
|
const indexedAssetIdentities = new Set();
|
|
936
|
+
const deletedUsageEntryIds = new Set();
|
|
584
937
|
const insertTransaction = db.transaction(() => {
|
|
585
938
|
// Perform the full-rebuild wipe as the FIRST step of the insert
|
|
586
939
|
// transaction so delete and re-insert are atomic — a concurrent reader
|
|
587
940
|
// never observes an empty database between the two operations.
|
|
588
|
-
if (
|
|
941
|
+
if (fullDelete) {
|
|
589
942
|
try {
|
|
590
943
|
db.exec("DELETE FROM embeddings");
|
|
591
944
|
}
|
|
@@ -603,20 +956,22 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
|
|
|
603
956
|
db.exec("DELETE FROM entries_fts");
|
|
604
957
|
db.exec("DELETE FROM utility_scores");
|
|
605
958
|
db.exec("DELETE FROM index_dir_state");
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
/* ignore if table doesn't exist */
|
|
613
|
-
}
|
|
959
|
+
// Chunk-8 WI-8.3: usage_events lives in state.db now (not index.db), so the
|
|
960
|
+
// wipe no longer detaches it here. The finalize pass's relinkUsageEvents
|
|
961
|
+
// (cross-DB) nulls entry_ids that no longer resolve to a rebuilt entry and
|
|
962
|
+
// re-resolves the rest by entry_ref — subsuming the old detach.
|
|
614
963
|
db.exec("DELETE FROM entries");
|
|
615
964
|
}
|
|
616
|
-
for (const { dirPath, currentStashDir, files, stash, skip, reason } of dirRecords) {
|
|
965
|
+
for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile, indexVariant, remove, pruneMissing, } of dirRecords) {
|
|
966
|
+
if (remove) {
|
|
967
|
+
const removedIds = deleteEntriesByDirAndStash(db, dirPath, currentStashDir, { cleanupUsageEvents: false });
|
|
968
|
+
addEntryIds(deletedUsageEntryIds, removedIds);
|
|
969
|
+
deleteIndexDirState(db, dirPath);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
617
972
|
if (skip) {
|
|
618
973
|
if (reason?.kind === "unchanged") {
|
|
619
|
-
const fingerprint = computeDirFingerprint(dirPath, files);
|
|
974
|
+
const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
|
|
620
975
|
upsertIndexDirState(db, {
|
|
621
976
|
dirPath,
|
|
622
977
|
fileSetHash: fingerprint.fileSetHash,
|
|
@@ -626,30 +981,49 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
|
|
|
626
981
|
}
|
|
627
982
|
continue;
|
|
628
983
|
}
|
|
629
|
-
//
|
|
630
|
-
|
|
984
|
+
// Diff-persist (F4a M-core-2, spec §14.2): upsert the current file set
|
|
985
|
+
// FIRST (ON CONFLICT preserving `entries.id` so embeddings / utility /
|
|
986
|
+
// usage stay attached to unchanged rows), tracking every upserted
|
|
987
|
+
// `entry_key`, then prune only the DEPARTED rows below. Replaces the old
|
|
988
|
+
// `deleteEntriesByDir` truncate-and-reinsert (which discarded ids).
|
|
989
|
+
const keptEntryKeys = new Set();
|
|
631
990
|
let persistedRows = 0;
|
|
632
991
|
let dedupedRows = 0;
|
|
633
992
|
if (stash) {
|
|
993
|
+
const bundle = bundleByRoot.get(path.resolve(currentStashDir));
|
|
994
|
+
if (!bundle)
|
|
995
|
+
throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
|
|
996
|
+
const ownerIdentity = bundle.bundleId;
|
|
634
997
|
for (const entry of stash.entries) {
|
|
635
998
|
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : null;
|
|
636
999
|
if (!entryPath) {
|
|
637
1000
|
warn(`Skipping entry with no resolvable path in ${dirPath}`);
|
|
638
1001
|
continue;
|
|
639
1002
|
}
|
|
640
|
-
|
|
1003
|
+
const adapterConceptId = conceptIdByFile?.get(entryPath);
|
|
1004
|
+
if (!adapterConceptId) {
|
|
1005
|
+
warn(`Skipping entry without adapter-owned concept identity: ${entryPath}`);
|
|
641
1006
|
continue;
|
|
642
|
-
|
|
643
|
-
|
|
1007
|
+
}
|
|
1008
|
+
// Adapter-owned concept identity is path-based and cannot be replaced
|
|
1009
|
+
// by presentation fields such as type/title.
|
|
1010
|
+
const identityKey = `${ownerIdentity}\0${adapterConceptId}`;
|
|
644
1011
|
if (indexedAssetIdentities.has(identityKey)) {
|
|
645
1012
|
dedupedRows++;
|
|
646
1013
|
continue;
|
|
647
1014
|
}
|
|
648
1015
|
indexedAssetIdentities.add(identityKey);
|
|
649
|
-
const entryKey =
|
|
1016
|
+
const entryKey = bundle?.adapterId !== "akm" && adapterConceptId
|
|
1017
|
+
? `${currentStashDir}:concept:${adapterConceptId}`
|
|
1018
|
+
: `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
1019
|
+
keptEntryKeys.add(entryKey);
|
|
650
1020
|
const searchText = buildSearchText(entry);
|
|
651
1021
|
const entryWithSize = attachFileSize(entry, entryPath);
|
|
652
|
-
|
|
1022
|
+
// content_hash = doc.hash from the drain, keyed by the recognized
|
|
1023
|
+
// file's path. A missing hash preserves the existing value on upsert.
|
|
1024
|
+
const contentHash = hashByFile?.get(entryPath);
|
|
1025
|
+
const provenance = deriveEntryProvenance(bundle, entry.type, entry.name, adapterConceptId);
|
|
1026
|
+
const entryId = upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entryWithSize, searchText, provenance, contentHash);
|
|
653
1027
|
persistedRows++;
|
|
654
1028
|
if (entry.type === "workflow") {
|
|
655
1029
|
const doc = takeWorkflowDocument(entry);
|
|
@@ -659,13 +1033,19 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
|
|
|
659
1033
|
}
|
|
660
1034
|
}
|
|
661
1035
|
// Collect dirs needing LLM enhancement during the first walk.
|
|
662
|
-
// Only dirs with "generated" entries need enrichment
|
|
663
|
-
// forces re-processing of already-enriched entries).
|
|
1036
|
+
// Only dirs with "generated" entries need enrichment.
|
|
664
1037
|
if (stash.entries.some((e) => e.quality === "generated")) {
|
|
665
1038
|
dirsNeedingLlm.push({ dirPath, files, currentStashDir, stash });
|
|
666
1039
|
}
|
|
667
1040
|
}
|
|
668
|
-
|
|
1041
|
+
// Prune the departed rows: everything under this dir NOT re-upserted above
|
|
1042
|
+
// (files deleted, deduped away, or abstained on by the adapter). With
|
|
1043
|
+
// an empty kept-set this deletes every row for the dir — the exact net
|
|
1044
|
+
// effect of the old unconditional `deleteEntriesByDir`, minus the id churn.
|
|
1045
|
+
if (pruneMissing !== false) {
|
|
1046
|
+
addEntryIds(deletedUsageEntryIds, deleteEntriesByDirExceptKeys(db, dirPath, currentStashDir, keptEntryKeys, { cleanupUsageEvents: false }));
|
|
1047
|
+
}
|
|
1048
|
+
const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
|
|
669
1049
|
const persistedReason = persistedRows === 0
|
|
670
1050
|
? inferZeroRowReason(stash, reason, warnings, dirPath, dedupedRows)
|
|
671
1051
|
: reason?.kind === "full-rebuild"
|
|
@@ -696,17 +1076,55 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
|
|
|
696
1076
|
}
|
|
697
1077
|
});
|
|
698
1078
|
insertTransaction();
|
|
1079
|
+
deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
|
|
699
1080
|
return { dirsNeedingLlm };
|
|
700
1081
|
}
|
|
701
|
-
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
|
|
1082
|
+
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress, reconcileMissingDirs = true) {
|
|
702
1083
|
// Phase 1 (async): walk directories and pre-generate all metadata outside the
|
|
703
1084
|
// transaction.
|
|
704
|
-
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress);
|
|
1085
|
+
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs);
|
|
705
1086
|
// Phase 2 (sync): write all pre-generated metadata inside a single transaction.
|
|
706
|
-
|
|
707
|
-
|
|
1087
|
+
// Source roots feed the #624-P1 zero-document preflight (a full-rebuild wipe
|
|
1088
|
+
// is suppressed when the scan is empty because roots are unreadable).
|
|
1089
|
+
const sourceRoots = allSourceEntries.map((s) => s.path);
|
|
1090
|
+
// Chunk-5 Step 2 (spec §14.4): map each source root → its durable bundle id so
|
|
1091
|
+
// the writer can persist `item_ref = <bundle>//<conceptId>` and the component/
|
|
1092
|
+
// adapter provenance alongside the legacy columns. `deriveInstallations`
|
|
1093
|
+
// preserves source order, so a positional zip yields the SAME bundle id the
|
|
1094
|
+
// dispatched `adapter.recognize` emits as `IndexDocument.ref` for that root.
|
|
1095
|
+
const installations = deriveInstallations(allSourceEntries);
|
|
1096
|
+
const bundleByRoot = new Map();
|
|
1097
|
+
allSourceEntries.forEach((source, i) => {
|
|
1098
|
+
const inst = installations[i];
|
|
1099
|
+
if (!inst)
|
|
1100
|
+
return;
|
|
1101
|
+
const component = inst.components[0];
|
|
1102
|
+
bundleByRoot.set(path.resolve(source.path), {
|
|
1103
|
+
bundleId: inst.id,
|
|
1104
|
+
componentId: component?.id ?? inst.id,
|
|
1105
|
+
adapterId: component?.adapter ?? "akm",
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, complete, bundleByRoot);
|
|
1109
|
+
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm, complete };
|
|
708
1110
|
}
|
|
709
|
-
|
|
1111
|
+
function indexedProvenanceForFile(db, filePath) {
|
|
1112
|
+
const row = db
|
|
1113
|
+
.prepare("SELECT item_ref AS itemRef, bundle_id AS bundleId, component_id AS componentId, " +
|
|
1114
|
+
"concept_id AS conceptId, adapter_id AS adapterId FROM entries WHERE file_path = ? LIMIT 1")
|
|
1115
|
+
.get(filePath);
|
|
1116
|
+
if (!row?.itemRef || !row.bundleId || !row.componentId || !row.conceptId || !row.adapterId) {
|
|
1117
|
+
throw new Error(`Missing indexed provenance for ${filePath}`);
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
itemRef: row.itemRef,
|
|
1121
|
+
bundleId: row.bundleId,
|
|
1122
|
+
componentId: row.componentId,
|
|
1123
|
+
conceptId: row.conceptId,
|
|
1124
|
+
adapterId: row.adapterId,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal) {
|
|
710
1128
|
// Resolve per-pass LLM config via the unified shim. Returns undefined when
|
|
711
1129
|
// either no `akm.llm` is configured or the user opted this pass out via
|
|
712
1130
|
// `index.enrichment.llm = false`. (#208)
|
|
@@ -716,27 +1134,29 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
716
1134
|
// Aggregate per-entry failures so a misconfigured LLM endpoint surfaces
|
|
717
1135
|
// as a single visible warning instead of silently degrading every entry
|
|
718
1136
|
// and leaving the user wondering why nothing got enhanced.
|
|
719
|
-
const summary = { attempted: 0, succeeded: 0, failureSamples: [] };
|
|
1137
|
+
const summary = { attempted: 0, succeeded: 0, skipped: 0, failureSamples: [] };
|
|
720
1138
|
let completedDirs = 0;
|
|
721
1139
|
let completedEntries = 0;
|
|
722
1140
|
const totalDirs = dirsNeedingLlm.length;
|
|
723
1141
|
const totalEntries = dirsNeedingLlm.reduce((sum, { stash }) => {
|
|
724
1142
|
const entriesToEnhance = stash.entries.filter((e) => {
|
|
725
|
-
if (e.quality !== "generated"
|
|
1143
|
+
if (e.quality !== "generated")
|
|
726
1144
|
return false;
|
|
727
|
-
if (
|
|
1145
|
+
if (isEnrichmentComplete(e))
|
|
728
1146
|
return false;
|
|
729
1147
|
return true;
|
|
730
1148
|
});
|
|
731
1149
|
return sum + entriesToEnhance.length;
|
|
732
1150
|
}, 0);
|
|
733
|
-
// P3 — wall-clock budget for the enrichment pass. Defaults to
|
|
734
|
-
// (or 10 minutes if not set). Users can extend
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
const enrichDeadline =
|
|
1151
|
+
// P3 — wall-clock budget for the enrichment pass. Defaults to the resolved
|
|
1152
|
+
// engine's timeoutMs (or 10 minutes if not set). Users can extend it via
|
|
1153
|
+
// `index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`, or the
|
|
1154
|
+
// engine's own `engines.<name>.timeoutMs`) — no separate knob needed.
|
|
1155
|
+
const enrichDeadline = createEnrichmentDeadline(llmConfig.timeoutMs, totalEntries);
|
|
738
1156
|
let deadlineHit = false;
|
|
739
1157
|
const enrichSignal = (() => {
|
|
1158
|
+
if (!enrichDeadline)
|
|
1159
|
+
return signal ?? new AbortController().signal;
|
|
740
1160
|
if (!signal)
|
|
741
1161
|
return enrichDeadline;
|
|
742
1162
|
// Combine: abort when either fires.
|
|
@@ -780,14 +1200,13 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
780
1200
|
await concurrentMap(dirsNeedingLlm, async ({ dirPath, files, currentStashDir, stash: originalStash }) => {
|
|
781
1201
|
if (enrichSignal.aborted)
|
|
782
1202
|
return undefined;
|
|
783
|
-
// Only enhance generated entries
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
// unless the caller explicitly requests re-enrichment via reEnrich=true.
|
|
1203
|
+
// Only enhance generated entries; user-provided overrides should not
|
|
1204
|
+
// be overwritten. Skip entries that are already fully enriched
|
|
1205
|
+
// (description + tags + searchHints).
|
|
787
1206
|
const entriesToEnhance = originalStash.entries.filter((e) => {
|
|
788
|
-
if (e.quality !== "generated"
|
|
1207
|
+
if (e.quality !== "generated")
|
|
789
1208
|
return false;
|
|
790
|
-
if (
|
|
1209
|
+
if (isEnrichmentComplete(e)) {
|
|
791
1210
|
warnVerbose(`[akm] skipping LLM enrichment for "${e.name}" — entry already complete`);
|
|
792
1211
|
return false;
|
|
793
1212
|
}
|
|
@@ -806,7 +1225,7 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
806
1225
|
lastProgressAt = Date.now();
|
|
807
1226
|
const targetStash = { entries: entriesToEnhance };
|
|
808
1227
|
const entryKeys = entriesToEnhance.map((e) => `${currentStashDir}:${e.type}:${e.name}`);
|
|
809
|
-
const enhanced = await enhanceStashWithLlm(llmConfig, targetStash, files, summary, enrichSignal, db, entryKeys,
|
|
1228
|
+
const enhanced = await enhanceStashWithLlm(llmConfig, targetStash, files, summary, enrichSignal, db, entryKeys, config, (event) => {
|
|
810
1229
|
completedEntries++;
|
|
811
1230
|
lastProgressAt = Date.now();
|
|
812
1231
|
onProgress?.({
|
|
@@ -827,7 +1246,8 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
827
1246
|
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
828
1247
|
const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
829
1248
|
const searchText = buildSearchText(entry);
|
|
830
|
-
|
|
1249
|
+
const provenance = indexedProvenanceForFile(db, entryPath);
|
|
1250
|
+
upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, attachFileSize(entry, entryPath), searchText, provenance);
|
|
831
1251
|
}
|
|
832
1252
|
})();
|
|
833
1253
|
completedDirs++;
|
|
@@ -841,10 +1261,10 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
841
1261
|
});
|
|
842
1262
|
return undefined;
|
|
843
1263
|
},
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
1264
|
+
// Defaults: 2 for remote LLM APIs, 1 for local model servers (LM
|
|
1265
|
+
// Studio, Ollama run one inference at a time — parallel requests cause
|
|
1266
|
+
// "Model reloaded" / 500 errors). No config override reaches this path:
|
|
1267
|
+
// `resolveLlmEngineUse` does not forward `engines.<name>.concurrency`.
|
|
848
1268
|
getDefaultLlmConcurrency(llmConfig));
|
|
849
1269
|
}
|
|
850
1270
|
finally {
|
|
@@ -852,19 +1272,25 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
852
1272
|
clearInterval(heartbeatTimer);
|
|
853
1273
|
}
|
|
854
1274
|
if (deadlineHit) {
|
|
855
|
-
warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase
|
|
1275
|
+
warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase index.enrichment.timeoutMs for a larger budget.");
|
|
856
1276
|
}
|
|
857
|
-
|
|
1277
|
+
// Gate-closed (`skipped`) entries are not failures — exclude them so a
|
|
1278
|
+
// deliberately disabled feature never surfaces as an enrichment error.
|
|
1279
|
+
const failed = summary.attempted - summary.succeeded - summary.skipped;
|
|
1280
|
+
if (failed > 0 && summary.succeeded === 0) {
|
|
858
1281
|
const sample = summary.failureSamples.length ? ` Example: ${summary.failureSamples[0]}` : "";
|
|
859
|
-
warn(`LLM enhancement failed for all ${
|
|
1282
|
+
warn(`LLM enhancement failed for all ${failed} attempted entries — index built without LLM enrichment.` +
|
|
860
1283
|
` Check llm.endpoint and llm.model in your config.${sample}`);
|
|
861
1284
|
}
|
|
862
|
-
else if (
|
|
863
|
-
const failed = summary.attempted - summary.succeeded;
|
|
1285
|
+
else if (failed > 0) {
|
|
864
1286
|
const sample = summary.failureSamples.length ? ` Examples: ${summary.failureSamples.join("; ")}` : "";
|
|
865
1287
|
warn(`LLM enhancement failed for ${failed}/${summary.attempted} entries — they were left un-enhanced.${sample}`);
|
|
866
1288
|
}
|
|
867
1289
|
}
|
|
1290
|
+
export function createEnrichmentDeadline(timeoutMs, totalEntries) {
|
|
1291
|
+
const perEntryTimeoutMs = timeoutMs === undefined ? 10 * 60 * 1000 : timeoutMs;
|
|
1292
|
+
return perEntryTimeoutMs === null ? undefined : AbortSignal.timeout(perEntryTimeoutMs * Math.max(totalEntries, 1));
|
|
1293
|
+
}
|
|
868
1294
|
async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
869
1295
|
throwIfAborted(signal);
|
|
870
1296
|
if (config.semanticSearchMode === "off") {
|
|
@@ -922,25 +1348,39 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
|
922
1348
|
// state is rolled back on failure rather than leaving the table half-filled.
|
|
923
1349
|
let storedCount = 0;
|
|
924
1350
|
let skippedCount = 0;
|
|
1351
|
+
let vecFailedCount = 0;
|
|
925
1352
|
db.transaction(() => {
|
|
926
1353
|
for (let i = 0; i < allEntries.length; i++) {
|
|
927
|
-
|
|
1354
|
+
const res = upsertEmbedding(db, allEntries[i].id, embeddings[i]);
|
|
1355
|
+
if (res.stored) {
|
|
928
1356
|
storedCount++;
|
|
929
1357
|
}
|
|
930
1358
|
else {
|
|
931
1359
|
skippedCount++;
|
|
932
1360
|
}
|
|
1361
|
+
if (res.vec === "failed")
|
|
1362
|
+
vecFailedCount++;
|
|
933
1363
|
}
|
|
934
1364
|
})();
|
|
935
1365
|
if (skippedCount > 0) {
|
|
936
1366
|
warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
|
|
937
1367
|
}
|
|
1368
|
+
// Record the ACTUAL vec-insert outcome so semantic search reflects it
|
|
1369
|
+
// instead of inferring readiness from stored-BLOB counts. Any failure
|
|
1370
|
+
// marks the fast path degraded, routing search to the JS-cosine fallback
|
|
1371
|
+
// over the (complete) BLOB table — honest degradation, not a hard failure.
|
|
1372
|
+
setVecFastPathReady(db, vecFailedCount === 0);
|
|
1373
|
+
if (vecFailedCount > 0) {
|
|
1374
|
+
warn(`[embed] ${vecFailedCount} sqlite-vec fast-path insert${vecFailedCount === 1 ? "" : "s"} failed — ` +
|
|
1375
|
+
"semantic search will use the slower JS-cosine fallback over stored embeddings. " +
|
|
1376
|
+
"Rebuild with 'akm index --full' after resolving the vec table (often a vector-dimension mismatch).");
|
|
1377
|
+
}
|
|
938
1378
|
onProgress({
|
|
939
1379
|
phase: "embeddings",
|
|
940
1380
|
message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
|
|
941
1381
|
});
|
|
942
1382
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
943
|
-
return { success: true };
|
|
1383
|
+
return { success: true, vecInsertFailures: vecFailedCount };
|
|
944
1384
|
}
|
|
945
1385
|
finally {
|
|
946
1386
|
if (heartbeatTimer)
|
|
@@ -1032,7 +1472,7 @@ function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
|
|
|
1032
1472
|
`Semantic search verification failed (${embeddingCount}/${embeddableEntries} embeddings available).`,
|
|
1033
1473
|
guidance: embeddingProvider === "remote"
|
|
1034
1474
|
? "Check your embedding endpoint and credentials, then retry `akm index --full --verbose`."
|
|
1035
|
-
: "Retry `akm index --full --verbose`. If it still fails, confirm local model downloads are permitted and see docs/configuration.md for local embedding dependency setup.",
|
|
1475
|
+
: "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.",
|
|
1036
1476
|
semanticSearchEnabled: true,
|
|
1037
1477
|
semanticSearchMode: config.semanticSearchMode,
|
|
1038
1478
|
semanticStatus: "blocked",
|
|
@@ -1042,29 +1482,22 @@ function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
|
|
|
1042
1482
|
vecAvailable,
|
|
1043
1483
|
};
|
|
1044
1484
|
}
|
|
1045
|
-
function buildIndexedDirCandidate(dirPath, indexableFiles, generated
|
|
1046
|
-
const
|
|
1047
|
-
? generated.entries.map((entry) => mergeLegacyEntry(entry, legacyOverrides.entries))
|
|
1048
|
-
: generated.entries;
|
|
1049
|
-
const stash = mergedEntries.length > 0 ? { entries: mergedEntries } : legacyOverrides;
|
|
1485
|
+
function buildIndexedDirCandidate(dirPath, indexableFiles, generated) {
|
|
1486
|
+
const stash = generated.entries.length > 0 ? { entries: generated.entries } : null;
|
|
1050
1487
|
const staleFiles = stash ? resolveIndexedFiles(dirPath, indexableFiles, stash) : indexableFiles;
|
|
1051
1488
|
return { stash, staleFiles };
|
|
1052
1489
|
}
|
|
1053
1490
|
function resolveIndexedFiles(dirPath, files, stash) {
|
|
1054
|
-
const fileBasenameMap = buildFileBasenameMap(files);
|
|
1055
1491
|
const resolved = new Set();
|
|
1056
1492
|
for (const entry of stash.entries) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
: matchEntryToFile(entry.name, fileBasenameMap);
|
|
1060
|
-
if (entryPath)
|
|
1061
|
-
resolved.add(entryPath);
|
|
1493
|
+
if (entry.filename)
|
|
1494
|
+
resolved.add(path.join(dirPath, entry.filename));
|
|
1062
1495
|
}
|
|
1063
1496
|
return resolved.size > 0 ? [...resolved] : files;
|
|
1064
1497
|
}
|
|
1065
|
-
async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db, entryKeys,
|
|
1498
|
+
async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db, entryKeys, akmConfig, onEntryDone) {
|
|
1066
1499
|
const { enhanceMetadata } = await import("../llm/metadata-enhance.js");
|
|
1067
|
-
const { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } = await import("
|
|
1500
|
+
const { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } = await import("../storage/repositories/index-llm-cache-repository.js");
|
|
1068
1501
|
const results = await concurrentMap(stash.entries, async (entry, idx) => {
|
|
1069
1502
|
if (signal?.aborted)
|
|
1070
1503
|
return entry;
|
|
@@ -1082,13 +1515,13 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1082
1515
|
warn(`Could not read file for LLM enrichment: ${entry.filename ?? entry.name}`);
|
|
1083
1516
|
}
|
|
1084
1517
|
}
|
|
1085
|
-
// Incremental cache: skip LLM call when file body is unchanged
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1518
|
+
// Incremental cache: skip LLM call when file body is unchanged. The
|
|
1519
|
+
// cache key is the entry_key (stashDir:type:name) which is stable
|
|
1520
|
+
// across index runs.
|
|
1088
1521
|
const cacheBody = fileContent ?? `${entry.name}\n${entry.description ?? ""}`;
|
|
1089
1522
|
const bodyHash = computeBodyHash(cacheBody);
|
|
1090
1523
|
const cacheKey = entryKeys?.[idx] ?? `${entry.type}:${entry.name}`;
|
|
1091
|
-
if (db
|
|
1524
|
+
if (db) {
|
|
1092
1525
|
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
1093
1526
|
if (cached) {
|
|
1094
1527
|
try {
|
|
@@ -1110,7 +1543,27 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1110
1543
|
}
|
|
1111
1544
|
}
|
|
1112
1545
|
}
|
|
1113
|
-
const
|
|
1546
|
+
const outcome = await enhanceMetadata(llmConfig, entry, fileContent, signal, akmConfig);
|
|
1547
|
+
if (outcome.status !== "enriched") {
|
|
1548
|
+
// Not a genuine LLM success: the gate was closed (`skipped`) or the
|
|
1549
|
+
// call errored/timed out (`failed`). Do NOT mark the entry enriched
|
|
1550
|
+
// and do NOT write the LLM cache — caching here would poison the
|
|
1551
|
+
// entry into a permanent enrichment skip even though nothing was
|
|
1552
|
+
// enhanced. Surface failures honestly; stay silent on gated-off skips.
|
|
1553
|
+
if (outcome.status === "failed") {
|
|
1554
|
+
const msg = outcome.error ?? "metadata enrichment failed";
|
|
1555
|
+
if (summary.failureSamples.length < 3 && !summary.failureSamples.includes(msg)) {
|
|
1556
|
+
summary.failureSamples.push(msg);
|
|
1557
|
+
}
|
|
1558
|
+
onEntryDone?.({ entryName: entry.name, outcome: "failed" });
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
summary.skipped++;
|
|
1562
|
+
onEntryDone?.({ entryName: entry.name, outcome: "skipped" });
|
|
1563
|
+
}
|
|
1564
|
+
return entry;
|
|
1565
|
+
}
|
|
1566
|
+
const improvements = outcome.metadata;
|
|
1114
1567
|
const updated = { ...entry };
|
|
1115
1568
|
if (improvements.description)
|
|
1116
1569
|
updated.description = improvements.description;
|
|
@@ -1118,7 +1571,11 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1118
1571
|
updated.searchHints = improvements.searchHints;
|
|
1119
1572
|
if (improvements.tags?.length)
|
|
1120
1573
|
updated.tags = improvements.tags;
|
|
1121
|
-
// Mark as enriched so subsequent index runs skip re-enrichment (P2)
|
|
1574
|
+
// Mark as enriched so subsequent index runs skip re-enrichment (P2).
|
|
1575
|
+
// An empty-but-successful response is still cached: the LLM was paid
|
|
1576
|
+
// for this body_hash and produced no improvements, so re-running would
|
|
1577
|
+
// only re-pay for the same no-op. (The cache protects against re-paying
|
|
1578
|
+
// for the LLM call when the file body is unchanged.)
|
|
1122
1579
|
updated.quality = "enriched";
|
|
1123
1580
|
// Persist to cache so the next run can skip the LLM call when the
|
|
1124
1581
|
// file body has not changed.
|
|
@@ -1144,8 +1601,8 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1144
1601
|
return entry;
|
|
1145
1602
|
}
|
|
1146
1603
|
},
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1604
|
+
// Defaults: 2 for remote LLM APIs, 1 for local model servers. No config
|
|
1605
|
+
// override reaches this path (see getDefaultLlmConcurrency).
|
|
1149
1606
|
getDefaultLlmConcurrency(llmConfig));
|
|
1150
1607
|
// concurrentMap returns Array<T | undefined>; filter out undefined slots
|
|
1151
1608
|
// (which can only occur if the callback itself returned undefined, which
|
|
@@ -1153,54 +1610,68 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1153
1610
|
const enhanced = results.map((r, i) => r ?? stash.entries[i]);
|
|
1154
1611
|
return { entries: enhanced };
|
|
1155
1612
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
// Only keep first match per base name to avoid ambiguity
|
|
1164
|
-
if (!map.has(base))
|
|
1165
|
-
map.set(base, file);
|
|
1166
|
-
}
|
|
1167
|
-
return map;
|
|
1613
|
+
// ── lookup ─────────────────────────────────────────────────────────────────
|
|
1614
|
+
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
1615
|
+
import { conceptIdFromTypeName } from "../core/asset/resolve-ref.js";
|
|
1616
|
+
async function resolveLookupSources() {
|
|
1617
|
+
const { loadConfig } = await import("../core/config/config.js");
|
|
1618
|
+
const { resolveSourceEntries } = await import("./search/search-source.js");
|
|
1619
|
+
return resolveSourceEntries(undefined, loadConfig());
|
|
1168
1620
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
* 1. Exact basename match: entry.name === filename without extension
|
|
1174
|
-
* 2. Last path segment match: for entries with names like "dir/sub-entry",
|
|
1175
|
-
* try matching the last segment
|
|
1176
|
-
* 3. No implicit file fallback: ambiguous legacy entries are skipped
|
|
1177
|
-
*/
|
|
1178
|
-
export function matchEntryToFile(entryName, fileMap) {
|
|
1179
|
-
// Exact match on entry name
|
|
1180
|
-
const exact = fileMap.get(entryName);
|
|
1181
|
-
if (exact)
|
|
1182
|
-
return exact;
|
|
1183
|
-
// Try last segment for hierarchical names (e.g. "corpus/agentic-patterns/foo")
|
|
1184
|
-
const lastSegment = entryName.split("/").pop() ?? entryName;
|
|
1185
|
-
if (lastSegment !== entryName) {
|
|
1186
|
-
const segmentMatch = fileMap.get(lastSegment);
|
|
1187
|
-
if (segmentMatch)
|
|
1188
|
-
return segmentMatch;
|
|
1189
|
-
}
|
|
1190
|
-
return null;
|
|
1621
|
+
function resolveLookupScope(bundle, sources) {
|
|
1622
|
+
if (!bundle)
|
|
1623
|
+
return { candidateDirs: sources.map((source) => source.path), qualified: false };
|
|
1624
|
+
return { candidateDirs: resolveSourcesForOrigin(bundle, sources).map((source) => source.path), qualified: true };
|
|
1191
1625
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1626
|
+
/** Resolve an adapter-owned `[bundle//]conceptId` without interpreting its path as an AKM type. */
|
|
1627
|
+
export async function lookupBundleRef(ref) {
|
|
1628
|
+
const sources = await resolveLookupSources();
|
|
1629
|
+
if (sources.length === 0)
|
|
1630
|
+
return null;
|
|
1631
|
+
const { candidateDirs, qualified } = resolveLookupScope(ref.bundle, sources);
|
|
1632
|
+
if (candidateDirs.length === 0)
|
|
1633
|
+
return null;
|
|
1634
|
+
const db = openExistingDatabase(getDbPath());
|
|
1635
|
+
try {
|
|
1636
|
+
const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, ref.conceptId);
|
|
1637
|
+
for (const dir of candidateDirs) {
|
|
1638
|
+
const id = findEntryIdByRef(db, inputRef, dir);
|
|
1639
|
+
if (id === undefined)
|
|
1640
|
+
continue;
|
|
1641
|
+
const row = db
|
|
1642
|
+
.prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type, " +
|
|
1643
|
+
"entry_json AS entryJson, item_ref AS itemRef, bundle_id AS bundleId, concept_id AS conceptId, " +
|
|
1644
|
+
"adapter_id AS adapterId FROM entries WHERE id = ?")
|
|
1645
|
+
.get(id);
|
|
1646
|
+
if (!row)
|
|
1647
|
+
continue;
|
|
1648
|
+
let document;
|
|
1649
|
+
try {
|
|
1650
|
+
document = JSON.parse(row.entryJson);
|
|
1651
|
+
}
|
|
1652
|
+
catch {
|
|
1653
|
+
// Corrupt optional projection does not erase the durable path identity.
|
|
1654
|
+
}
|
|
1655
|
+
if (!row.itemRef || !row.bundleId || !row.conceptId || !row.adapterId)
|
|
1656
|
+
continue;
|
|
1657
|
+
return {
|
|
1658
|
+
entryKey: row.entryKey,
|
|
1659
|
+
filePath: row.filePath,
|
|
1660
|
+
stashDir: row.stashDir,
|
|
1661
|
+
type: row.type,
|
|
1662
|
+
name: document?.name ?? ref.conceptId.split("/").pop() ?? ref.conceptId,
|
|
1663
|
+
adapterId: row.adapterId,
|
|
1664
|
+
document,
|
|
1665
|
+
itemRef: row.itemRef,
|
|
1666
|
+
bundleId: row.bundleId,
|
|
1667
|
+
conceptId: row.conceptId,
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
finally {
|
|
1673
|
+
closeDatabase(db);
|
|
1674
|
+
}
|
|
1204
1675
|
}
|
|
1205
1676
|
/**
|
|
1206
1677
|
* Look up a single asset by ref. Spec §6.2 — `akm show` queries this and
|
|
@@ -1208,70 +1679,11 @@ function mergeLegacyEntry(entry, legacyEntries) {
|
|
|
1208
1679
|
* file corresponds to which ref; the indexer walks `provider.path()` for
|
|
1209
1680
|
* every configured source, so this query covers all source kinds.
|
|
1210
1681
|
*
|
|
1211
|
-
* Match rules:
|
|
1212
|
-
* - `ref.origin === undefined` → first match across all sources (primary
|
|
1213
|
-
* source first, then in declared order — same priority as the indexer's
|
|
1214
|
-
* write order).
|
|
1215
|
-
* - `ref.origin === "local"` → primary source only (entry_key prefix is
|
|
1216
|
-
* the primary stash dir).
|
|
1217
|
-
* - `ref.origin === <name>` → restrict to the matching source name. We
|
|
1218
|
-
* resolve the source's directory and match on `entry_key` prefix.
|
|
1219
|
-
*
|
|
1220
1682
|
* Returns `null` when no row matches — callers translate that into a
|
|
1221
1683
|
* `NotFoundError` with their own messaging.
|
|
1222
1684
|
*/
|
|
1223
1685
|
export async function lookup(ref) {
|
|
1224
|
-
|
|
1225
|
-
const { resolveSourceEntries } = await import("./search/search-source.js");
|
|
1226
|
-
const config = loadConfig();
|
|
1227
|
-
const sources = resolveSourceEntries(undefined, config);
|
|
1228
|
-
if (sources.length === 0)
|
|
1229
|
-
return null;
|
|
1230
|
-
const dbPath = getDbPath();
|
|
1231
|
-
const db = openExistingDatabase(dbPath);
|
|
1232
|
-
try {
|
|
1233
|
-
const escapeLike = (value) => value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1234
|
-
// Canonical names strip .md for markdown assets, but users often pass
|
|
1235
|
-
// refs with .md (e.g. command:release.md). Normalize by trying both.
|
|
1236
|
-
const nameVariants = [ref.name];
|
|
1237
|
-
if (ref.name.endsWith(".md")) {
|
|
1238
|
-
nameVariants.push(ref.name.slice(0, -3));
|
|
1239
|
-
}
|
|
1240
|
-
const candidateDirs = (() => {
|
|
1241
|
-
if (!ref.origin)
|
|
1242
|
-
return sources.map((s) => s.path);
|
|
1243
|
-
if (ref.origin === "local")
|
|
1244
|
-
return [sources[0].path];
|
|
1245
|
-
const named = sources.find((s) => s.registryId === ref.origin);
|
|
1246
|
-
return named ? [named.path] : [];
|
|
1247
|
-
})();
|
|
1248
|
-
if (candidateDirs.length === 0)
|
|
1249
|
-
return null;
|
|
1250
|
-
for (const name of nameVariants) {
|
|
1251
|
-
const suffix = `:${ref.type}:${name}`;
|
|
1252
|
-
const escapedSuffix = escapeLike(suffix);
|
|
1253
|
-
for (const dir of candidateDirs) {
|
|
1254
|
-
const escapedDir = escapeLike(dir);
|
|
1255
|
-
const row = db
|
|
1256
|
-
.prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type FROM entries " +
|
|
1257
|
-
"WHERE entry_key LIKE ? ESCAPE '\\' AND entry_type = ? LIMIT 1")
|
|
1258
|
-
.get(`${escapedDir}${escapedSuffix}`, ref.type);
|
|
1259
|
-
if (row) {
|
|
1260
|
-
return {
|
|
1261
|
-
entryKey: row.entryKey,
|
|
1262
|
-
filePath: row.filePath,
|
|
1263
|
-
stashDir: row.stashDir,
|
|
1264
|
-
type: row.type,
|
|
1265
|
-
name: ref.name,
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
return null;
|
|
1271
|
-
}
|
|
1272
|
-
finally {
|
|
1273
|
-
closeDatabase(db);
|
|
1274
|
-
}
|
|
1686
|
+
return lookupBundleRef({ bundle: ref.origin, conceptId: conceptIdFromTypeName(ref.type, ref.name) });
|
|
1275
1687
|
}
|
|
1276
1688
|
// ── Utility score recomputation ──────────────────────────────────────────────
|
|
1277
1689
|
/** Retention window for usage events: events older than this are purged. */
|
|
@@ -1292,12 +1704,11 @@ const USAGE_EVENT_RETENTION_DAYS = 90;
|
|
|
1292
1704
|
*
|
|
1293
1705
|
* Called during `akm index` after FTS rebuild.
|
|
1294
1706
|
*/
|
|
1295
|
-
export function recomputeUtilityScores(db) {
|
|
1707
|
+
export function recomputeUtilityScores(db, stateDb) {
|
|
1296
1708
|
const EMA_DECAY = 0.7;
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
purgeOldUsageEvents(db, USAGE_EVENT_RETENTION_DAYS);
|
|
1709
|
+
// Purge stale usage events (90-day retention). usage_events lives in state.db
|
|
1710
|
+
// (Chunk-8 WI-8.3); its table is created by state migration 020.
|
|
1711
|
+
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
|
|
1301
1712
|
// Time-proportional decay: apply one round of EMA per elapsed day so
|
|
1302
1713
|
// indexing frequency doesn't affect how fast scores decay.
|
|
1303
1714
|
const lastComputedAt = getMeta(db, "last_utility_computed_at");
|
|
@@ -1308,15 +1719,14 @@ export function recomputeUtilityScores(db) {
|
|
|
1308
1719
|
}
|
|
1309
1720
|
const emaDecay = EMA_DECAY ** elapsedDays;
|
|
1310
1721
|
const emaNew = 1 - emaDecay; // complement so weights still sum to 1
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
// entries, so its entry_id can become stale (entry deleted, re-keyed,
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
|
|
1319
|
-
const usageRows = db
|
|
1722
|
+
// Aggregate explicit user demand per entry_id from state.db's usage_events, then keep only entries
|
|
1723
|
+
// that STILL EXIST in index.db's `entries` (the former in-SQL JOIN is now a
|
|
1724
|
+
// cross-DB filter). This latter check is critical: usage_events has no FK to
|
|
1725
|
+
// entries, so its entry_id can become stale (entry deleted, re-keyed, moved
|
|
1726
|
+
// between sources). Without it, writing the derived row to utility_scores
|
|
1727
|
+
// (which DOES have an FK) raises "FOREIGN KEY constraint failed" and rolls
|
|
1728
|
+
// back the whole finalize transaction — failing every index run.
|
|
1729
|
+
const aggregatedRows = stateDb
|
|
1320
1730
|
.prepare(`
|
|
1321
1731
|
SELECT u.entry_id,
|
|
1322
1732
|
SUM(CASE WHEN u.event_type = 'search' THEN 1 ELSE 0 END) AS search_count,
|
|
@@ -1325,15 +1735,13 @@ export function recomputeUtilityScores(db) {
|
|
|
1325
1735
|
SUM(CASE WHEN u.event_type = 'feedback' AND u.signal = 'negative' THEN 1 ELSE 0 END) AS negative_feedback_count,
|
|
1326
1736
|
MAX(u.created_at) AS last_used_at
|
|
1327
1737
|
FROM usage_events u
|
|
1328
|
-
JOIN entries e ON e.id = u.entry_id
|
|
1329
1738
|
WHERE u.entry_id IS NOT NULL
|
|
1739
|
+
AND u.source = 'user'
|
|
1330
1740
|
GROUP BY u.entry_id
|
|
1331
1741
|
`)
|
|
1332
1742
|
.all();
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1743
|
+
const entryExists = db.prepare("SELECT 1 FROM entries WHERE id = ?");
|
|
1744
|
+
const usageByEntry = new Map(aggregatedRows.filter((row) => entryExists.get(row.entry_id) != null).map((row) => [row.entry_id, row]));
|
|
1337
1745
|
// Batch-load existing utility scores
|
|
1338
1746
|
const existingScores = new Map();
|
|
1339
1747
|
const scoreRows = db.prepare("SELECT entry_id, utility, last_used_at FROM utility_scores").all();
|
|
@@ -1341,7 +1749,16 @@ export function recomputeUtilityScores(db) {
|
|
|
1341
1749
|
existingScores.set(row.entry_id, { utility: row.utility, lastUsedAt: row.last_used_at ?? undefined });
|
|
1342
1750
|
}
|
|
1343
1751
|
const now = new Date().toISOString();
|
|
1344
|
-
|
|
1752
|
+
const entryIds = new Set([...existingScores.keys(), ...usageByEntry.keys()]);
|
|
1753
|
+
for (const entryId of entryIds) {
|
|
1754
|
+
const row = usageByEntry.get(entryId) ?? {
|
|
1755
|
+
entry_id: entryId,
|
|
1756
|
+
search_count: 0,
|
|
1757
|
+
show_count: 0,
|
|
1758
|
+
positive_feedback_count: 0,
|
|
1759
|
+
negative_feedback_count: 0,
|
|
1760
|
+
last_used_at: null,
|
|
1761
|
+
};
|
|
1345
1762
|
const selectRate = row.search_count > 0 ? Math.min(1, row.show_count / row.search_count) : 0;
|
|
1346
1763
|
const feedbackTotal = row.positive_feedback_count + row.negative_feedback_count;
|
|
1347
1764
|
const feedbackRate = feedbackTotal > 0 ? Math.max(0, row.positive_feedback_count - row.negative_feedback_count) / feedbackTotal : 0;
|