akm-cli 0.9.0-rc.8 → 0.9.0
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 +1063 -44
- package/README.md +51 -25
- package/SECURITY.md +14 -1
- package/STABILITY.md +497 -0
- package/dist/akm +148 -35
- package/dist/{akm-migrate-storage → akm-migrate} +6 -9
- package/dist/assets/hints/cli-hints-full.md +223 -95
- package/dist/assets/hints/cli-hints-short.md +85 -22
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/stash-skeleton/README.md +6 -5
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
- package/dist/assets/tasks/core/extract.yml +1 -1
- package/dist/assets/tasks/core/version-check.yml +1 -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 +1 -3
- package/dist/assets/workflows/workflow-template.md +32 -15
- package/dist/cli/invocation.js +40 -15
- package/dist/cli/parse-args.js +0 -22
- package/dist/cli/retired-commands.js +121 -0
- package/dist/cli/shared.js +154 -22
- package/dist/cli/unknown-flags.js +236 -0
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +696 -258
- package/dist/commands/agent/agent-dispatch.js +14 -3
- package/dist/commands/agent/contribute-cli.js +73 -88
- package/dist/commands/completions.js +79 -22
- package/dist/commands/config-cli.js +17 -150
- package/dist/commands/env/env-cli.js +59 -143
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +36 -66
- package/dist/commands/env/secret.js +24 -57
- package/dist/commands/feedback-cli.js +141 -87
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +3 -4
- package/dist/commands/health/checks.js +85 -23
- package/dist/commands/health/html-report.js +7 -10
- package/dist/commands/health/improve-metrics.js +25 -83
- package/dist/commands/health/md-report.js +5 -9
- package/dist/commands/health/metrics.js +62 -20
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +4 -5
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +3 -48
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/commands/health/types-improve.js +7 -0
- package/dist/commands/health.js +99 -28
- package/dist/commands/improve/anti-collapse.js +2 -2
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +41 -40
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +140 -1000
- package/dist/commands/improve/distill/promote-memory.js +12 -12
- package/dist/commands/improve/distill/quality-gate.js +6 -6
- package/dist/commands/improve/distill.js +58 -69
- package/dist/commands/improve/eligibility.js +105 -57
- package/dist/commands/improve/extract-cli.js +14 -133
- package/dist/commands/improve/improve-cli.js +98 -114
- package/dist/commands/improve/improve-result-file.js +1 -28
- package/dist/commands/improve/improve-strategies.js +8 -5
- package/dist/commands/improve/improve.js +128 -91
- package/dist/commands/improve/loop-stages.js +182 -20
- package/dist/commands/improve/memory/derived-ref.js +45 -43
- package/dist/commands/improve/memory/memory-belief.js +1 -1
- package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
- package/dist/commands/improve/memory/memory-improve.js +6 -5
- package/dist/commands/improve/outcome-loop.js +22 -65
- package/dist/commands/improve/preparation.js +114 -123
- package/dist/commands/improve/proactive-maintenance.js +2 -5
- package/dist/commands/improve/reflect.js +56 -160
- package/dist/commands/improve/salience.js +11 -122
- package/dist/commands/improve/source-identity.js +10 -38
- package/dist/commands/lint/base-linter.js +20 -124
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +249 -43
- package/dist/commands/{events.js → log.js} +33 -38
- package/dist/commands/migrate-cli.js +92 -12
- package/dist/commands/migration-tool.js +46 -0
- package/dist/commands/observability-cli.js +70 -209
- package/dist/commands/proposal/drain.js +101 -29
- package/dist/commands/proposal/proposal-cli.js +76 -48
- package/dist/commands/proposal/proposal.js +54 -18
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +23 -15
- package/dist/commands/proposal/repository.js +701 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
- package/dist/commands/proposal/validators/proposal-validators.js +55 -7
- package/dist/commands/proposal/validators/proposals.js +4 -7
- package/dist/commands/read/curate.js +34 -53
- package/dist/commands/read/knowledge.js +150 -95
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +42 -15
- package/dist/commands/read/search-cli.js +180 -78
- package/dist/commands/read/search.js +58 -43
- package/dist/commands/read/show.js +197 -141
- package/dist/commands/registry-cli.js +12 -51
- package/dist/commands/remember.js +14 -57
- package/dist/commands/sources/add-cli.js +100 -31
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +7 -2
- package/dist/commands/sources/info.js +18 -5
- package/dist/commands/sources/init.js +12 -12
- package/dist/commands/sources/installed-stashes.js +382 -98
- package/dist/commands/sources/schema-repair.js +3 -2
- package/dist/commands/sources/self-update.js +131 -38
- package/dist/commands/sources/source-add.js +72 -17
- package/dist/commands/sources/source-clone.js +129 -45
- package/dist/commands/sources/source-manage.js +43 -23
- package/dist/commands/sources/sources-cli.js +57 -208
- package/dist/commands/sources/stash-cli.js +46 -53
- package/dist/commands/tasks/tasks-cli.js +91 -97
- package/dist/commands/tasks/tasks.js +276 -421
- package/dist/commands/workflow-cli.js +175 -450
- package/dist/core/adapter/adapters/akm-adapter.js +47 -28
- package/dist/core/adapter/adapters/akm-lint.js +42 -27
- package/dist/core/adapter/adapters/akm-metadata.js +15 -44
- package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
- package/dist/core/adapter/adapters/index.js +6 -6
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
- package/dist/core/adapter/adapters/okf-adapter.js +187 -19
- package/dist/core/adapter/adapters/shared.js +3 -19
- package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +6 -4
- package/dist/core/adapter/validate-context.js +214 -0
- package/dist/core/asset/akm-markdown.js +63 -0
- package/dist/core/asset/asset-placement.js +20 -6
- package/dist/core/asset/asset-ref.js +11 -9
- package/dist/core/asset/frontmatter-lint.js +30 -0
- package/dist/core/asset/frontmatter.js +37 -9
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +89 -18
- package/dist/core/asset/stash-meta.js +1 -1
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +152 -38
- package/dist/core/config/config-io.js +12 -1
- package/dist/core/config/config-schema.js +35 -8
- package/dist/core/config/config-sources.js +55 -11
- package/dist/core/config/config-walker.js +25 -9
- package/dist/core/config/config.js +9 -48
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +5 -1
- package/dist/core/config/schema/experimental.js +30 -0
- package/dist/core/config/schema/improve-processes.js +0 -6
- package/dist/core/config/schema/improve.js +21 -3
- package/dist/core/config/schema/index-config.js +8 -15
- package/dist/core/config/schema/output.js +4 -1
- package/dist/core/config/schema/setup.js +9 -18
- package/dist/core/config/schema/sources-bundles.js +49 -33
- package/dist/core/config/schema/workflow.js +3 -3
- package/dist/core/env-secret-ref.js +76 -46
- package/dist/core/errors.js +18 -12
- package/dist/core/events.js +46 -128
- package/dist/core/file-change.js +6 -5
- package/dist/core/fs-txn.js +83 -7
- package/dist/core/git-message.js +2 -2
- package/dist/core/improve-result.js +1 -100
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +2 -1
- package/dist/core/migration-operation.js +16 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/parse.js +4 -1
- package/dist/core/paths.js +17 -20
- package/dist/core/recognition-util.js +12 -14
- package/dist/core/redaction.js +34 -0
- package/dist/core/standards/resolve-standards-context.js +2 -14
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +2 -2
- package/dist/core/state/migrations.js +41 -18
- package/dist/core/state-db.js +5 -14
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +6 -4
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/type-presentation.js +3 -3
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +771 -95
- package/dist/indexer/bundle-identity-guard.js +3 -2
- package/dist/indexer/db/graph-db.js +0 -24
- package/dist/indexer/ensure-index.js +1 -0
- package/dist/indexer/graph/graph-boost.js +9 -34
- package/dist/indexer/graph/graph-extraction.js +8 -5
- package/dist/indexer/index-writer-lock.js +53 -17
- package/dist/indexer/index-written-assets.js +16 -22
- package/dist/indexer/indexer.js +497 -239
- package/dist/indexer/installations.js +14 -96
- package/dist/indexer/passes/dir-staleness.js +16 -9
- package/dist/indexer/passes/memory-inference.js +11 -9
- package/dist/indexer/passes/metadata.js +113 -47
- package/dist/indexer/scan/doc-to-entry.js +38 -1
- package/dist/indexer/scan/drain-dir.js +13 -23
- package/dist/indexer/search/db-search.js +99 -54
- package/dist/indexer/search/fts-query.js +47 -24
- package/dist/indexer/search/ranking-contributors.js +42 -20
- package/dist/indexer/search/ranking.js +18 -99
- package/dist/indexer/search/search-fields.js +7 -2
- package/dist/indexer/search/search-source.js +82 -93
- package/dist/indexer/usage/usage-events.js +0 -89
- package/dist/indexer/walk/file-context.js +2 -1
- package/dist/indexer/walk/matchers.js +30 -43
- package/dist/indexer/walk/path-resolver.js +7 -2
- package/dist/indexer/walk/walker.js +38 -12
- package/dist/integrations/agent/builders.js +0 -6
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/engine-fallback.js +76 -0
- package/dist/integrations/agent/profiles.js +14 -0
- package/dist/integrations/agent/prompts.js +12 -8
- package/dist/integrations/agent/runner-dispatch.js +4 -2
- package/dist/integrations/agent/runner.js +0 -1
- package/dist/integrations/agent/spawn.js +5 -6
- package/dist/integrations/github.js +1 -1
- package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
- package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
- package/dist/integrations/harnesses/claude/session-log.js +0 -10
- package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
- package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
- package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
- package/dist/integrations/harnesses/index.js +3 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
- package/dist/integrations/harnesses/opencode/session-log.js +0 -15
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
- package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
- package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
- package/dist/integrations/lockfile.js +101 -6
- package/dist/integrations/session-logs/index.js +3 -28
- package/dist/llm/client.js +136 -100
- package/dist/llm/embedders/remote.js +13 -5
- package/dist/llm/feature-gate.js +4 -12
- package/dist/llm/graph-extract.js +5 -11
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +5 -7
- package/dist/llm/structured-call.js +1 -1
- package/dist/llm/usage-persist.js +26 -5
- package/dist/llm/usage-telemetry.js +25 -2
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +259 -0
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -36
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +56 -83
- package/dist/output/shapes/migrate.js +8 -0
- package/dist/output/shapes/passthrough.js +7 -41
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes.js +2 -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 +164 -96
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/health-format.js +103 -0
- package/dist/output/text/health.js +7 -0
- package/dist/output/text/helpers.js +10 -8
- package/dist/output/text/lint-format.js +43 -0
- package/dist/output/text/{save.js → lint.js} +2 -2
- package/dist/output/text/migrate.js +88 -0
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +44 -72
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/show-directives.js +15 -7
- package/dist/output/text/status-list.js +32 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +24 -203
- package/dist/output/text/workflow.js +1 -7
- package/dist/output/text.js +16 -17
- 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 +13 -23
- package/dist/registry/resolve.js +42 -7
- package/dist/registry/semver.js +34 -84
- package/dist/runtime.js +2 -23
- package/dist/scripts/akm-migrate-node.js +60290 -0
- package/dist/scripts/akm-migrate.js +59628 -0
- package/dist/setup/detect.js +42 -15
- package/dist/setup/registry-stash-loader.js +2 -2
- package/dist/setup/setup.js +236 -136
- package/dist/setup/steps/connection.js +7 -9
- package/dist/setup/steps/platforms.js +9 -9
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +12 -13
- package/dist/setup/steps/stashdir.js +2 -3
- package/dist/setup/steps/tasks.js +237 -120
- package/dist/sources/freshness.js +1 -1
- 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 +25 -23
- package/dist/sources/providers/git-stash.js +395 -106
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +7 -4
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +6 -1
- package/dist/sources/resolve.js +6 -5
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
- package/dist/sources/snapshot-fetchers/github.js +100 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
- package/dist/sources/snapshot-fetchers/registry.js +17 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +282 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
- package/dist/sources/snapshot-fetchers/x.js +910 -0
- package/dist/storage/database.js +7 -0
- package/dist/storage/engines/sqlite-migrations.js +23 -111
- package/dist/storage/managed-db.js +2 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +27 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -12
- package/dist/storage/repositories/index-connection.js +17 -6
- package/dist/storage/repositories/index-entries-repository.js +151 -240
- package/dist/storage/repositories/index-entry-mapper.js +15 -11
- package/dist/storage/repositories/index-fts-repository.js +5 -2
- package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
- package/dist/storage/repositories/index-meta-repository.js +2 -3
- package/dist/storage/repositories/index-schema.js +10 -25
- package/dist/storage/repositories/index-utility-repository.js +15 -28
- package/dist/storage/repositories/index-vec-repository.js +6 -1
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +296 -59
- package/dist/storage/repositories/registry-cache.js +19 -0
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +15 -13
- package/dist/storage/repositories/workflow-runs-repository.js +52 -40
- package/dist/tasks/backends/cron.js +105 -15
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +85 -38
- package/dist/tasks/backends/schtasks.js +135 -15
- package/dist/tasks/embedded.js +56 -40
- package/dist/tasks/parser.js +7 -157
- package/dist/tasks/resolve-akm-bin.js +137 -59
- package/dist/tasks/runner.js +79 -42
- package/dist/tasks/scheduler-invocation.js +220 -10
- package/dist/tasks/schema.js +24 -1
- package/dist/tasks/task-id.js +1 -3
- package/dist/tasks/validator.js +20 -6
- package/dist/workflows/authoring/authoring.js +94 -143
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +77 -57
- package/dist/workflows/exec/param-secrets.js +9 -9
- package/dist/workflows/exec/run-workflow.js +133 -79
- package/dist/workflows/exec/step-work.js +219 -346
- package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
- package/dist/workflows/ir/compile.js +141 -270
- package/dist/workflows/ir/freeze.js +40 -30
- package/dist/workflows/ir/params.js +135 -11
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +25 -26
- package/dist/workflows/parser.js +872 -307
- package/dist/workflows/program/expressions.js +20 -208
- package/dist/workflows/program/schema.js +7 -10
- package/dist/workflows/renderer.js +95 -68
- package/dist/workflows/resource-limits.js +2 -0
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/plan-classifier.js +16 -75
- package/dist/workflows/runtime/runs.js +186 -127
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +2 -2
- package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +30 -36
- package/dist/workflows/validator.js +21 -62
- package/docs/README.md +68 -0
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +11 -11
- package/docs/migration/release-notes/0.9.0.md +208 -27
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +564 -208
- package/docs/migration/v0.9.0-troubleshooting.md +561 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/cli.md +2253 -0
- package/docs/reference/configuration.md +358 -0
- package/docs/reference/data-and-telemetry.md +105 -42
- package/docs/reference/workflows.md +647 -0
- package/package.json +22 -11
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +81 -128
- package/schemas/akm-workflow.json +74 -73
- package/dist/assets/tasks/core/backup.yml +0 -5
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/cli/config-migrate.js +0 -1806
- package/dist/cli/config-validate.js +0 -41
- package/dist/commands/backup-cli.js +0 -56
- package/dist/commands/bundle/bundle-cli.js +0 -68
- package/dist/commands/bundle/bundle.js +0 -219
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -489
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/mv-cli.js +0 -1221
- package/dist/commands/sources/history.js +0 -201
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/core/migration-backup.js +0 -1234
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/migrate/legacy/config-source-migration.js +0 -223
- package/dist/migrate/legacy/content-migration.js +0 -305
- package/dist/migrate/legacy/legacy-layout.js +0 -779
- package/dist/migrate/legacy/legacy-paths.js +0 -25
- package/dist/migrate/legacy/legacy-stash-json.js +0 -72
- package/dist/migrate/legacy/proposal-fs-import.js +0 -168
- package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
- package/dist/migrate/legacy/three-db-cutover.js +0 -841
- package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
- package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
- package/dist/migrate/legacy-ref-grammar.js +0 -214
- 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/registry/build-index.js +0 -382
- package/dist/schemas/akm-config.json +0 -4704
- package/dist/schemas/akm-task.json +0 -87
- package/dist/schemas/akm-workflow.json +0 -372
- package/dist/scripts/migrate-storage.js +0 -3816
- package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
- package/dist/workflows/cli.js +0 -53
- package/dist/workflows/exec/brief.js +0 -481
- package/dist/workflows/exec/report.js +0 -1460
- package/dist/workflows/exec/watch.js +0 -116
- package/dist/workflows/program/parser.js +0 -813
- package/dist/workflows/program/project.js +0 -104
package/dist/indexer/indexer.js
CHANGED
|
@@ -3,22 +3,23 @@
|
|
|
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 { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
6
7
|
import { adapterForId } from "../core/adapter/registry.js";
|
|
7
8
|
import { isHttpUrl, toErrorMessage } from "../core/common.js";
|
|
8
9
|
import { concurrentMap } from "../core/concurrent.js";
|
|
9
|
-
import { recoverTxnsForRoot } from "../core/fs-txn.js";
|
|
10
10
|
import { getDbPath } from "../core/paths.js";
|
|
11
11
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
12
12
|
import { withStateDb } from "../core/state-db.js";
|
|
13
13
|
import { isVerbose, warn, warnVerbose } from "../core/warn.js";
|
|
14
14
|
import { resolveIndexPassLLM } from "../llm/index-passes.js";
|
|
15
|
+
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
15
16
|
import { closeDatabase, openExistingDatabase, openIndexDatabase } from "../storage/repositories/index-connection.js";
|
|
16
|
-
import { deleteEntriesByDirExceptKeys, deleteEntriesByIds, deleteEntriesByStashDir, getEmbeddableEntryCount, getEntryCount, relinkUsageEvents, upsertEntry, upsertWorkflowDocument, } from "../storage/repositories/index-entries-repository.js";
|
|
17
|
+
import { deleteEntriesByDirAndStash, deleteEntriesByDirExceptKeys, deleteEntriesByIds, deleteEntriesByStashDir, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedDirPathsByStashDir, getIndexedStashDirsByDir, relinkUsageEvents, upsertEntry, upsertWorkflowDocument, } from "../storage/repositories/index-entries-repository.js";
|
|
17
18
|
import { rebuildFts } from "../storage/repositories/index-fts-repository.js";
|
|
18
19
|
import { clearStaleCacheEntries } from "../storage/repositories/index-llm-cache-repository.js";
|
|
19
|
-
import { deleteIndexDirStatesByStashDir, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
20
|
+
import { deleteIndexDirState, deleteIndexDirStatesByStashDir, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
20
21
|
import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
21
|
-
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
22
|
+
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
22
23
|
import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
|
|
23
24
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
24
25
|
import { withIndexWriterLease } from "./index-writer-lock.js";
|
|
@@ -29,7 +30,7 @@ import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
|
29
30
|
import { buildSearchText } from "./search/search-fields.js";
|
|
30
31
|
import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
|
|
31
32
|
import { purgeOldUsageEvents } from "./usage/usage-events.js";
|
|
32
|
-
import {
|
|
33
|
+
import { walkStashFlatWithStatus } from "./walk/walker.js";
|
|
33
34
|
function throwIfAborted(signal) {
|
|
34
35
|
if (signal?.aborted) {
|
|
35
36
|
throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
|
|
@@ -54,7 +55,11 @@ export function getDefaultLlmConcurrency(llmConfig) {
|
|
|
54
55
|
// Remote endpoints default to a modest 2-wide pool (owner ruling 2026-07-21):
|
|
55
56
|
// enough to overlap request latency without hammering rate-limited APIs.
|
|
56
57
|
// Local model servers stay at 1 (single loaded model; parallel requests
|
|
57
|
-
// trigger reload thrash).
|
|
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).
|
|
58
63
|
return 2;
|
|
59
64
|
}
|
|
60
65
|
// ── Phase functions ──────────────────────────────────────────────────────────
|
|
@@ -86,9 +91,7 @@ async function runSourceCachePhase(ctx) {
|
|
|
86
91
|
for (const dir of prevStashDirs) {
|
|
87
92
|
if (!currentSet.has(dir)) {
|
|
88
93
|
ctx.hadRemovedSources = true;
|
|
89
|
-
|
|
90
|
-
deleteIndexDirStatesByStashDir(db, dir);
|
|
91
|
-
deleteStoredGraph(db, dir);
|
|
94
|
+
ctx.removedSourceDirs.push(dir);
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -97,6 +100,15 @@ async function runSourceCachePhase(ctx) {
|
|
|
97
100
|
// further to do here. The flag is exposed on ctx for runWalkPhase().
|
|
98
101
|
void config;
|
|
99
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
|
+
}
|
|
100
112
|
/**
|
|
101
113
|
* Walk phase: scan the filesystem, generate metadata, and persist entries to
|
|
102
114
|
* the database. Also kicks off LLM enrichment for directories that need it.
|
|
@@ -105,16 +117,17 @@ async function runSourceCachePhase(ctx) {
|
|
|
105
117
|
* `ctx.walkWarnings`, and `ctx.dirsNeedingLlm` for downstream phases.
|
|
106
118
|
*/
|
|
107
119
|
async function runWalkPhase(ctx) {
|
|
108
|
-
const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full,
|
|
120
|
+
const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full, clean, signal, onProgress, config } = ctx;
|
|
109
121
|
throwIfAborted(signal);
|
|
110
122
|
ctx.timing.tWalkStart = Date.now();
|
|
111
123
|
const doFullDelete = full || !isIncremental;
|
|
112
|
-
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);
|
|
113
125
|
ctx.scannedDirs = scannedDirs;
|
|
114
126
|
ctx.skippedDirs = skippedDirs;
|
|
115
127
|
ctx.generatedCount = generatedCount;
|
|
116
128
|
ctx.walkWarnings = warnings;
|
|
117
129
|
ctx.dirsNeedingLlm = dirsNeedingLlm;
|
|
130
|
+
ctx.scanComplete = complete;
|
|
118
131
|
onProgress({
|
|
119
132
|
phase: "scan",
|
|
120
133
|
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
@@ -135,7 +148,7 @@ async function runWalkPhase(ctx) {
|
|
|
135
148
|
ctx.timing.tWalkEnd = Date.now();
|
|
136
149
|
throwIfAborted(signal);
|
|
137
150
|
// LLM enrichment for directories that need it
|
|
138
|
-
await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
151
|
+
await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal);
|
|
139
152
|
onProgress({
|
|
140
153
|
phase: "llm",
|
|
141
154
|
message: resolveIndexPassLLM("enrichment", config)
|
|
@@ -193,15 +206,19 @@ async function runFinalizePhase(ctx) {
|
|
|
193
206
|
/* ignore */
|
|
194
207
|
}
|
|
195
208
|
throwIfAborted(signal);
|
|
196
|
-
//
|
|
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.
|
|
197
212
|
const embeddingResult = ctx.embeddingResult ?? { success: false };
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
if (ctx.scanComplete) {
|
|
214
|
+
setMeta(db, "builtAt", new Date().toISOString());
|
|
215
|
+
setMeta(db, "stashDir", stashDir);
|
|
216
|
+
setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
|
|
217
|
+
}
|
|
201
218
|
setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
|
|
202
219
|
// Stash-organization conventions (SPEC-8): track which `index.indexBodyOpening`
|
|
203
220
|
// state the index was built with, and warn while the flag diverges from it.
|
|
204
|
-
const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, ctx.full || !isIncremental);
|
|
221
|
+
const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, (ctx.full || !isIncremental) && ctx.scanComplete);
|
|
205
222
|
if (bodyOpeningWarning)
|
|
206
223
|
warn(bodyOpeningWarning);
|
|
207
224
|
warnIfVecMissing(db);
|
|
@@ -264,7 +281,7 @@ export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
|
|
|
264
281
|
`${flagEnabled ? "disabled" : "enabled"}. Incremental runs only re-extract changed files, so ` +
|
|
265
282
|
"indexed text and embeddings are stale for unchanged entries. Run `akm index --full` to apply the new " +
|
|
266
283
|
"setting everywhere (embeddings regenerate), and re-mint collapse-detector canary baselines via " +
|
|
267
|
-
"`
|
|
284
|
+
"`bun scripts/refresh-canary-set.ts --refresh` if you use them.");
|
|
268
285
|
}
|
|
269
286
|
// ── Clean pass ───────────────────────────────────────────────────────────────
|
|
270
287
|
/**
|
|
@@ -302,7 +319,82 @@ export async function akmIndex(options) {
|
|
|
302
319
|
return akmIndexOverride(options);
|
|
303
320
|
return akmIndexReal(options);
|
|
304
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
|
+
}
|
|
305
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
|
+
}
|
|
306
398
|
const requestedAt = Date.now();
|
|
307
399
|
let acquiredAt = requestedAt;
|
|
308
400
|
return withIndexWriterLease({
|
|
@@ -321,33 +413,41 @@ async function akmIndexReal(options) {
|
|
|
321
413
|
const stashDir = options.stashDir;
|
|
322
414
|
const onProgress = options?.onProgress ?? (() => { });
|
|
323
415
|
const signal = options?.signal;
|
|
324
|
-
const reEnrich = options?.reEnrich === true;
|
|
325
416
|
const full = options?.full === true;
|
|
326
417
|
const clean = options?.clean === true;
|
|
327
418
|
const dryRun = options?.dryRun === true;
|
|
328
419
|
// Load config and resolve all stash sources
|
|
329
|
-
const { loadConfig } = await import("../core/config/config.js");
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
|
|
336
|
-
warnOnUnmigratedVaults(stashDir);
|
|
420
|
+
const { loadConfig, mutateConfig } = await import("../core/config/config.js");
|
|
421
|
+
let config = loadConfig();
|
|
422
|
+
// Durable state must be runtime-compatible before source hydration,
|
|
423
|
+
// adapter persistence, or index.db creation can mutate the installation.
|
|
424
|
+
onProgress({ phase: "preflight", message: "Validating durable state." });
|
|
425
|
+
withStateDb(() => undefined);
|
|
337
426
|
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
338
427
|
// so their content directories exist on disk for the walker to discover.
|
|
339
428
|
const sourceCacheStart = Date.now();
|
|
340
429
|
onProgress({ phase: "preflight", message: "Hydrating source caches." });
|
|
341
430
|
const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
|
|
342
|
-
|
|
431
|
+
// Inject the store-backed secret resolver from here — a composition root
|
|
432
|
+
// ABOVE the provider/fetcher import cycle (this module reaches
|
|
433
|
+
// search-source only via dynamic import). This is what lets a website
|
|
434
|
+
// source's X fetcher resolve `secrets/x-bearer-token` during
|
|
435
|
+
// bundle-update / hydrate, not just from the command-layer URL-ingest
|
|
436
|
+
// path. `secret-seam` is imported here, never from inside the cycle.
|
|
437
|
+
const { storeSecretResolver } = await import("../sources/snapshot-fetchers/secret-seam.js");
|
|
438
|
+
await ensureSourceCaches(config, {
|
|
439
|
+
force: full,
|
|
440
|
+
materialize: options.hydrateSources !== false,
|
|
441
|
+
secrets: storeSecretResolver,
|
|
442
|
+
});
|
|
343
443
|
const sourceCacheEnd = Date.now();
|
|
344
444
|
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
445
|
+
const detected = detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, {
|
|
446
|
+
announce: options.implicit !== true,
|
|
447
|
+
});
|
|
448
|
+
config = detected.config;
|
|
449
|
+
const persistedAdapters = detected.persistedAdapters;
|
|
345
450
|
const allSourceDirs = allSourceEntries.map((s) => s.path);
|
|
346
|
-
for (const sourceDir of new Set([stashDir, ...allSourceDirs])) {
|
|
347
|
-
// Unified fs-txn engine (WI-6.3): finish/roll back interrupted mv
|
|
348
|
-
// transactions for every source root before indexing walks it.
|
|
349
|
-
await recoverTxnsForRoot(sourceDir, (journal) => journal.kind === "mv");
|
|
350
|
-
}
|
|
351
451
|
onProgress({
|
|
352
452
|
phase: "preflight",
|
|
353
453
|
message: `Resolved ${allSourceDirs.length} stash source${allSourceDirs.length === 1 ? "" : "s"}.`,
|
|
@@ -370,7 +470,7 @@ async function akmIndexReal(options) {
|
|
|
370
470
|
sources: allSourceEntries,
|
|
371
471
|
sourceDirs: allSourceDirs,
|
|
372
472
|
full,
|
|
373
|
-
|
|
473
|
+
clean,
|
|
374
474
|
stashDir,
|
|
375
475
|
onProgress,
|
|
376
476
|
signal,
|
|
@@ -387,6 +487,8 @@ async function akmIndexReal(options) {
|
|
|
387
487
|
isIncremental,
|
|
388
488
|
builtAtMs,
|
|
389
489
|
hadRemovedSources: false,
|
|
490
|
+
removedSourceDirs: [],
|
|
491
|
+
scanComplete: true,
|
|
390
492
|
scannedDirs: 0,
|
|
391
493
|
skippedDirs: 0,
|
|
392
494
|
generatedCount: 0,
|
|
@@ -408,6 +510,7 @@ async function akmIndexReal(options) {
|
|
|
408
510
|
// ── Phase sequence ───────────────────────────────────────────────────────
|
|
409
511
|
await runSourceCachePhase(ctx);
|
|
410
512
|
await runWalkPhase(ctx);
|
|
513
|
+
applyRemovedSources(ctx);
|
|
411
514
|
await runEmbeddingPhase(ctx);
|
|
412
515
|
await runFinalizePhase(ctx);
|
|
413
516
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -425,7 +528,13 @@ async function akmIndexReal(options) {
|
|
|
425
528
|
phase: "finalize",
|
|
426
529
|
message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
|
|
427
530
|
});
|
|
428
|
-
|
|
531
|
+
if (ctx.scanComplete) {
|
|
532
|
+
cleanResult = runCleanPass(db, dryRun);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
warn("[index] --clean skipped because one or more configured sources were not scanned completely.");
|
|
536
|
+
cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
|
|
537
|
+
}
|
|
429
538
|
}
|
|
430
539
|
const cleanEnd = Date.now();
|
|
431
540
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -438,6 +547,9 @@ async function akmIndexReal(options) {
|
|
|
438
547
|
directoriesScanned: ctx.scannedDirs,
|
|
439
548
|
directoriesSkipped: ctx.skippedDirs,
|
|
440
549
|
...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
|
|
550
|
+
...(Object.keys(persistedAdapters).length > 0
|
|
551
|
+
? { configUpdated: { detectedAdapters: persistedAdapters } }
|
|
552
|
+
: {}),
|
|
441
553
|
verification,
|
|
442
554
|
timing: {
|
|
443
555
|
totalMs: Date.now() - timing.t0,
|
|
@@ -460,6 +572,13 @@ async function akmIndexReal(options) {
|
|
|
460
572
|
}
|
|
461
573
|
});
|
|
462
574
|
}
|
|
575
|
+
function removalsFirst(records) {
|
|
576
|
+
return [...records.filter((record) => record.remove), ...records.filter((record) => !record.remove)];
|
|
577
|
+
}
|
|
578
|
+
function addEntryIds(target, ids) {
|
|
579
|
+
for (const id of ids)
|
|
580
|
+
target.add(id);
|
|
581
|
+
}
|
|
463
582
|
/**
|
|
464
583
|
* Map each source root → its durable `BundleComponent` (`deriveInstallations`,
|
|
465
584
|
* batch-unique bundle ids, source order preserved). The per-dir document drain
|
|
@@ -478,6 +597,134 @@ function buildComponentBySource(sources) {
|
|
|
478
597
|
});
|
|
479
598
|
return map;
|
|
480
599
|
}
|
|
600
|
+
function componentForSource(components, sourcePath) {
|
|
601
|
+
return (components.get(sourcePath) ?? {
|
|
602
|
+
id: sourcePath,
|
|
603
|
+
adapter: "akm",
|
|
604
|
+
root: sourcePath,
|
|
605
|
+
writable: false,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function groupFileContextsByDir(fileContexts) {
|
|
609
|
+
const groups = new Map();
|
|
610
|
+
for (const ctx of fileContexts) {
|
|
611
|
+
const group = groups.get(ctx.parentDirAbs);
|
|
612
|
+
if (group)
|
|
613
|
+
group.push(ctx);
|
|
614
|
+
else
|
|
615
|
+
groups.set(ctx.parentDirAbs, [ctx]);
|
|
616
|
+
}
|
|
617
|
+
return groups;
|
|
618
|
+
}
|
|
619
|
+
function sourceSnapshotRemovals(db, currentStashDir, currentDirs, allIndexedDirsBySource) {
|
|
620
|
+
const indexedDirs = allIndexedDirsBySource?.get(path.resolve(currentStashDir)) ?? getIndexedDirPathsByStashDir(db, currentStashDir);
|
|
621
|
+
return [...indexedDirs]
|
|
622
|
+
.map((dirPath) => path.resolve(dirPath))
|
|
623
|
+
.filter((dirPath) => !currentDirs.has(dirPath))
|
|
624
|
+
.map((dirPath) => ({
|
|
625
|
+
dirPath,
|
|
626
|
+
currentStashDir,
|
|
627
|
+
files: [],
|
|
628
|
+
stash: null,
|
|
629
|
+
skip: false,
|
|
630
|
+
remove: true,
|
|
631
|
+
reason: { kind: "not-in-source-snapshot" },
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs) {
|
|
635
|
+
const componentBySource = buildComponentBySource(allSourceEntries);
|
|
636
|
+
const handoffDirs = new Set();
|
|
637
|
+
const plans = allSourceEntries.map((sourceAdded) => {
|
|
638
|
+
const currentStashDir = sourceAdded.path;
|
|
639
|
+
const component = componentForSource(componentBySource, currentStashDir);
|
|
640
|
+
if (sourceAdded.unresolved) {
|
|
641
|
+
return {
|
|
642
|
+
currentStashDir,
|
|
643
|
+
component,
|
|
644
|
+
adapter: undefined,
|
|
645
|
+
indexVariant: undefined,
|
|
646
|
+
dirGroups: new Map(),
|
|
647
|
+
removals: [],
|
|
648
|
+
walkComplete: false,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const walked = walkStashFlatWithStatus(currentStashDir, {
|
|
652
|
+
includeAllDirectories: component.adapter === "okf",
|
|
653
|
+
});
|
|
654
|
+
const dirGroups = groupFileContextsByDir(walked.files);
|
|
655
|
+
const adapter = adapterForId(component.adapter);
|
|
656
|
+
return {
|
|
657
|
+
currentStashDir,
|
|
658
|
+
component,
|
|
659
|
+
adapter,
|
|
660
|
+
indexVariant: adapter ? `${adapter.id}@${adapter.version}` : undefined,
|
|
661
|
+
dirGroups,
|
|
662
|
+
removals: [],
|
|
663
|
+
walkComplete: walked.complete,
|
|
664
|
+
};
|
|
665
|
+
});
|
|
666
|
+
const removalKeys = new Set();
|
|
667
|
+
const addRemoval = (plan, dirPath, stashDir) => {
|
|
668
|
+
const resolvedDir = path.resolve(dirPath);
|
|
669
|
+
const key = `${resolvedDir}\0${path.resolve(stashDir)}`;
|
|
670
|
+
if (removalKeys.has(key))
|
|
671
|
+
return;
|
|
672
|
+
removalKeys.add(key);
|
|
673
|
+
plan.removals.push({
|
|
674
|
+
dirPath,
|
|
675
|
+
currentStashDir: stashDir,
|
|
676
|
+
files: [],
|
|
677
|
+
stash: null,
|
|
678
|
+
skip: false,
|
|
679
|
+
remove: true,
|
|
680
|
+
reason: { kind: "not-in-source-snapshot" },
|
|
681
|
+
});
|
|
682
|
+
handoffDirs.add(resolvedDir);
|
|
683
|
+
};
|
|
684
|
+
const allComplete = plans.every((plan) => plan.walkComplete && plan.adapter !== undefined);
|
|
685
|
+
// A full, globally-complete run uses the atomic table wipe below. Every
|
|
686
|
+
// other run reconciles only sources that produced trustworthy snapshots.
|
|
687
|
+
if (reconcileMissingDirs && (isIncremental || !allComplete)) {
|
|
688
|
+
const allIndexedDirsBySource = !isIncremental ? new Map() : undefined;
|
|
689
|
+
if (allIndexedDirsBySource) {
|
|
690
|
+
for (const entry of getAllEntries(db)) {
|
|
691
|
+
const sourceRoot = path.resolve(entry.stashDir);
|
|
692
|
+
const dirs = allIndexedDirsBySource.get(sourceRoot) ?? new Set();
|
|
693
|
+
dirs.add(path.resolve(entry.dirPath));
|
|
694
|
+
allIndexedDirsBySource.set(sourceRoot, dirs);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
for (const plan of plans) {
|
|
698
|
+
if (!plan.walkComplete || !plan.adapter)
|
|
699
|
+
continue;
|
|
700
|
+
const currentDirs = new Set([...plan.dirGroups.keys()].map((dirPath) => path.resolve(dirPath)));
|
|
701
|
+
for (const removal of sourceSnapshotRemovals(db, plan.currentStashDir, currentDirs, allIndexedDirsBySource)) {
|
|
702
|
+
addRemoval(plan, removal.dirPath, removal.currentStashDir);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Cross-source ownership handoffs can delete another source's rows, so they
|
|
707
|
+
// still require every possible owner to have completed its scan.
|
|
708
|
+
if (!allComplete)
|
|
709
|
+
return { plans, handoffDirs };
|
|
710
|
+
// The first configured source that exposes a physical directory owns it.
|
|
711
|
+
// Remove rows left by a prior owner even when both adapters are identical.
|
|
712
|
+
const claimedDirs = new Set();
|
|
713
|
+
for (const plan of plans) {
|
|
714
|
+
for (const dirPath of plan.dirGroups.keys()) {
|
|
715
|
+
const resolvedDir = path.resolve(dirPath);
|
|
716
|
+
if (claimedDirs.has(resolvedDir))
|
|
717
|
+
continue;
|
|
718
|
+
claimedDirs.add(resolvedDir);
|
|
719
|
+
for (const priorOwner of getIndexedStashDirsByDir(db, dirPath)) {
|
|
720
|
+
if (path.resolve(priorOwner) !== path.resolve(plan.currentStashDir)) {
|
|
721
|
+
addRemoval(plan, dirPath, priorOwner);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { plans, handoffDirs };
|
|
727
|
+
}
|
|
481
728
|
/**
|
|
482
729
|
* Phase 1 (async): walk every source directory and pre-generate all metadata
|
|
483
730
|
* outside any transaction, producing the per-directory scan records that
|
|
@@ -488,24 +735,20 @@ function buildComponentBySource(sources) {
|
|
|
488
735
|
* outside `db.transaction()` so the persist pass can be a single synchronous
|
|
489
736
|
* transaction.
|
|
490
737
|
*/
|
|
491
|
-
|
|
738
|
+
function reportSourceScanProgress(onProgress, processed, total, message) {
|
|
739
|
+
onProgress?.({ phase: "scan", message, processed, total });
|
|
740
|
+
}
|
|
741
|
+
async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs = true) {
|
|
492
742
|
let scannedDirs = 0;
|
|
493
743
|
let skippedDirs = 0;
|
|
494
744
|
let generatedCount = 0;
|
|
495
745
|
const warnings = [];
|
|
496
746
|
const seenPaths = new Set();
|
|
497
|
-
const
|
|
747
|
+
const { plans, handoffDirs } = buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs);
|
|
498
748
|
const dirRecords = [];
|
|
499
749
|
let processedDirs = 0;
|
|
500
750
|
let priorDirsChanged = hadRemovedSources;
|
|
501
|
-
const reportScanProgress = (message) =>
|
|
502
|
-
onProgress?.({
|
|
503
|
-
phase: "scan",
|
|
504
|
-
message,
|
|
505
|
-
processed: processedDirs,
|
|
506
|
-
total: allSourceEntries.length,
|
|
507
|
-
});
|
|
508
|
-
};
|
|
751
|
+
const reportScanProgress = (message) => reportSourceScanProgress(onProgress, processedDirs, allSourceEntries.length, message);
|
|
509
752
|
const reportDirDecision = (kind, dirPath, currentStashDir, reason, persistedRowCount) => {
|
|
510
753
|
if (!isVerbose())
|
|
511
754
|
return;
|
|
@@ -514,10 +757,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
514
757
|
reportScanProgress(`${kind === "scan" ? "Rescanning" : "Skipping"} ${path.relative(currentStashDir, dirPath) || "."} ` +
|
|
515
758
|
`from ${currentStashDir}: ${reason.kind}${detail}${rowInfo}`);
|
|
516
759
|
};
|
|
517
|
-
//
|
|
518
|
-
// dir may surface via multiple stash roots; only the first occurrence is
|
|
519
|
-
// indexed, and the later ones are recorded as skips. Returns true when the
|
|
520
|
-
// dir was already seen (caller should skip further processing).
|
|
760
|
+
// Only the first source that exposes a physical directory may index it.
|
|
521
761
|
const markSeenOrSkipDuplicate = (dirPath, currentStashDir, files) => {
|
|
522
762
|
const resolved = path.resolve(dirPath);
|
|
523
763
|
if (seenPaths.has(resolved)) {
|
|
@@ -532,9 +772,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
532
772
|
// Incremental freshness gate shared by both branches: consult the persisted
|
|
533
773
|
// dir state and record either a skip (unchanged + eligible for incremental
|
|
534
774
|
// skip) or a scan record carrying the candidate stash.
|
|
535
|
-
const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash, hashByFile, conceptIdByFile) => {
|
|
536
|
-
const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs);
|
|
537
|
-
if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
775
|
+
const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash, hashByFile, conceptIdByFile, indexVariant, forceScan, pruneMissing) => {
|
|
776
|
+
const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs, indexVariant);
|
|
777
|
+
if (isIncremental && !forceScan && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
538
778
|
skippedDirs++;
|
|
539
779
|
dirRecords.push({
|
|
540
780
|
dirPath,
|
|
@@ -543,6 +783,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
543
783
|
stash: null,
|
|
544
784
|
skip: true,
|
|
545
785
|
reason: previousState.reason,
|
|
786
|
+
indexVariant,
|
|
546
787
|
});
|
|
547
788
|
reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
|
|
548
789
|
return;
|
|
@@ -560,46 +801,44 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
560
801
|
persistedRowCount: previousState.persistedRowCount,
|
|
561
802
|
hashByFile,
|
|
562
803
|
conceptIdByFile,
|
|
804
|
+
indexVariant,
|
|
805
|
+
pruneMissing,
|
|
563
806
|
});
|
|
564
807
|
reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
|
|
565
808
|
};
|
|
566
|
-
for (const
|
|
567
|
-
const currentStashDir =
|
|
568
|
-
const fileContexts = walkStashFlat(currentStashDir);
|
|
809
|
+
for (const plan of plans) {
|
|
810
|
+
const { currentStashDir, component, adapter, dirGroups, removals, walkComplete } = plan;
|
|
569
811
|
processedDirs++;
|
|
570
812
|
reportScanProgress(`Processed ${processedDirs}/${allSourceEntries.length} source${allSourceEntries.length === 1 ? "" : "s"}.`);
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const dir = ctx.parentDirAbs;
|
|
577
|
-
const group = dirGroups.get(dir);
|
|
578
|
-
if (group)
|
|
579
|
-
group.push(ctx);
|
|
580
|
-
else
|
|
581
|
-
dirGroups.set(dir, [ctx]);
|
|
813
|
+
if (!walkComplete) {
|
|
814
|
+
for (const dirPath of dirGroups.keys())
|
|
815
|
+
seenPaths.add(path.resolve(dirPath));
|
|
816
|
+
warn(`[index] source "${component.id}" was not scanned completely; preserving its last-known-good rows.`);
|
|
817
|
+
continue;
|
|
582
818
|
}
|
|
583
|
-
const component = componentBySource.get(currentStashDir) ?? {
|
|
584
|
-
id: currentStashDir,
|
|
585
|
-
adapter: "akm",
|
|
586
|
-
root: currentStashDir,
|
|
587
|
-
writable: false,
|
|
588
|
-
};
|
|
589
819
|
// Owner ruling 2026-07-21: dispatch each component's DETECTED adapter (§4).
|
|
590
820
|
// An unknown adapter id has no `adapterForId` match → skip the whole
|
|
591
821
|
// component with a warning (one bundle = one component = one adapter).
|
|
592
|
-
const adapter = adapterForId(component.adapter);
|
|
593
822
|
if (!adapter) {
|
|
823
|
+
for (const dirPath of dirGroups.keys())
|
|
824
|
+
seenPaths.add(path.resolve(dirPath));
|
|
594
825
|
warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
|
|
595
826
|
continue;
|
|
596
827
|
}
|
|
828
|
+
const indexVariant = plan.indexVariant ?? `${adapter.id}@${adapter.version}`;
|
|
829
|
+
for (const removal of removals) {
|
|
830
|
+
dirRecords.push(removal);
|
|
831
|
+
scannedDirs++;
|
|
832
|
+
priorDirsChanged = true;
|
|
833
|
+
reportDirDecision("scan", removal.dirPath, currentStashDir, removal.reason);
|
|
834
|
+
}
|
|
597
835
|
for (const [dirPath, ctxs] of dirGroups) {
|
|
598
836
|
// Adapter-owned filtering (owner ruling 2026-07-21): the drain no longer
|
|
599
837
|
// pre-filters with AKM-stash policy — each adapter's `recognize` claims or
|
|
600
838
|
// abstains on its own bundle's walked files. The core walk keeps only the
|
|
601
839
|
// universal hygiene `walkStashFlat` already applies (.git/dot-dirs/etc.).
|
|
602
840
|
const indexableFiles = ctxs.map((ctx) => ctx.absPath);
|
|
841
|
+
const forceScan = handoffDirs.has(path.resolve(dirPath));
|
|
603
842
|
if (markSeenOrSkipDuplicate(dirPath, currentStashDir, indexableFiles))
|
|
604
843
|
continue;
|
|
605
844
|
if (indexableFiles.length === 0) {
|
|
@@ -609,7 +848,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
609
848
|
reportDirDecision("skip", dirPath, currentStashDir, reason);
|
|
610
849
|
continue;
|
|
611
850
|
}
|
|
612
|
-
const cachedZeroRowState = isIncremental &&
|
|
851
|
+
const cachedZeroRowState = isIncremental &&
|
|
852
|
+
!forceScan &&
|
|
853
|
+
getCachedZeroRowDirState(db, dirPath, indexableFiles, builtAtMs, priorDirsChanged, indexVariant);
|
|
613
854
|
if (cachedZeroRowState) {
|
|
614
855
|
skippedDirs++;
|
|
615
856
|
dirRecords.push({
|
|
@@ -619,6 +860,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
619
860
|
stash: null,
|
|
620
861
|
skip: true,
|
|
621
862
|
reason: cachedZeroRowState.reason,
|
|
863
|
+
indexVariant,
|
|
622
864
|
});
|
|
623
865
|
reportDirDecision("skip", dirPath, currentStashDir, cachedZeroRowState.reason, cachedZeroRowState.persistedRowCount);
|
|
624
866
|
continue;
|
|
@@ -639,10 +881,27 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
639
881
|
if (generated.entries.length > 0) {
|
|
640
882
|
generatedCount += generated.entries.length;
|
|
641
883
|
}
|
|
642
|
-
recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash, drained.hashByFile, drained.conceptIdByFile);
|
|
884
|
+
recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash, drained.hashByFile, drained.conceptIdByFile, indexVariant, forceScan, walkComplete);
|
|
643
885
|
}
|
|
644
886
|
}
|
|
645
|
-
return {
|
|
887
|
+
return {
|
|
888
|
+
dirRecords: removalsFirst(dirRecords),
|
|
889
|
+
scannedDirs,
|
|
890
|
+
skippedDirs,
|
|
891
|
+
generatedCount,
|
|
892
|
+
warnings,
|
|
893
|
+
complete: plans.every((plan) => plan.walkComplete && plan.adapter !== undefined),
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function preserveExistingIndex(doFullDelete, dirRecords, sourceRoots) {
|
|
897
|
+
if (!doFullDelete)
|
|
898
|
+
return false;
|
|
899
|
+
const incomingDocCount = dirRecords.reduce((n, record) => n + (record.skip ? 0 : (record.stash?.entries.length ?? 0)), 0);
|
|
900
|
+
if (incomingDocCount > 0 || allSourceRootsReadable(sourceRoots))
|
|
901
|
+
return false;
|
|
902
|
+
warn("[index] --full produced zero documents while one or more source roots are missing or unreadable — " +
|
|
903
|
+
"preserving the existing index (last-known-good) rather than wiping it. Re-run once the sources are available.");
|
|
904
|
+
return true;
|
|
646
905
|
}
|
|
647
906
|
/**
|
|
648
907
|
* #624-P1 zero-document preflight probe. A source root counts as "readable"
|
|
@@ -671,8 +930,9 @@ function allSourceRootsReadable(roots) {
|
|
|
671
930
|
* Phase 2 (sync): write all pre-generated scan records inside a single
|
|
672
931
|
* transaction, returning the directories that still need LLM enrichment.
|
|
673
932
|
*/
|
|
674
|
-
function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, bundleByRoot) {
|
|
933
|
+
function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, scanComplete, bundleByRoot) {
|
|
675
934
|
const dirsNeedingLlm = [];
|
|
935
|
+
const fullDelete = doFullDelete && scanComplete;
|
|
676
936
|
// #624-P1 zero-document preflight (spec §4). A full-rebuild wipe is a
|
|
677
937
|
// legitimate mass-delete ONLY when the scan legitimately found nothing. If
|
|
678
938
|
// the walk produced zero documents AND any configured source root is missing
|
|
@@ -681,25 +941,19 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
681
941
|
// last-known-good index (entries + embeddings + utility/usage). Preserve it
|
|
682
942
|
// and warn instead; the next successful run reconciles. A genuinely empty
|
|
683
943
|
// stash whose roots ARE readable still wipes, as before.
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
// Cross-stash dedup: track indexed assets by type + entry.name so a
|
|
693
|
-
// lower-priority stash row with the same logical index identity is skipped
|
|
694
|
-
// when a higher-priority root already covers it.
|
|
695
|
-
// Sources are ordered by priority (primary stash first), so the first
|
|
696
|
-
// occurrence wins.
|
|
944
|
+
if (preserveExistingIndex(fullDelete, dirRecords, sourceRoots))
|
|
945
|
+
return { dirsNeedingLlm };
|
|
946
|
+
// Per-source dedup: the same logical asset can appear more than once within
|
|
947
|
+
// one owning source, where source order still makes the first occurrence win.
|
|
948
|
+
// The owner is part of the key so identical concepts in different bundles
|
|
949
|
+
// remain distinct indexed rows.
|
|
697
950
|
const indexedAssetIdentities = new Set();
|
|
951
|
+
const deletedUsageEntryIds = new Set();
|
|
698
952
|
const insertTransaction = db.transaction(() => {
|
|
699
953
|
// Perform the full-rebuild wipe as the FIRST step of the insert
|
|
700
954
|
// transaction so delete and re-insert are atomic — a concurrent reader
|
|
701
955
|
// never observes an empty database between the two operations.
|
|
702
|
-
if (
|
|
956
|
+
if (fullDelete) {
|
|
703
957
|
try {
|
|
704
958
|
db.exec("DELETE FROM embeddings");
|
|
705
959
|
}
|
|
@@ -723,10 +977,16 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
723
977
|
// re-resolves the rest by entry_ref — subsuming the old detach.
|
|
724
978
|
db.exec("DELETE FROM entries");
|
|
725
979
|
}
|
|
726
|
-
for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile } of dirRecords) {
|
|
980
|
+
for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile, indexVariant, remove, pruneMissing, } of dirRecords) {
|
|
981
|
+
if (remove) {
|
|
982
|
+
const removedIds = deleteEntriesByDirAndStash(db, dirPath, currentStashDir, { cleanupUsageEvents: false });
|
|
983
|
+
addEntryIds(deletedUsageEntryIds, removedIds);
|
|
984
|
+
deleteIndexDirState(db, dirPath);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
727
987
|
if (skip) {
|
|
728
988
|
if (reason?.kind === "unchanged") {
|
|
729
|
-
const fingerprint = computeDirFingerprint(dirPath, files);
|
|
989
|
+
const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
|
|
730
990
|
upsertIndexDirState(db, {
|
|
731
991
|
dirPath,
|
|
732
992
|
fileSetHash: fingerprint.fileSetHash,
|
|
@@ -745,36 +1005,39 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
745
1005
|
let persistedRows = 0;
|
|
746
1006
|
let dedupedRows = 0;
|
|
747
1007
|
if (stash) {
|
|
1008
|
+
const bundle = bundleByRoot.get(path.resolve(currentStashDir));
|
|
1009
|
+
if (!bundle)
|
|
1010
|
+
throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
|
|
1011
|
+
const ownerIdentity = bundle.bundleId;
|
|
748
1012
|
for (const entry of stash.entries) {
|
|
749
1013
|
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : null;
|
|
750
1014
|
if (!entryPath) {
|
|
751
1015
|
warn(`Skipping entry with no resolvable path in ${dirPath}`);
|
|
752
1016
|
continue;
|
|
753
1017
|
}
|
|
754
|
-
|
|
755
|
-
|
|
1018
|
+
const adapterConceptId = conceptIdByFile?.get(entryPath);
|
|
1019
|
+
if (!adapterConceptId) {
|
|
1020
|
+
warn(`Skipping entry without adapter-owned concept identity: ${entryPath}`);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
// Adapter-owned concept identity is path-based and cannot be replaced
|
|
1024
|
+
// by presentation fields such as type/title.
|
|
1025
|
+
const identityKey = `${ownerIdentity}\0${adapterConceptId}`;
|
|
756
1026
|
if (indexedAssetIdentities.has(identityKey)) {
|
|
757
1027
|
dedupedRows++;
|
|
758
1028
|
continue;
|
|
759
1029
|
}
|
|
760
1030
|
indexedAssetIdentities.add(identityKey);
|
|
761
|
-
const entryKey =
|
|
1031
|
+
const entryKey = bundle?.adapterId !== "akm" && adapterConceptId
|
|
1032
|
+
? `${currentStashDir}:concept:${adapterConceptId}`
|
|
1033
|
+
: `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
762
1034
|
keptEntryKeys.add(entryKey);
|
|
763
1035
|
const searchText = buildSearchText(entry);
|
|
764
1036
|
const entryWithSize = attachFileSize(entry, entryPath);
|
|
765
1037
|
// content_hash = doc.hash from the drain, keyed by the recognized
|
|
766
|
-
// file's path
|
|
767
|
-
// any existing hash (upsert COALESCE) for sidecar-only entries.
|
|
1038
|
+
// file's path. A missing hash preserves the existing value on upsert.
|
|
768
1039
|
const contentHash = hashByFile?.get(entryPath);
|
|
769
|
-
|
|
770
|
-
// (the D-R2 qualified conceptId + `<bundle>//<conceptId>` item_ref) via
|
|
771
|
-
// the shared helper that the write-path fast path also uses. NULL
|
|
772
|
-
// provenance when the source root has no bundle mapping (e.g. write-back
|
|
773
|
-
// paths) — healed on next index.
|
|
774
|
-
const bundle = bundleByRoot?.get(path.resolve(currentStashDir));
|
|
775
|
-
const provenance = bundle
|
|
776
|
-
? deriveEntryProvenance(bundle, entry.type, entry.name, conceptIdByFile?.get(entryPath))
|
|
777
|
-
: undefined;
|
|
1040
|
+
const provenance = deriveEntryProvenance(bundle, entry.type, entry.name, adapterConceptId);
|
|
778
1041
|
const entryId = upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entryWithSize, searchText, provenance, contentHash);
|
|
779
1042
|
persistedRows++;
|
|
780
1043
|
if (entry.type === "workflow") {
|
|
@@ -785,8 +1048,7 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
785
1048
|
}
|
|
786
1049
|
}
|
|
787
1050
|
// Collect dirs needing LLM enhancement during the first walk.
|
|
788
|
-
// Only dirs with "generated" entries need enrichment
|
|
789
|
-
// forces re-processing of already-enriched entries).
|
|
1051
|
+
// Only dirs with "generated" entries need enrichment.
|
|
790
1052
|
if (stash.entries.some((e) => e.quality === "generated")) {
|
|
791
1053
|
dirsNeedingLlm.push({ dirPath, files, currentStashDir, stash });
|
|
792
1054
|
}
|
|
@@ -795,8 +1057,10 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
795
1057
|
// (files deleted, deduped away, or abstained on by the adapter). With
|
|
796
1058
|
// an empty kept-set this deletes every row for the dir — the exact net
|
|
797
1059
|
// effect of the old unconditional `deleteEntriesByDir`, minus the id churn.
|
|
798
|
-
|
|
799
|
-
|
|
1060
|
+
if (pruneMissing !== false) {
|
|
1061
|
+
addEntryIds(deletedUsageEntryIds, deleteEntriesByDirExceptKeys(db, dirPath, currentStashDir, keptEntryKeys, { cleanupUsageEvents: false }));
|
|
1062
|
+
}
|
|
1063
|
+
const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
|
|
800
1064
|
const persistedReason = persistedRows === 0
|
|
801
1065
|
? inferZeroRowReason(stash, reason, warnings, dirPath, dedupedRows)
|
|
802
1066
|
: reason?.kind === "full-rebuild"
|
|
@@ -827,12 +1091,13 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
827
1091
|
}
|
|
828
1092
|
});
|
|
829
1093
|
insertTransaction();
|
|
1094
|
+
deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
|
|
830
1095
|
return { dirsNeedingLlm };
|
|
831
1096
|
}
|
|
832
|
-
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
|
|
1097
|
+
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress, reconcileMissingDirs = true) {
|
|
833
1098
|
// Phase 1 (async): walk directories and pre-generate all metadata outside the
|
|
834
1099
|
// transaction.
|
|
835
|
-
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress);
|
|
1100
|
+
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs);
|
|
836
1101
|
// Phase 2 (sync): write all pre-generated metadata inside a single transaction.
|
|
837
1102
|
// Source roots feed the #624-P1 zero-document preflight (a full-rebuild wipe
|
|
838
1103
|
// is suppressed when the scan is empty because roots are unreadable).
|
|
@@ -855,10 +1120,26 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
|
|
|
855
1120
|
adapterId: component?.adapter ?? "akm",
|
|
856
1121
|
});
|
|
857
1122
|
});
|
|
858
|
-
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, bundleByRoot);
|
|
859
|
-
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
|
|
1123
|
+
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, complete, bundleByRoot);
|
|
1124
|
+
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm, complete };
|
|
860
1125
|
}
|
|
861
|
-
|
|
1126
|
+
function indexedProvenanceForFile(db, filePath) {
|
|
1127
|
+
const row = db
|
|
1128
|
+
.prepare("SELECT item_ref AS itemRef, bundle_id AS bundleId, component_id AS componentId, " +
|
|
1129
|
+
"concept_id AS conceptId, adapter_id AS adapterId FROM entries WHERE file_path = ? LIMIT 1")
|
|
1130
|
+
.get(filePath);
|
|
1131
|
+
if (!row?.itemRef || !row.bundleId || !row.componentId || !row.conceptId || !row.adapterId) {
|
|
1132
|
+
throw new Error(`Missing indexed provenance for ${filePath}`);
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
itemRef: row.itemRef,
|
|
1136
|
+
bundleId: row.bundleId,
|
|
1137
|
+
componentId: row.componentId,
|
|
1138
|
+
conceptId: row.conceptId,
|
|
1139
|
+
adapterId: row.adapterId,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal) {
|
|
862
1143
|
// Resolve per-pass LLM config via the unified shim. Returns undefined when
|
|
863
1144
|
// either no `akm.llm` is configured or the user opted this pass out via
|
|
864
1145
|
// `index.enrichment.llm = false`. (#208)
|
|
@@ -874,17 +1155,18 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
874
1155
|
const totalDirs = dirsNeedingLlm.length;
|
|
875
1156
|
const totalEntries = dirsNeedingLlm.reduce((sum, { stash }) => {
|
|
876
1157
|
const entriesToEnhance = stash.entries.filter((e) => {
|
|
877
|
-
if (e.quality !== "generated"
|
|
1158
|
+
if (e.quality !== "generated")
|
|
878
1159
|
return false;
|
|
879
|
-
if (
|
|
1160
|
+
if (isEnrichmentComplete(e))
|
|
880
1161
|
return false;
|
|
881
1162
|
return true;
|
|
882
1163
|
});
|
|
883
1164
|
return sum + entriesToEnhance.length;
|
|
884
1165
|
}, 0);
|
|
885
|
-
// P3 — wall-clock budget for the enrichment pass. Defaults to
|
|
886
|
-
// (or 10 minutes if not set). Users can extend
|
|
887
|
-
//
|
|
1166
|
+
// P3 — wall-clock budget for the enrichment pass. Defaults to the resolved
|
|
1167
|
+
// engine's timeoutMs (or 10 minutes if not set). Users can extend it via
|
|
1168
|
+
// `index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`, or the
|
|
1169
|
+
// engine's own `engines.<name>.timeoutMs`) — no separate knob needed.
|
|
888
1170
|
const enrichDeadline = createEnrichmentDeadline(llmConfig.timeoutMs, totalEntries);
|
|
889
1171
|
let deadlineHit = false;
|
|
890
1172
|
const enrichSignal = (() => {
|
|
@@ -933,14 +1215,13 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
933
1215
|
await concurrentMap(dirsNeedingLlm, async ({ dirPath, files, currentStashDir, stash: originalStash }) => {
|
|
934
1216
|
if (enrichSignal.aborted)
|
|
935
1217
|
return undefined;
|
|
936
|
-
// Only enhance generated entries
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
// unless the caller explicitly requests re-enrichment via reEnrich=true.
|
|
1218
|
+
// Only enhance generated entries; user-provided overrides should not
|
|
1219
|
+
// be overwritten. Skip entries that are already fully enriched
|
|
1220
|
+
// (description + tags + searchHints).
|
|
940
1221
|
const entriesToEnhance = originalStash.entries.filter((e) => {
|
|
941
|
-
if (e.quality !== "generated"
|
|
1222
|
+
if (e.quality !== "generated")
|
|
942
1223
|
return false;
|
|
943
|
-
if (
|
|
1224
|
+
if (isEnrichmentComplete(e)) {
|
|
944
1225
|
warnVerbose(`[akm] skipping LLM enrichment for "${e.name}" — entry already complete`);
|
|
945
1226
|
return false;
|
|
946
1227
|
}
|
|
@@ -959,7 +1240,7 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
959
1240
|
lastProgressAt = Date.now();
|
|
960
1241
|
const targetStash = { entries: entriesToEnhance };
|
|
961
1242
|
const entryKeys = entriesToEnhance.map((e) => `${currentStashDir}:${e.type}:${e.name}`);
|
|
962
|
-
const enhanced = await enhanceStashWithLlm(llmConfig, targetStash, files, summary, enrichSignal, db, entryKeys,
|
|
1243
|
+
const enhanced = await enhanceStashWithLlm(llmConfig, targetStash, files, summary, enrichSignal, db, entryKeys, config, (event) => {
|
|
963
1244
|
completedEntries++;
|
|
964
1245
|
lastProgressAt = Date.now();
|
|
965
1246
|
onProgress?.({
|
|
@@ -980,7 +1261,8 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
980
1261
|
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
|
|
981
1262
|
const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`;
|
|
982
1263
|
const searchText = buildSearchText(entry);
|
|
983
|
-
|
|
1264
|
+
const provenance = indexedProvenanceForFile(db, entryPath);
|
|
1265
|
+
upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, attachFileSize(entry, entryPath), searchText, provenance);
|
|
984
1266
|
}
|
|
985
1267
|
})();
|
|
986
1268
|
completedDirs++;
|
|
@@ -996,7 +1278,8 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
996
1278
|
},
|
|
997
1279
|
// Defaults: 2 for remote LLM APIs, 1 for local model servers (LM
|
|
998
1280
|
// Studio, Ollama run one inference at a time — parallel requests cause
|
|
999
|
-
// "Model reloaded" / 500 errors).
|
|
1281
|
+
// "Model reloaded" / 500 errors). No config override reaches this path:
|
|
1282
|
+
// `resolveLlmEngineUse` does not forward `engines.<name>.concurrency`.
|
|
1000
1283
|
getDefaultLlmConcurrency(llmConfig));
|
|
1001
1284
|
}
|
|
1002
1285
|
finally {
|
|
@@ -1004,7 +1287,7 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
|
|
|
1004
1287
|
clearInterval(heartbeatTimer);
|
|
1005
1288
|
}
|
|
1006
1289
|
if (deadlineHit) {
|
|
1007
|
-
warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase
|
|
1290
|
+
warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase index.enrichment.timeoutMs for a larger budget.");
|
|
1008
1291
|
}
|
|
1009
1292
|
// Gate-closed (`skipped`) entries are not failures — exclude them so a
|
|
1010
1293
|
// deliberately disabled feature never surfaces as an enrichment error.
|
|
@@ -1186,12 +1469,23 @@ function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
|
|
|
1186
1469
|
};
|
|
1187
1470
|
}
|
|
1188
1471
|
if (embeddingCount >= embeddableEntries) {
|
|
1472
|
+
// "ready-vec" must reflect the path search will ACTUALLY take: the vec
|
|
1473
|
+
// extension being loaded is not enough when the embedding phase recorded
|
|
1474
|
+
// fast-path insert failures (searchVec then routes to the JS-cosine
|
|
1475
|
+
// fallback via isVecFastPathReady). Reporting vec health from
|
|
1476
|
+
// isVecAvailable alone overstated `akm info` after partial vec failures
|
|
1477
|
+
// (§24.2 "Semantic" gate — truthful ready-vec).
|
|
1478
|
+
const vecActive = vecAvailable && isVecFastPathReady(db);
|
|
1189
1479
|
return {
|
|
1190
1480
|
ok: true,
|
|
1191
|
-
message: `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings, ${
|
|
1481
|
+
message: `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings, ${vecActive
|
|
1482
|
+
? "sqlite-vec active"
|
|
1483
|
+
: vecAvailable
|
|
1484
|
+
? "JS fallback active — vec fast path degraded, run 'akm index --full' to restore"
|
|
1485
|
+
: "JS fallback active"}).`,
|
|
1192
1486
|
semanticSearchEnabled: true,
|
|
1193
1487
|
semanticSearchMode: config.semanticSearchMode,
|
|
1194
|
-
semanticStatus:
|
|
1488
|
+
semanticStatus: vecActive ? "ready-vec" : "ready-js",
|
|
1195
1489
|
embeddingProvider,
|
|
1196
1490
|
entryCount: embeddableEntries,
|
|
1197
1491
|
embeddingCount,
|
|
@@ -1220,18 +1514,14 @@ function buildIndexedDirCandidate(dirPath, indexableFiles, generated) {
|
|
|
1220
1514
|
return { stash, staleFiles };
|
|
1221
1515
|
}
|
|
1222
1516
|
function resolveIndexedFiles(dirPath, files, stash) {
|
|
1223
|
-
const fileBasenameMap = buildFileBasenameMap(files);
|
|
1224
1517
|
const resolved = new Set();
|
|
1225
1518
|
for (const entry of stash.entries) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
: matchEntryToFile(entry.name, fileBasenameMap);
|
|
1229
|
-
if (entryPath)
|
|
1230
|
-
resolved.add(entryPath);
|
|
1519
|
+
if (entry.filename)
|
|
1520
|
+
resolved.add(path.join(dirPath, entry.filename));
|
|
1231
1521
|
}
|
|
1232
1522
|
return resolved.size > 0 ? [...resolved] : files;
|
|
1233
1523
|
}
|
|
1234
|
-
async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db, entryKeys,
|
|
1524
|
+
async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db, entryKeys, akmConfig, onEntryDone) {
|
|
1235
1525
|
const { enhanceMetadata } = await import("../llm/metadata-enhance.js");
|
|
1236
1526
|
const { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } = await import("../storage/repositories/index-llm-cache-repository.js");
|
|
1237
1527
|
const results = await concurrentMap(stash.entries, async (entry, idx) => {
|
|
@@ -1251,13 +1541,13 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1251
1541
|
warn(`Could not read file for LLM enrichment: ${entry.filename ?? entry.name}`);
|
|
1252
1542
|
}
|
|
1253
1543
|
}
|
|
1254
|
-
// Incremental cache: skip LLM call when file body is unchanged
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1544
|
+
// Incremental cache: skip LLM call when file body is unchanged. The
|
|
1545
|
+
// cache key is the entry_key (stashDir:type:name) which is stable
|
|
1546
|
+
// across index runs.
|
|
1257
1547
|
const cacheBody = fileContent ?? `${entry.name}\n${entry.description ?? ""}`;
|
|
1258
1548
|
const bodyHash = computeBodyHash(cacheBody);
|
|
1259
1549
|
const cacheKey = entryKeys?.[idx] ?? `${entry.type}:${entry.name}`;
|
|
1260
|
-
if (db
|
|
1550
|
+
if (db) {
|
|
1261
1551
|
const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
|
|
1262
1552
|
if (cached) {
|
|
1263
1553
|
try {
|
|
@@ -1337,8 +1627,8 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1337
1627
|
return entry;
|
|
1338
1628
|
}
|
|
1339
1629
|
},
|
|
1340
|
-
// Defaults: 2 for remote LLM APIs, 1 for local model servers.
|
|
1341
|
-
//
|
|
1630
|
+
// Defaults: 2 for remote LLM APIs, 1 for local model servers. No config
|
|
1631
|
+
// override reaches this path (see getDefaultLlmConcurrency).
|
|
1342
1632
|
getDefaultLlmConcurrency(llmConfig));
|
|
1343
1633
|
// concurrentMap returns Array<T | undefined>; filter out undefined slots
|
|
1344
1634
|
// (which can only occur if the callback itself returned undefined, which
|
|
@@ -1346,41 +1636,68 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
|
|
|
1346
1636
|
const enhanced = results.map((r, i) => r ?? stash.entries[i]);
|
|
1347
1637
|
return { entries: enhanced };
|
|
1348
1638
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
// Only keep first match per base name to avoid ambiguity
|
|
1357
|
-
if (!map.has(base))
|
|
1358
|
-
map.set(base, file);
|
|
1359
|
-
}
|
|
1360
|
-
return map;
|
|
1639
|
+
// ── lookup ─────────────────────────────────────────────────────────────────
|
|
1640
|
+
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
1641
|
+
import { conceptIdFromTypeName } from "../core/asset/resolve-ref.js";
|
|
1642
|
+
async function resolveLookupSources() {
|
|
1643
|
+
const { loadConfig } = await import("../core/config/config.js");
|
|
1644
|
+
const { resolveSourceEntries } = await import("./search/search-source.js");
|
|
1645
|
+
return resolveSourceEntries(undefined, loadConfig());
|
|
1361
1646
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1647
|
+
function resolveLookupScope(bundle, sources) {
|
|
1648
|
+
if (!bundle)
|
|
1649
|
+
return { candidateDirs: sources.map((source) => source.path), qualified: false };
|
|
1650
|
+
return { candidateDirs: resolveSourcesForOrigin(bundle, sources).map((source) => source.path), qualified: true };
|
|
1651
|
+
}
|
|
1652
|
+
/** Resolve an adapter-owned `[bundle//]conceptId` without interpreting its path as an AKM type. */
|
|
1653
|
+
export async function lookupBundleRef(ref) {
|
|
1654
|
+
const sources = await resolveLookupSources();
|
|
1655
|
+
if (sources.length === 0)
|
|
1656
|
+
return null;
|
|
1657
|
+
const { candidateDirs, qualified } = resolveLookupScope(ref.bundle, sources);
|
|
1658
|
+
if (candidateDirs.length === 0)
|
|
1659
|
+
return null;
|
|
1660
|
+
const db = openExistingDatabase(getDbPath());
|
|
1661
|
+
try {
|
|
1662
|
+
const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, ref.conceptId);
|
|
1663
|
+
for (const dir of candidateDirs) {
|
|
1664
|
+
const id = findEntryIdByRef(db, inputRef, dir);
|
|
1665
|
+
if (id === undefined)
|
|
1666
|
+
continue;
|
|
1667
|
+
const row = db
|
|
1668
|
+
.prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type, " +
|
|
1669
|
+
"entry_json AS entryJson, item_ref AS itemRef, bundle_id AS bundleId, concept_id AS conceptId, " +
|
|
1670
|
+
"adapter_id AS adapterId FROM entries WHERE id = ?")
|
|
1671
|
+
.get(id);
|
|
1672
|
+
if (!row)
|
|
1673
|
+
continue;
|
|
1674
|
+
let document;
|
|
1675
|
+
try {
|
|
1676
|
+
document = JSON.parse(row.entryJson);
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
// Corrupt optional projection does not erase the durable path identity.
|
|
1680
|
+
}
|
|
1681
|
+
if (!row.itemRef || !row.bundleId || !row.conceptId || !row.adapterId)
|
|
1682
|
+
continue;
|
|
1683
|
+
return {
|
|
1684
|
+
entryKey: row.entryKey,
|
|
1685
|
+
filePath: row.filePath,
|
|
1686
|
+
stashDir: row.stashDir,
|
|
1687
|
+
type: row.type,
|
|
1688
|
+
name: document?.name ?? ref.conceptId.split("/").pop() ?? ref.conceptId,
|
|
1689
|
+
adapterId: row.adapterId,
|
|
1690
|
+
document,
|
|
1691
|
+
itemRef: row.itemRef,
|
|
1692
|
+
bundleId: row.bundleId,
|
|
1693
|
+
conceptId: row.conceptId,
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
finally {
|
|
1699
|
+
closeDatabase(db);
|
|
1382
1700
|
}
|
|
1383
|
-
return null;
|
|
1384
1701
|
}
|
|
1385
1702
|
/**
|
|
1386
1703
|
* Look up a single asset by ref. Spec §6.2 — `akm show` queries this and
|
|
@@ -1388,70 +1705,11 @@ export function matchEntryToFile(entryName, fileMap) {
|
|
|
1388
1705
|
* file corresponds to which ref; the indexer walks `provider.path()` for
|
|
1389
1706
|
* every configured source, so this query covers all source kinds.
|
|
1390
1707
|
*
|
|
1391
|
-
* Match rules:
|
|
1392
|
-
* - `ref.origin === undefined` → first match across all sources (primary
|
|
1393
|
-
* source first, then in declared order — same priority as the indexer's
|
|
1394
|
-
* write order).
|
|
1395
|
-
* - `ref.origin === "local"` → primary source only (entry_key prefix is
|
|
1396
|
-
* the primary stash dir).
|
|
1397
|
-
* - `ref.origin === <name>` → restrict to the matching source name. We
|
|
1398
|
-
* resolve the source's directory and match on `entry_key` prefix.
|
|
1399
|
-
*
|
|
1400
1708
|
* Returns `null` when no row matches — callers translate that into a
|
|
1401
1709
|
* `NotFoundError` with their own messaging.
|
|
1402
1710
|
*/
|
|
1403
1711
|
export async function lookup(ref) {
|
|
1404
|
-
|
|
1405
|
-
const { resolveSourceEntries } = await import("./search/search-source.js");
|
|
1406
|
-
const config = loadConfig();
|
|
1407
|
-
const sources = resolveSourceEntries(undefined, config);
|
|
1408
|
-
if (sources.length === 0)
|
|
1409
|
-
return null;
|
|
1410
|
-
const dbPath = getDbPath();
|
|
1411
|
-
const db = openExistingDatabase(dbPath);
|
|
1412
|
-
try {
|
|
1413
|
-
const escapeLike = (value) => value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1414
|
-
// Canonical names strip .md for markdown assets, but users often pass
|
|
1415
|
-
// refs with .md (e.g. command:release.md). Normalize by trying both.
|
|
1416
|
-
const nameVariants = [ref.name];
|
|
1417
|
-
if (ref.name.endsWith(".md")) {
|
|
1418
|
-
nameVariants.push(ref.name.slice(0, -3));
|
|
1419
|
-
}
|
|
1420
|
-
const candidateDirs = (() => {
|
|
1421
|
-
if (!ref.origin)
|
|
1422
|
-
return sources.map((s) => s.path);
|
|
1423
|
-
if (ref.origin === "local")
|
|
1424
|
-
return [sources[0].path];
|
|
1425
|
-
const named = sources.find((s) => s.registryId === ref.origin);
|
|
1426
|
-
return named ? [named.path] : [];
|
|
1427
|
-
})();
|
|
1428
|
-
if (candidateDirs.length === 0)
|
|
1429
|
-
return null;
|
|
1430
|
-
for (const name of nameVariants) {
|
|
1431
|
-
const suffix = `:${ref.type}:${name}`;
|
|
1432
|
-
const escapedSuffix = escapeLike(suffix);
|
|
1433
|
-
for (const dir of candidateDirs) {
|
|
1434
|
-
const escapedDir = escapeLike(dir);
|
|
1435
|
-
const row = db
|
|
1436
|
-
.prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type FROM entries " +
|
|
1437
|
-
"WHERE entry_key LIKE ? ESCAPE '\\' AND entry_type = ? LIMIT 1")
|
|
1438
|
-
.get(`${escapedDir}${escapedSuffix}`, ref.type);
|
|
1439
|
-
if (row) {
|
|
1440
|
-
return {
|
|
1441
|
-
entryKey: row.entryKey,
|
|
1442
|
-
filePath: row.filePath,
|
|
1443
|
-
stashDir: row.stashDir,
|
|
1444
|
-
type: row.type,
|
|
1445
|
-
name: ref.name,
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
return null;
|
|
1451
|
-
}
|
|
1452
|
-
finally {
|
|
1453
|
-
closeDatabase(db);
|
|
1454
|
-
}
|
|
1712
|
+
return lookupBundleRef({ bundle: ref.origin, conceptId: conceptIdFromTypeName(ref.type, ref.name) });
|
|
1455
1713
|
}
|
|
1456
1714
|
// ── Utility score recomputation ──────────────────────────────────────────────
|
|
1457
1715
|
/** Retention window for usage events: events older than this are purged. */
|