akm-cli 0.9.0-rc.0 → 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 +1283 -22
- 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/prompts/workflow-unit-preamble.md +26 -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 +99 -0
- package/dist/commands/env/env-cli.js +84 -224
- 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/migration-help.js +7 -4
- 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 +232 -121
- 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 -834
- 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/json-schema.js +142 -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 -34
- 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 -56
- 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 +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/prompts.js +46 -18
- package/dist/integrations/agent/runner-dispatch.js +93 -4
- package/dist/integrations/agent/runner.js +76 -208
- package/dist/integrations/agent/spawn.js +88 -196
- package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
- package/dist/integrations/harnesses/aider/index.js +48 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
- package/dist/integrations/harnesses/amazonq/index.js +45 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/config-import.js +1 -3
- package/dist/integrations/harnesses/claude/index.js +24 -35
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -75
- package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
- package/dist/integrations/harnesses/codex/index.js +52 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +48 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +48 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/ids.js +24 -0
- package/dist/integrations/harnesses/index.js +54 -34
- 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 +14 -32
- package/dist/integrations/harnesses/opencode/session-log.js +67 -125
- package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
- package/dist/integrations/harnesses/openhands/index.js +48 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
- package/dist/integrations/harnesses/pi/index.js +45 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/shared.js +17 -0
- package/dist/integrations/harnesses/types.js +43 -32
- package/dist/integrations/lockfile.js +211 -24
- package/dist/integrations/session-logs/index.js +36 -39
- 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 +14 -279
- 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 +11 -39
- 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 -1164
- 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 +3 -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 +240 -19
- 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/text-import-hook.mjs +1 -1
- package/dist/workflows/authoring/authoring.js +104 -43
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/cli.js +0 -16
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/exec/brief.js +450 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1038 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1460 -0
- package/dist/workflows/exec/run-workflow.js +602 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1190 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/workflow-engine-gate.js +67 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +246 -0
- package/dist/workflows/ir/freeze.js +233 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +540 -0
- package/dist/workflows/parser.js +878 -304
- package/dist/workflows/program/expressions.js +181 -0
- package/dist/workflows/program/schema.js +51 -0
- package/dist/workflows/renderer.js +100 -45
- package/dist/workflows/resource-limits.js +22 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/checkin.js +1 -1
- package/dist/workflows/runtime/plan-classifier.js +131 -0
- package/dist/workflows/runtime/runs.js +376 -119
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/unit-phases.js +20 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +2 -3
- 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/schemas/akm-workflow.json +373 -0
- 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 -259
- 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 -1413
- 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 -49
- 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/scripts/migrate-storage.js +0 -19108
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
- 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/db.js +0 -215
- package/docs/data-and-telemetry.md +0 -226
- /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/db/db.js
DELETED
|
@@ -1,1413 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
7
|
-
import { bestEffort } from "../../core/best-effort.js";
|
|
8
|
-
import { getDbPath } from "../../core/paths.js";
|
|
9
|
-
import { warn } from "../../core/warn.js";
|
|
10
|
-
import { cosineSimilarity } from "../../llm/embedders/types.js";
|
|
11
|
-
import { sha256Hex } from "../../runtime.js";
|
|
12
|
-
import { openManagedDatabase } from "../../storage/managed-db.js";
|
|
13
|
-
import { computeNextUtility, HIGH_UTILITY_THRESHOLD, UTILITY_REVIEW_THRESHOLD, } from "../feedback/utility-policy.js";
|
|
14
|
-
import { buildPrefixQuery, sanitizeFtsQuery } from "../search/fts-query.js";
|
|
15
|
-
import { buildSearchFields } from "../search/search-fields.js";
|
|
16
|
-
import { ENTRY_COLUMNS, rowToIndexedEntry } from "./entry-mapper.js";
|
|
17
|
-
import { ensureSchema } from "./schema.js";
|
|
18
|
-
export { HIGH_UTILITY_THRESHOLD, sanitizeFtsQuery, UTILITY_REVIEW_THRESHOLD };
|
|
19
|
-
// ── Database lifecycle ──────────────────────────────────────────────────────
|
|
20
|
-
export function openIndexDatabase(dbPath, options) {
|
|
21
|
-
return openManagedDatabase({
|
|
22
|
-
path: dbPath ?? getDbPath(),
|
|
23
|
-
init: (db) => {
|
|
24
|
-
// Try to load sqlite-vec extension
|
|
25
|
-
loadVecExtension(db);
|
|
26
|
-
// Dim resolution: explicit option wins; otherwise consult the on-disk
|
|
27
|
-
// config so unparameterised opens (registry providers, graph helpers,
|
|
28
|
-
// ad-hoc CLI subcommands) honour the operator-declared dimension. Only if
|
|
29
|
-
// both are absent do we fall through to the no-clobber path, which keeps
|
|
30
|
-
// ensureSchema from touching `index_meta.embeddingDim` at all.
|
|
31
|
-
const resolvedDim = options?.embeddingDim ?? resolveConfiguredEmbeddingDim();
|
|
32
|
-
ensureSchema(db, resolvedDim);
|
|
33
|
-
// Warn once at init if using JS fallback with many entries
|
|
34
|
-
warnIfVecMissing(db, { once: true });
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Read the operator-configured embedding dimension from the on-disk config.
|
|
40
|
-
* Returns `undefined` when no config file is present, when the config has
|
|
41
|
-
* no `embedding.dimension` set, or when reading the config throws (e.g.
|
|
42
|
-
* inside isolated test fixtures with no XDG home). Failure is silent on
|
|
43
|
-
* purpose — every openDatabase() call would otherwise have to handle a
|
|
44
|
-
* config-not-found error path, and the fallback (no-clobber semantics) is
|
|
45
|
-
* already correct.
|
|
46
|
-
*/
|
|
47
|
-
function resolveConfiguredEmbeddingDim() {
|
|
48
|
-
try {
|
|
49
|
-
const { loadConfig } = require("../../core/config/config");
|
|
50
|
-
const dim = loadConfig().embedding?.dimension;
|
|
51
|
-
if (typeof dim === "number" && Number.isInteger(dim) && dim > 0 && dim <= 4096) {
|
|
52
|
-
return dim;
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
export function openExistingDatabase(dbPath) {
|
|
61
|
-
// Existing-DB callers must not mutate schema or embedding metadata on open,
|
|
62
|
-
// but some paths still need write access to usage_events and other tables —
|
|
63
|
-
// so init only loads the vec extension, it does not run ensureSchema.
|
|
64
|
-
return openManagedDatabase({ path: dbPath ?? getDbPath(), init: loadVecExtension });
|
|
65
|
-
}
|
|
66
|
-
export function closeDatabase(db) {
|
|
67
|
-
db.close();
|
|
68
|
-
}
|
|
69
|
-
// ── sqlite-vec extension ────────────────────────────────────────────────────
|
|
70
|
-
const vecStatus = new WeakMap();
|
|
71
|
-
function loadVecExtension(db) {
|
|
72
|
-
try {
|
|
73
|
-
const esmRequire = createRequire(import.meta.url);
|
|
74
|
-
const sqliteVec = esmRequire("sqlite-vec");
|
|
75
|
-
// `db` here is the genuine underlying driver handle returned by the storage
|
|
76
|
-
// boundary (bun:sqlite on Bun, better-sqlite3 on Node) — only structurally
|
|
77
|
-
// narrowed for callers. sqlite-vec's `load()` accepts either real handle,
|
|
78
|
-
// so no raw-handle escape hatch is required.
|
|
79
|
-
sqliteVec.load(db);
|
|
80
|
-
vecStatus.set(db, true);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
vecStatus.set(db, false);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
export function isVecAvailable(db) {
|
|
87
|
-
return vecStatus.get(db) ?? false;
|
|
88
|
-
}
|
|
89
|
-
const VEC_DOCS_URL = "https://github.com/itlackey/akm/blob/main/docs/configuration.md#sqlite-vec-extension";
|
|
90
|
-
const VEC_FALLBACK_THRESHOLD = 10_000;
|
|
91
|
-
// Per-database warning state: tracks which databases have already emitted the
|
|
92
|
-
// vec-missing warning so we don't spam on every openDatabase() call.
|
|
93
|
-
const vecInitWarnedDbs = new WeakSet();
|
|
94
|
-
/**
|
|
95
|
-
* Warn if sqlite-vec is unavailable and embedding count exceeds threshold.
|
|
96
|
-
* Called from openDatabase (once at init) and from indexer (each run).
|
|
97
|
-
*/
|
|
98
|
-
export function warnIfVecMissing(db, { once } = { once: false }) {
|
|
99
|
-
if (isVecAvailable(db))
|
|
100
|
-
return;
|
|
101
|
-
if (once && vecInitWarnedDbs.has(db))
|
|
102
|
-
return;
|
|
103
|
-
bestEffort(() => {
|
|
104
|
-
const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
|
|
105
|
-
const count = row?.cnt ?? 0;
|
|
106
|
-
if (count >= VEC_FALLBACK_THRESHOLD) {
|
|
107
|
-
warn("Semantic search is using JS fallback for %d entries. Install sqlite-vec for faster performance.\n See: %s", count, VEC_DOCS_URL);
|
|
108
|
-
if (once)
|
|
109
|
-
vecInitWarnedDbs.add(db);
|
|
110
|
-
}
|
|
111
|
-
}, "embeddings table may not exist yet during init");
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Purge stored embeddings (BLOB rows in `embeddings`, plus the `entries_vec`
|
|
115
|
-
* virtual table) and mark the index as embedding-free. The single place that
|
|
116
|
-
* invalidates embeddings — used on a dimension change, a model/provider change,
|
|
117
|
-
* and a full rebuild.
|
|
118
|
-
*
|
|
119
|
-
* No backup: embeddings are a derived cache, fully regenerable from the markdown
|
|
120
|
-
* by the next `akm index`. (Recovery model decided 2026-06-25.)
|
|
121
|
-
*
|
|
122
|
-
* `dropVecTable: true` DROPs `entries_vec` — used on a DIMENSION change, where
|
|
123
|
-
* the vec0 table must be recreated at the new width by the caller. The default
|
|
124
|
-
* clears its rows in place (same dimension, stale vectors).
|
|
125
|
-
*/
|
|
126
|
-
export function purgeEmbeddings(db, opts) {
|
|
127
|
-
bestEffort(() => db.exec("DELETE FROM embeddings"), "purge embeddings");
|
|
128
|
-
if (isVecAvailable(db)) {
|
|
129
|
-
bestEffort(() => db.exec(opts?.dropVecTable ? "DROP TABLE IF EXISTS entries_vec" : "DELETE FROM entries_vec"), "purge entries_vec");
|
|
130
|
-
}
|
|
131
|
-
setMeta(db, "hasEmbeddings", "0");
|
|
132
|
-
}
|
|
133
|
-
// ── Meta helpers ────────────────────────────────────────────────────────────
|
|
134
|
-
export function getMeta(db, key) {
|
|
135
|
-
const row = db.prepare("SELECT value FROM index_meta WHERE key = ?").get(key);
|
|
136
|
-
return row?.value;
|
|
137
|
-
}
|
|
138
|
-
export function setMeta(db, key, value) {
|
|
139
|
-
db.prepare("INSERT OR REPLACE INTO index_meta (key, value) VALUES (?, ?)").run(key, value);
|
|
140
|
-
}
|
|
141
|
-
export function getIndexDirState(db, dirPath) {
|
|
142
|
-
const row = db
|
|
143
|
-
.prepare("SELECT dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at FROM index_dir_state WHERE dir_path = ?")
|
|
144
|
-
.get(dirPath);
|
|
145
|
-
if (!row)
|
|
146
|
-
return undefined;
|
|
147
|
-
return {
|
|
148
|
-
dirPath: row.dir_path,
|
|
149
|
-
fileSetHash: row.file_set_hash,
|
|
150
|
-
fileMtimeMaxMs: row.file_mtime_max_ms,
|
|
151
|
-
reason: row.reason,
|
|
152
|
-
updatedAt: row.updated_at,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
export function upsertIndexDirState(db, state) {
|
|
156
|
-
db.prepare(`INSERT INTO index_dir_state (dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at)
|
|
157
|
-
VALUES (?, ?, ?, ?, ?)
|
|
158
|
-
ON CONFLICT(dir_path) DO UPDATE SET
|
|
159
|
-
file_set_hash = excluded.file_set_hash,
|
|
160
|
-
file_mtime_max_ms = excluded.file_mtime_max_ms,
|
|
161
|
-
reason = excluded.reason,
|
|
162
|
-
updated_at = excluded.updated_at`).run(state.dirPath, state.fileSetHash, state.fileMtimeMaxMs, state.reason, new Date().toISOString());
|
|
163
|
-
}
|
|
164
|
-
export function deleteIndexDirState(db, dirPath) {
|
|
165
|
-
db.prepare("DELETE FROM index_dir_state WHERE dir_path = ?").run(dirPath);
|
|
166
|
-
}
|
|
167
|
-
export function deleteIndexDirStatesByStashDir(db, stashDir) {
|
|
168
|
-
db.prepare("DELETE FROM index_dir_state WHERE dir_path = ? OR dir_path LIKE ?").run(stashDir, `${stashDir}${path.sep}%`);
|
|
169
|
-
}
|
|
170
|
-
// ── Entry operations ────────────────────────────────────────────────────────
|
|
171
|
-
/**
|
|
172
|
-
* SQLite parameter chunk size — chosen well below SQLITE_MAX_VARIABLE_NUMBER
|
|
173
|
-
* (default 999 on most builds) so multi-row `IN (?, ?, ...)` queries stay
|
|
174
|
-
* within bounds. Shared by helpers below.
|
|
175
|
-
*/
|
|
176
|
-
const SQLITE_CHUNK_SIZE = 500;
|
|
177
|
-
/**
|
|
178
|
-
* Insert or update an entry in the `entries` table. Returns the row id.
|
|
179
|
-
*
|
|
180
|
-
* **Important:** This does not update the FTS index. Callers must call
|
|
181
|
-
* `rebuildFts()` after all upserts are complete for full-text search to
|
|
182
|
-
* reflect the changes.
|
|
183
|
-
*/
|
|
184
|
-
export function upsertEntry(db, entryKey, dirPath, filePath, stashDir, entry, searchText) {
|
|
185
|
-
// Hot path during indexing — cache the two prepared statements per
|
|
186
|
-
// database connection so we don't pay the SQL parse/compile cost on
|
|
187
|
-
// every call. The dirty-mark INSERT and the upsert-with-RETURNING
|
|
188
|
-
// share the same WeakMap so they live and die with the connection.
|
|
189
|
-
const stmts = getUpsertStmts(db);
|
|
190
|
-
// Phase 5A / Advantage D5: surface derived memory parent ref into the
|
|
191
|
-
// dedicated `derived_from` column so retrieval-time lookup (parent→child)
|
|
192
|
-
// does not have to scan + JSON-decode every memory row.
|
|
193
|
-
const derivedFrom = typeof entry.derivedFrom === "string" && entry.derivedFrom.trim() ? entry.derivedFrom.trim() : null;
|
|
194
|
-
const result = stmts.upsert.get(entryKey, dirPath, filePath, stashDir, JSON.stringify(entry), searchText, entry.type, derivedFrom);
|
|
195
|
-
if (!result)
|
|
196
|
-
throw new Error("upsertEntry: entry_key not found after upsert");
|
|
197
|
-
// Mark this entry as FTS-dirty so `rebuildFts({ incremental: true })`
|
|
198
|
-
// only revisits entries that actually changed. INSERT OR IGNORE is
|
|
199
|
-
// idempotent across multiple upserts of the same row.
|
|
200
|
-
stmts.markDirty.run(result.id);
|
|
201
|
-
return result.id;
|
|
202
|
-
}
|
|
203
|
-
const upsertStmtsByDb = new WeakMap();
|
|
204
|
-
function getUpsertStmts(db) {
|
|
205
|
-
const existing = upsertStmtsByDb.get(db);
|
|
206
|
-
if (existing)
|
|
207
|
-
return existing;
|
|
208
|
-
const stmts = {
|
|
209
|
-
// RETURNING id handles ON CONFLICT DO UPDATE correctly — no second
|
|
210
|
-
// SELECT round-trip needed (last_insert_rowid() is unreliable for
|
|
211
|
-
// ON CONFLICT). Use `.get()` so a single row comes back.
|
|
212
|
-
upsert: db.prepare(`
|
|
213
|
-
INSERT INTO entries (entry_key, dir_path, file_path, stash_dir, entry_json, search_text, entry_type, derived_from)
|
|
214
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
215
|
-
ON CONFLICT(entry_key) DO UPDATE SET
|
|
216
|
-
dir_path = excluded.dir_path,
|
|
217
|
-
file_path = excluded.file_path,
|
|
218
|
-
stash_dir = excluded.stash_dir,
|
|
219
|
-
entry_json = excluded.entry_json,
|
|
220
|
-
search_text = excluded.search_text,
|
|
221
|
-
entry_type = excluded.entry_type,
|
|
222
|
-
derived_from = excluded.derived_from
|
|
223
|
-
RETURNING id
|
|
224
|
-
`),
|
|
225
|
-
markDirty: db.prepare("INSERT OR IGNORE INTO entries_fts_dirty (entry_id) VALUES (?)"),
|
|
226
|
-
};
|
|
227
|
-
upsertStmtsByDb.set(db, stmts);
|
|
228
|
-
return stmts;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Phase 5A / Advantage D5: look up the derived-memory child row whose
|
|
232
|
-
* `derived_from` column matches `parentRef` (e.g. `"memory:claude-prefs"`).
|
|
233
|
-
*
|
|
234
|
-
* Returns the most-recently-updated derived child when multiple exist (one
|
|
235
|
-
* parent should yield exactly one `.derived` child in practice, but the
|
|
236
|
-
* ordering keeps results deterministic). Returns `null` when no derived
|
|
237
|
-
* child has been indexed for this parent.
|
|
238
|
-
*/
|
|
239
|
-
export function getDerivedForParent(db, parentRef) {
|
|
240
|
-
if (!parentRef)
|
|
241
|
-
return null;
|
|
242
|
-
try {
|
|
243
|
-
const row = db
|
|
244
|
-
.prepare(`SELECT ${ENTRY_COLUMNS}
|
|
245
|
-
FROM entries
|
|
246
|
-
WHERE derived_from = ?
|
|
247
|
-
ORDER BY id DESC
|
|
248
|
-
LIMIT 1`)
|
|
249
|
-
.get(parentRef);
|
|
250
|
-
if (!row)
|
|
251
|
-
return null;
|
|
252
|
-
return rowToIndexedEntry(row, "getDerivedForParent");
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
/* `derived_from` column may not exist on legacy DBs that haven't been
|
|
256
|
-
rebuilt; treat as "no derived child". */
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* 03-R3: for the given derived-twin row ids, fetch each twin's BASE memory
|
|
262
|
-
* `beliefState`, keyed by twin id.
|
|
263
|
-
*
|
|
264
|
-
* Used by the derived-twin belief inheritance in search ranking: a `.derived`
|
|
265
|
-
* twin has no belief state of its own, so it inherits its base memory's
|
|
266
|
-
* demoting state (contradicted/superseded/…) at search time. A twin's
|
|
267
|
-
* `entry_key` is exactly its base's `entry_key` plus the `.derived` suffix
|
|
268
|
-
* (same stash + type prefix, `<name>` vs `<name>.derived`), so the base is
|
|
269
|
-
* found by stripping that suffix — no ref/prefix reconstruction. Returns a map
|
|
270
|
-
* of twin id → base beliefState for bases that carry a non-empty state.
|
|
271
|
-
* Best-effort: any query error (e.g. legacy DB) yields no inheritance rather
|
|
272
|
-
* than failing the search.
|
|
273
|
-
*/
|
|
274
|
-
export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
|
|
275
|
-
const out = new Map();
|
|
276
|
-
if (twinIds.length === 0)
|
|
277
|
-
return out;
|
|
278
|
-
// Chunk at SQLITE_CHUNK_SIZE like the sibling bulk-by-id helpers, so a large
|
|
279
|
-
// `--limit` candidate set never trips SQLITE_MAX_VARIABLE_NUMBER (which would
|
|
280
|
-
// otherwise fall into the best-effort catch and silently disable the feature).
|
|
281
|
-
for (let i = 0; i < twinIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
282
|
-
const chunk = twinIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
283
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
284
|
-
bestEffort(() => {
|
|
285
|
-
const rows = db
|
|
286
|
-
.prepare(`SELECT twin.id AS twin_id, json_extract(base.entry_json, '$.beliefState') AS belief
|
|
287
|
-
FROM entries twin
|
|
288
|
-
JOIN entries base
|
|
289
|
-
ON base.entry_type = 'memory'
|
|
290
|
-
AND base.entry_key = substr(twin.entry_key, 1, length(twin.entry_key) - length('.derived'))
|
|
291
|
-
WHERE twin.id IN (${placeholders})
|
|
292
|
-
AND twin.entry_key LIKE '%.derived'
|
|
293
|
-
AND json_extract(base.entry_json, '$.beliefState') IS NOT NULL`)
|
|
294
|
-
.all(...chunk);
|
|
295
|
-
for (const r of rows) {
|
|
296
|
-
if (typeof r.belief === "string" && r.belief.trim().length > 0)
|
|
297
|
-
out.set(r.twin_id, r.belief.trim());
|
|
298
|
-
}
|
|
299
|
-
}, "legacy DB / entry_json without beliefState — treat as no twin inheritance");
|
|
300
|
-
}
|
|
301
|
-
return out;
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Phase 2A / Rec 5: bulk-load positive feedback event counts for the given
|
|
305
|
-
* entry ids. Used by the utility-decay forgetting curve to stabilize
|
|
306
|
-
* (extend the half-life of) memories that have repeatedly proven useful.
|
|
307
|
-
*
|
|
308
|
-
* Returns a `Map<entryId, count>` containing only entries with at least one
|
|
309
|
-
* positive feedback event — missing ids implicitly map to `0`. Chunks at
|
|
310
|
-
* `SQLITE_CHUNK_SIZE` (500) to respect `SQLITE_MAX_VARIABLE_NUMBER`.
|
|
311
|
-
*
|
|
312
|
-
* Cheap when called with zero ids, and silently empty when the
|
|
313
|
-
* `usage_events` table is missing.
|
|
314
|
-
*/
|
|
315
|
-
export function getPositiveFeedbackCountsByIds(db, ids) {
|
|
316
|
-
const result = new Map();
|
|
317
|
-
if (ids.length === 0)
|
|
318
|
-
return result;
|
|
319
|
-
for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
|
|
320
|
-
const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
321
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
322
|
-
bestEffort(() => {
|
|
323
|
-
const rows = db
|
|
324
|
-
.prepare(`SELECT entry_id, COUNT(*) AS cnt
|
|
325
|
-
FROM usage_events
|
|
326
|
-
WHERE event_type = 'feedback'
|
|
327
|
-
AND signal = 'positive'
|
|
328
|
-
AND entry_id IN (${placeholders})
|
|
329
|
-
GROUP BY entry_id`)
|
|
330
|
-
.all(...chunk);
|
|
331
|
-
for (const row of rows) {
|
|
332
|
-
if (row.entry_id !== null && row.cnt > 0) {
|
|
333
|
-
result.set(row.entry_id, row.cnt);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}, "usage_events table may be missing on legacy DBs — treat as zero counts");
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
339
|
-
}
|
|
340
|
-
function deleteEntriesWhere(db, column, value) {
|
|
341
|
-
db.transaction(() => {
|
|
342
|
-
const ids = db.prepare(`SELECT id FROM entries WHERE ${column} = ?`).all(value);
|
|
343
|
-
deleteRelatedRows(db, ids);
|
|
344
|
-
db.prepare(`DELETE FROM entries WHERE ${column} = ?`).run(value);
|
|
345
|
-
})();
|
|
346
|
-
}
|
|
347
|
-
export function deleteEntriesByDir(db, dirPath) {
|
|
348
|
-
deleteEntriesWhere(db, "dir_path", dirPath);
|
|
349
|
-
}
|
|
350
|
-
export function deleteEntriesByStashDir(db, stashDir) {
|
|
351
|
-
deleteEntriesWhere(db, "stash_dir", stashDir);
|
|
352
|
-
}
|
|
353
|
-
function deleteRelatedRows(db, ids) {
|
|
354
|
-
if (ids.length === 0)
|
|
355
|
-
return;
|
|
356
|
-
const numericIds = ids.map((r) => r.id);
|
|
357
|
-
const vecAvail = isVecAvailable(db);
|
|
358
|
-
// Drop matching FTS rows + dirty markers immediately so an incremental
|
|
359
|
-
// rebuild after a deletion doesn't try to re-index entries that no longer
|
|
360
|
-
// exist (and so a full scan after deletion sees a consistent FTS).
|
|
361
|
-
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
362
|
-
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
363
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
364
|
-
bestEffort(() => db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk), "fts table may not exist on a brand-new db");
|
|
365
|
-
bestEffort(() => db.prepare(`DELETE FROM entries_fts_dirty WHERE entry_id IN (${placeholders})`).run(...chunk), "fts dirty table is created lazily by upsertEntry");
|
|
366
|
-
}
|
|
367
|
-
// Process in chunks to stay within SQLITE_MAX_VARIABLE_NUMBER
|
|
368
|
-
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
369
|
-
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
370
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
371
|
-
bestEffort(() => db.prepare(`DELETE FROM embeddings WHERE id IN (${placeholders})`).run(...chunk), "delete embeddings for entries");
|
|
372
|
-
if (vecAvail) {
|
|
373
|
-
bestEffort(() => db.prepare(`DELETE FROM entries_vec WHERE id IN (${placeholders})`).run(...chunk), "delete entries_vec for entries");
|
|
374
|
-
}
|
|
375
|
-
// Clean up utility scores before deleting entries
|
|
376
|
-
bestEffort(() => db.prepare(`DELETE FROM utility_scores WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores for entries");
|
|
377
|
-
bestEffort(() => db.prepare(`DELETE FROM utility_scores_scoped WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores_scoped for entries");
|
|
378
|
-
// Clean up usage events before deleting entries
|
|
379
|
-
bestEffort(() => db.prepare(`DELETE FROM usage_events WHERE entry_id IN (${placeholders})`).run(...chunk), "delete usage_events for entries");
|
|
380
|
-
}
|
|
381
|
-
// #624-P1: graph_files is NO LONGER keyed on entries.id, so deleting an
|
|
382
|
-
// entries row must NOT wipe the extracted graph (that is the whole point —
|
|
383
|
-
// the graph survives a reindex when body_hash is unchanged). We therefore do
|
|
384
|
-
// NOT delete graph_files here. We DO, however, recompute graph_meta counts
|
|
385
|
-
// for the stash roots touched by the deleted entries so the summary numbers
|
|
386
|
-
// stay consistent with the live child rows (the counts are derived, and the
|
|
387
|
-
// entries delete may have changed which files are considered/indexed).
|
|
388
|
-
//
|
|
389
|
-
// Resolve the affected stash roots from the entries rows BEFORE deletion.
|
|
390
|
-
const affectedStashRoots = new Set();
|
|
391
|
-
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
392
|
-
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
393
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
394
|
-
bestEffort(() => {
|
|
395
|
-
const rows = db
|
|
396
|
-
.prepare(`SELECT DISTINCT stash_dir FROM entries WHERE id IN (${placeholders})`)
|
|
397
|
-
.all(...chunk);
|
|
398
|
-
for (const row of rows) {
|
|
399
|
-
if (row.stash_dir)
|
|
400
|
-
affectedStashRoots.add(row.stash_dir);
|
|
401
|
-
}
|
|
402
|
-
}, "resolve stash roots for graph_meta recompute");
|
|
403
|
-
}
|
|
404
|
-
for (const stashRoot of affectedStashRoots) {
|
|
405
|
-
bestEffort(() => db
|
|
406
|
-
.prepare(`UPDATE graph_meta
|
|
407
|
-
SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
|
|
408
|
-
entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
|
|
409
|
-
relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
|
|
410
|
-
WHERE stash_root = ?`)
|
|
411
|
-
.run(stashRoot, stashRoot, stashRoot, stashRoot), "sync graph_meta counts after entries delete");
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Delete entries by their primary key IDs, along with all related rows
|
|
416
|
-
* (embeddings, entries_vec, entries_fts, utility_scores, usage_events).
|
|
417
|
-
*
|
|
418
|
-
* Used by the `--clean` post-pass to remove stale entries whose source files
|
|
419
|
-
* no longer exist on disk.
|
|
420
|
-
*/
|
|
421
|
-
export function deleteEntriesByIds(db, ids) {
|
|
422
|
-
if (ids.length === 0)
|
|
423
|
-
return;
|
|
424
|
-
db.transaction(() => {
|
|
425
|
-
const idObjs = ids.map((id) => ({ id }));
|
|
426
|
-
deleteRelatedRows(db, idObjs);
|
|
427
|
-
for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
|
|
428
|
-
const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
429
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
430
|
-
db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk);
|
|
431
|
-
}
|
|
432
|
-
})();
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Rebuild the FTS5 search index.
|
|
436
|
-
*
|
|
437
|
-
* `incremental` (default `false`): when true, only rebuild rows that
|
|
438
|
-
* `upsertEntry` marked dirty since the last `rebuildFts` call. The full path
|
|
439
|
-
* (default) wipes `entries_fts` and re-inserts every row from `entries` —
|
|
440
|
-
* appropriate for `akm index --full` and version-upgrade rebuilds.
|
|
441
|
-
*
|
|
442
|
-
* Both paths are wrapped in a single transaction so the FTS table is never
|
|
443
|
-
* left in a half-rebuilt state.
|
|
444
|
-
*
|
|
445
|
-
* Skipped corrupt-JSON rows are aggregated into one warning instead of
|
|
446
|
-
* spamming stderr per-entry.
|
|
447
|
-
*/
|
|
448
|
-
export function rebuildFts(db, options) {
|
|
449
|
-
const incremental = options?.incremental === true;
|
|
450
|
-
db.transaction(() => {
|
|
451
|
-
let rows;
|
|
452
|
-
if (incremental) {
|
|
453
|
-
// Read the dirty queue and join against entries to get the JSON.
|
|
454
|
-
// Then drop the matching rows from entries_fts so the INSERT below
|
|
455
|
-
// doesn't double-up. The dirty list is drained at the end.
|
|
456
|
-
rows = db
|
|
457
|
-
.prepare(`SELECT e.id AS id, e.entry_json AS entry_json
|
|
458
|
-
FROM entries_fts_dirty d
|
|
459
|
-
JOIN entries e ON e.id = d.entry_id`)
|
|
460
|
-
.all();
|
|
461
|
-
if (rows.length === 0)
|
|
462
|
-
return;
|
|
463
|
-
const ids = rows.map((r) => r.id);
|
|
464
|
-
// Delete only the dirty FTS rows — chunk to stay under
|
|
465
|
-
// SQLITE_MAX_VARIABLE_NUMBER on large dirty queues.
|
|
466
|
-
for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
|
|
467
|
-
const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
468
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
469
|
-
db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
// Full path: wipe and re-read every row.
|
|
474
|
-
db.exec("DELETE FROM entries_fts");
|
|
475
|
-
rows = db.prepare("SELECT id, entry_json FROM entries").all();
|
|
476
|
-
}
|
|
477
|
-
const insertStmt = db.prepare("INSERT INTO entries_fts (entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?)");
|
|
478
|
-
let skipped = 0;
|
|
479
|
-
for (const row of rows) {
|
|
480
|
-
let entry;
|
|
481
|
-
let fields;
|
|
482
|
-
try {
|
|
483
|
-
entry = JSON.parse(row.entry_json);
|
|
484
|
-
fields = buildSearchFields(entry);
|
|
485
|
-
}
|
|
486
|
-
catch {
|
|
487
|
-
skipped++;
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
insertStmt.run(row.id, fields.name, fields.description, fields.tags, fields.hints, fields.content);
|
|
491
|
-
}
|
|
492
|
-
if (skipped > 0) {
|
|
493
|
-
warn(`[db] rebuildFts: skipped ${skipped} entr${skipped === 1 ? "y" : "ies"} with invalid entry_json`);
|
|
494
|
-
}
|
|
495
|
-
// Always drain the dirty queue — both paths converge here. The
|
|
496
|
-
// incremental path drains it because we just consumed every dirty row;
|
|
497
|
-
// the full path drains it because a full rebuild covers everything the
|
|
498
|
-
// dirty list tracks. The table is guaranteed to exist (created by
|
|
499
|
-
// ensureSchema()).
|
|
500
|
-
//
|
|
501
|
-
// BUG-L1: previously the if/else arms ran identical statements — the
|
|
502
|
-
// duplication has been collapsed.
|
|
503
|
-
db.exec("DELETE FROM entries_fts_dirty");
|
|
504
|
-
})();
|
|
505
|
-
}
|
|
506
|
-
// ── Vector operations ───────────────────────────────────────────────────────
|
|
507
|
-
export function upsertEmbedding(db, entryId, embedding) {
|
|
508
|
-
// Pre-flight FK guard: when an entry is deleted between when its id is queued
|
|
509
|
-
// for embedding and when this INSERT runs (e.g. consolidation deletes during
|
|
510
|
-
// a concurrent improve cycle), the INSERT throws "FOREIGN KEY constraint failed"
|
|
511
|
-
// and rolls back the entire batch transaction in the caller, losing every
|
|
512
|
-
// embedding for that run. A cheap SELECT here turns the race into a clean skip.
|
|
513
|
-
const exists = db.prepare("SELECT 1 FROM entries WHERE id = ?").get(entryId);
|
|
514
|
-
if (!exists)
|
|
515
|
-
return false;
|
|
516
|
-
const buf = float32Buffer(embedding);
|
|
517
|
-
// Always write to BLOB table (works without sqlite-vec)
|
|
518
|
-
db.prepare("INSERT OR REPLACE INTO embeddings (id, embedding) VALUES (?, ?)").run(entryId, buf);
|
|
519
|
-
// Also write to sqlite-vec table when available (fast path).
|
|
520
|
-
// Wrapped in a transaction so a crash between DELETE and INSERT does not
|
|
521
|
-
// leave the entry missing from the vec table.
|
|
522
|
-
if (isVecAvailable(db)) {
|
|
523
|
-
bestEffort(() => {
|
|
524
|
-
db.transaction(() => {
|
|
525
|
-
db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
|
|
526
|
-
db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
|
|
527
|
-
})();
|
|
528
|
-
}, "vec table unavailable or constraint failure");
|
|
529
|
-
}
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
export function searchVec(db, queryEmbedding, k) {
|
|
533
|
-
// Fast path: use sqlite-vec when available
|
|
534
|
-
if (isVecAvailable(db)) {
|
|
535
|
-
const buf = float32Buffer(queryEmbedding);
|
|
536
|
-
try {
|
|
537
|
-
return db
|
|
538
|
-
.prepare("SELECT id, distance FROM entries_vec WHERE embedding MATCH ? AND k = ?")
|
|
539
|
-
.all(buf, k);
|
|
540
|
-
}
|
|
541
|
-
catch (err) {
|
|
542
|
-
// Log the failure so it's visible in diagnostics
|
|
543
|
-
warn("[db] searchVec (sqlite-vec path) failed:", err instanceof Error ? err.message : String(err));
|
|
544
|
-
return [];
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
// Fallback: JS-based cosine similarity over BLOB table
|
|
548
|
-
return searchBlobVec(db, queryEmbedding, k);
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Return the k nearest neighbours of an already-indexed entry using its
|
|
552
|
-
* persisted embedding — no re-embedding, no network. Decodes the stored BLOB by
|
|
553
|
-
* byte length (dim = bytes / 4) and reuses searchVec (sqlite-vec fast path or
|
|
554
|
-
* JS-cosine fallback). Returns [] when the entry has no stored embedding or the
|
|
555
|
-
* BLOB is corrupt. The query entry itself is typically returned with distance
|
|
556
|
-
* ~0 — callers should filter it out by id.
|
|
557
|
-
*/
|
|
558
|
-
export function getNeighborsByEntryId(db, id, k) {
|
|
559
|
-
const row = db.prepare("SELECT embedding FROM embeddings WHERE id = ?").get(id);
|
|
560
|
-
if (!row)
|
|
561
|
-
return [];
|
|
562
|
-
const queryEmbedding = bufferToFloat32(row.embedding, Math.floor(row.embedding.byteLength / 4));
|
|
563
|
-
if (!queryEmbedding)
|
|
564
|
-
return [];
|
|
565
|
-
return searchVec(db, queryEmbedding, k);
|
|
566
|
-
}
|
|
567
|
-
function float32Buffer(vec) {
|
|
568
|
-
const f32 = new Float32Array(vec);
|
|
569
|
-
return Buffer.from(f32.buffer);
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Decode a stored embedding BLOB into a Float32 array of `expectedDim`
|
|
573
|
-
* dimensions. Returns `null` (and emits a warning) when the byte length does
|
|
574
|
-
* not exactly match `expectedDim * 4`, including the legacy partial-trailing
|
|
575
|
-
* float case the previous truncating-divide silently swallowed.
|
|
576
|
-
*
|
|
577
|
-
* BUG-M2: the previous `buf.byteLength / 4` divide would truncate any
|
|
578
|
-
* trailing partial float and a misaligned `byteOffset` would throw — both
|
|
579
|
-
* surfaced as opaque generic errors caught upstream.
|
|
580
|
-
*/
|
|
581
|
-
function bufferToFloat32(buf, expectedDim) {
|
|
582
|
-
if (buf.byteLength !== expectedDim * 4) {
|
|
583
|
-
warn("[db] bufferToFloat32: skipping embedding row — expected %d bytes (%d dim x 4), got %d", expectedDim * 4, expectedDim, buf.byteLength);
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
// Copy into a fresh ArrayBuffer to sidestep any byteOffset alignment
|
|
587
|
-
// requirements imposed by Float32Array's typed-array view contract.
|
|
588
|
-
const aligned = new ArrayBuffer(buf.byteLength);
|
|
589
|
-
new Uint8Array(aligned).set(buf);
|
|
590
|
-
const f32 = new Float32Array(aligned);
|
|
591
|
-
return Array.from(f32);
|
|
592
|
-
}
|
|
593
|
-
function searchBlobVec(db, queryEmbedding, k) {
|
|
594
|
-
try {
|
|
595
|
-
const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
|
|
596
|
-
if (rows.length === 0)
|
|
597
|
-
return [];
|
|
598
|
-
const expectedDim = queryEmbedding.length;
|
|
599
|
-
const scored = [];
|
|
600
|
-
for (const row of rows) {
|
|
601
|
-
const embedding = bufferToFloat32(row.embedding, expectedDim);
|
|
602
|
-
if (embedding === null)
|
|
603
|
-
continue;
|
|
604
|
-
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
605
|
-
scored.push({ id: row.id, similarity });
|
|
606
|
-
}
|
|
607
|
-
scored.sort((a, b) => b.similarity - a.similarity);
|
|
608
|
-
// Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
|
|
609
|
-
// For normalized vectors: L2² = 2(1 - cos_sim)
|
|
610
|
-
return scored.slice(0, k).map(({ id, similarity }) => ({
|
|
611
|
-
id,
|
|
612
|
-
distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
|
|
613
|
-
}));
|
|
614
|
-
}
|
|
615
|
-
catch (err) {
|
|
616
|
-
// MD-5: Log the failure so it's visible in diagnostics
|
|
617
|
-
warn("[db] searchBlobVec (JS fallback) failed:", err instanceof Error ? err.message : String(err));
|
|
618
|
-
return [];
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
// ── FTS5 search ─────────────────────────────────────────────────────────────
|
|
622
|
-
export function searchFts(db, query, limit, entryType, excludeTypes) {
|
|
623
|
-
const ftsQuery = sanitizeFtsQuery(query);
|
|
624
|
-
if (!ftsQuery)
|
|
625
|
-
return [];
|
|
626
|
-
// Try the exact AND query first
|
|
627
|
-
const exactResults = runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes);
|
|
628
|
-
if (exactResults.length > 0)
|
|
629
|
-
return exactResults;
|
|
630
|
-
// Exact match returned zero results — try prefix fallback.
|
|
631
|
-
// Append FTS5 `*` suffix to each token that is >= 3 characters long.
|
|
632
|
-
// Short tokens (1-2 chars) are excluded from prefix expansion because
|
|
633
|
-
// they produce too many false positives.
|
|
634
|
-
const prefixQuery = buildPrefixQuery(ftsQuery);
|
|
635
|
-
if (!prefixQuery)
|
|
636
|
-
return [];
|
|
637
|
-
return runFtsQuery(db, prefixQuery, limit, entryType, excludeTypes);
|
|
638
|
-
}
|
|
639
|
-
function runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes) {
|
|
640
|
-
// #627 — exclude-type clause. Only applies on the untyped ('any') path; an
|
|
641
|
-
// explicit include filter (entryType) already narrows to a single type, so
|
|
642
|
-
// exclusion is redundant there. An empty list skips the clause entirely
|
|
643
|
-
// (never emit `NOT IN ()`, which is a SQL error / always-false).
|
|
644
|
-
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
645
|
-
// The typed and untyped paths differ ONLY by one WHERE clause (an entry_type
|
|
646
|
-
// equality vs. an optional NOT IN exclusion) and their param order — the
|
|
647
|
-
// SELECT/JOIN/ORDER/LIMIT is shared, so build it once. Join on integer
|
|
648
|
-
// entry_id directly (no CAST; we store integer). bm25() per-column weights:
|
|
649
|
-
// entry_id(0), name(10), description(5), tags(3), hints(2), content(1).
|
|
650
|
-
let filterClause;
|
|
651
|
-
let params;
|
|
652
|
-
if (entryType && entryType !== "any") {
|
|
653
|
-
filterClause = "AND e.entry_type = ?";
|
|
654
|
-
params = [ftsQuery, entryType, limit];
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
filterClause = excludes.length > 0 ? `AND e.entry_type NOT IN (${excludes.map(() => "?").join(", ")})` : "";
|
|
658
|
-
// Param order: MATCH, then the NOT IN values, then LIMIT.
|
|
659
|
-
params = [ftsQuery, ...excludes, limit];
|
|
660
|
-
}
|
|
661
|
-
const sql = `
|
|
662
|
-
SELECT e.id, e.file_path AS filePath, e.entry_json, e.search_text AS searchText,
|
|
663
|
-
bm25(entries_fts, 0, 10.0, 5.0, 3.0, 2.0, 1.0) AS bm25Score
|
|
664
|
-
FROM entries_fts f
|
|
665
|
-
JOIN entries e ON e.id = f.entry_id
|
|
666
|
-
WHERE entries_fts MATCH ?
|
|
667
|
-
${filterClause}
|
|
668
|
-
ORDER BY bm25Score, e.id ASC
|
|
669
|
-
LIMIT ?
|
|
670
|
-
`;
|
|
671
|
-
try {
|
|
672
|
-
const rows = db.prepare(sql).all(...params);
|
|
673
|
-
// Guard against corrupt JSON — skip the row rather than crashing
|
|
674
|
-
const results = [];
|
|
675
|
-
for (const row of rows) {
|
|
676
|
-
let entry;
|
|
677
|
-
try {
|
|
678
|
-
entry = JSON.parse(row.entry_json);
|
|
679
|
-
}
|
|
680
|
-
catch {
|
|
681
|
-
warn(`[db] searchFts: skipping entry id=${row.id} — corrupt entry_json`);
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
results.push({
|
|
685
|
-
id: row.id,
|
|
686
|
-
filePath: row.filePath,
|
|
687
|
-
entry,
|
|
688
|
-
searchText: row.searchText,
|
|
689
|
-
bm25Score: row.bm25Score,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
return results;
|
|
693
|
-
}
|
|
694
|
-
catch (err) {
|
|
695
|
-
warn("[db] runFtsQuery failed:", err instanceof Error ? err.message : String(err));
|
|
696
|
-
return [];
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
// ── All entries ─────────────────────────────────────────────────────────────
|
|
700
|
-
function parseEntryRows(rows, context) {
|
|
701
|
-
const entries = [];
|
|
702
|
-
for (const row of rows) {
|
|
703
|
-
const mapped = rowToIndexedEntry(row, context);
|
|
704
|
-
if (mapped)
|
|
705
|
-
entries.push(mapped);
|
|
706
|
-
}
|
|
707
|
-
return entries;
|
|
708
|
-
}
|
|
709
|
-
export function getAllEntries(db, entryType, excludeTypes) {
|
|
710
|
-
let sql;
|
|
711
|
-
let params;
|
|
712
|
-
// #627 — exclude-type clause applies only on the untyped ('any') path. Empty
|
|
713
|
-
// list skips the clause (never `NOT IN ()`).
|
|
714
|
-
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
715
|
-
if (entryType && entryType !== "any") {
|
|
716
|
-
sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE entry_type = ?`;
|
|
717
|
-
params = [entryType];
|
|
718
|
-
}
|
|
719
|
-
else if (excludes.length > 0) {
|
|
720
|
-
sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE entry_type NOT IN (${excludes.map(() => "?").join(", ")})`;
|
|
721
|
-
params = [...excludes];
|
|
722
|
-
}
|
|
723
|
-
else {
|
|
724
|
-
sql = `SELECT ${ENTRY_COLUMNS} FROM entries`;
|
|
725
|
-
params = [];
|
|
726
|
-
}
|
|
727
|
-
const rows = db.prepare(sql).all(...params);
|
|
728
|
-
return parseEntryRows(rows, "getAllEntries");
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* #609 — read graph entities (normalized) for a set of entry ids. Used by the
|
|
732
|
-
* recombine pass to cluster memories by shared graph entity ("graph"
|
|
733
|
-
* relatedness source). Returns a map of `entry_id -> entity_norm[]`. Entries
|
|
734
|
-
* with no graph entities (graph extraction has not run, or the file produced
|
|
735
|
-
* no entities) are simply absent from the map — callers must fail open
|
|
736
|
-
* (fall back to tag relatedness) when the map is empty.
|
|
737
|
-
*/
|
|
738
|
-
export function getEntitiesByEntryIds(db, entryIds) {
|
|
739
|
-
const result = new Map();
|
|
740
|
-
if (entryIds.length === 0)
|
|
741
|
-
return result;
|
|
742
|
-
// #624-P1: graph_file_entities no longer carries entry_id. Re-derive the
|
|
743
|
-
// entry_id -> entity_norm[] contract by JOINing through entries on
|
|
744
|
-
// (stash_dir, file_path) -> graph_files. Chunk the IN(?) list because the
|
|
745
|
-
// recombine pass can pass 10k+ entry ids (well over the SQLite param limit).
|
|
746
|
-
for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
747
|
-
const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
748
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
749
|
-
const rows = db
|
|
750
|
-
.prepare(`SELECT e.id AS entry_id, gfe.entity_norm AS entity_norm
|
|
751
|
-
FROM entries e
|
|
752
|
-
JOIN graph_files gf
|
|
753
|
-
ON gf.stash_root = e.stash_dir AND gf.file_path = e.file_path
|
|
754
|
-
JOIN graph_file_entities gfe
|
|
755
|
-
ON gfe.stash_root = gf.stash_root
|
|
756
|
-
AND gfe.file_path = gf.file_path
|
|
757
|
-
AND gfe.body_hash = gf.body_hash
|
|
758
|
-
WHERE e.id IN (${placeholders})
|
|
759
|
-
ORDER BY e.id, gfe.entity_order`)
|
|
760
|
-
.all(...chunk);
|
|
761
|
-
for (const row of rows) {
|
|
762
|
-
const list = result.get(row.entry_id);
|
|
763
|
-
if (list)
|
|
764
|
-
list.push(row.entity_norm);
|
|
765
|
-
else
|
|
766
|
-
result.set(row.entry_id, [row.entity_norm]);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return result;
|
|
770
|
-
}
|
|
771
|
-
export function findEntryIdByRef(db, ref) {
|
|
772
|
-
const parsed = parseAssetRef(ref);
|
|
773
|
-
const nameVariants = [parsed.name];
|
|
774
|
-
if (parsed.name.endsWith(".md")) {
|
|
775
|
-
nameVariants.push(parsed.name.slice(0, -3));
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
nameVariants.push(`${parsed.name}.md`);
|
|
779
|
-
}
|
|
780
|
-
const stmt = db.prepare("SELECT id FROM entries WHERE entry_type = ? AND substr(entry_key, length(entry_key) - length(?) + 1) = ? LIMIT 1");
|
|
781
|
-
for (const name of nameVariants) {
|
|
782
|
-
const suffix = `${parsed.type}:${name}`;
|
|
783
|
-
const row = stmt.get(parsed.type, suffix, suffix);
|
|
784
|
-
if (row)
|
|
785
|
-
return row.id;
|
|
786
|
-
}
|
|
787
|
-
return undefined;
|
|
788
|
-
}
|
|
789
|
-
export function getEntryCount(db) {
|
|
790
|
-
const row = db.prepare("SELECT COUNT(*) AS cnt FROM entries").get();
|
|
791
|
-
return row.cnt;
|
|
792
|
-
}
|
|
793
|
-
export function getEmbeddableEntryCount(db) {
|
|
794
|
-
return getEntryCount(db);
|
|
795
|
-
}
|
|
796
|
-
export function getEmbeddingCount(db) {
|
|
797
|
-
const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
|
|
798
|
-
return row.cnt;
|
|
799
|
-
}
|
|
800
|
-
export function getEntryById(db, id) {
|
|
801
|
-
const row = db.prepare("SELECT file_path, entry_json FROM entries WHERE id = ?").get(id);
|
|
802
|
-
if (!row)
|
|
803
|
-
return undefined;
|
|
804
|
-
// Guard against corrupt JSON
|
|
805
|
-
let entry;
|
|
806
|
-
try {
|
|
807
|
-
entry = JSON.parse(row.entry_json);
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
warn(`[db] getEntryById: skipping entry id=${id} — corrupt entry_json`);
|
|
811
|
-
return undefined;
|
|
812
|
-
}
|
|
813
|
-
return { filePath: row.file_path, entry };
|
|
814
|
-
}
|
|
815
|
-
export function getEntriesByDir(db, dirPath) {
|
|
816
|
-
const rows = db.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries WHERE dir_path = ?`).all(dirPath);
|
|
817
|
-
return parseEntryRows(rows, "getEntriesByDir");
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Resolve a single `entries.id` by exact `file_path` (the canonical on-disk
|
|
821
|
-
* path), or `undefined` if no row matches.
|
|
822
|
-
*
|
|
823
|
-
* Lifted verbatim (WS5) from the inline `SELECT id FROM entries WHERE
|
|
824
|
-
* file_path = ? LIMIT 1` in commands/search.ts so all `entries` SQL lives in
|
|
825
|
-
* this module. The result is a plain number materialised before return —
|
|
826
|
-
* nothing lazy crosses a connection boundary.
|
|
827
|
-
*/
|
|
828
|
-
export function getEntryIdByFilePath(db, filePath) {
|
|
829
|
-
const row = db.prepare("SELECT id FROM entries WHERE file_path = ? LIMIT 1").get(filePath);
|
|
830
|
-
return row?.id;
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Set of every non-empty `entries.file_path` currently indexed (across all
|
|
834
|
-
* stashes/sources). Used by staleness detection to spot files that exist on
|
|
835
|
-
* disk but were never indexed — a clock-independent signal for newly-added
|
|
836
|
-
* assets that an mtime-vs-builtAt comparison can miss when the two clocks
|
|
837
|
-
* (filesystem vs wall-clock) are skewed within the same millisecond.
|
|
838
|
-
*/
|
|
839
|
-
export function getIndexedFilePaths(db) {
|
|
840
|
-
const rows = db
|
|
841
|
-
.prepare("SELECT DISTINCT file_path FROM entries WHERE file_path IS NOT NULL AND file_path <> ''")
|
|
842
|
-
.all();
|
|
843
|
-
return new Set(rows.map((r) => r.file_path));
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Resolve a single `entries.file_path` by primary key, or `undefined` if no
|
|
847
|
-
* row matches.
|
|
848
|
-
*
|
|
849
|
-
* Lifted verbatim (WS5) from the inline `SELECT file_path FROM entries WHERE
|
|
850
|
-
* id = ?` in commands/feedback-cli.ts. Unlike {@link getEntryById}, this does
|
|
851
|
-
* NOT parse `entry_json`, so a row with corrupt JSON still yields its path —
|
|
852
|
-
* preserving feedback-cli's pre-extraction behaviour byte-for-byte.
|
|
853
|
-
*/
|
|
854
|
-
export function getEntryFilePathById(db, id) {
|
|
855
|
-
const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(id);
|
|
856
|
-
return row?.file_path;
|
|
857
|
-
}
|
|
858
|
-
/**
|
|
859
|
-
* Fetch every `(file_path, entry_json)` row whose entry belongs to a given
|
|
860
|
-
* stash root — matched either by exact `stash_dir` OR by `file_path` prefix.
|
|
861
|
-
*
|
|
862
|
-
* Lifted verbatim (WS5) from the inline query in commands/graph.ts'
|
|
863
|
-
* `buildRefByPath`. The full result set is materialised with `.all()` before
|
|
864
|
-
* return so callers can iterate it after the connection closes (WS5
|
|
865
|
-
* connection-lifetime rule). JSON parsing stays with the caller, unchanged.
|
|
866
|
-
*/
|
|
867
|
-
export function getEntryRefRowsForStashRoot(db, stashRoot) {
|
|
868
|
-
return db
|
|
869
|
-
.prepare("SELECT file_path, entry_json FROM entries WHERE stash_dir = ? OR file_path LIKE ?")
|
|
870
|
-
.all(stashRoot, `${stashRoot}%`);
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* Get the utility score for an entry, or undefined if none exists.
|
|
874
|
-
*/
|
|
875
|
-
export function getUtilityScore(db, entryId) {
|
|
876
|
-
const row = db
|
|
877
|
-
.prepare("SELECT entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at FROM utility_scores WHERE entry_id = ?")
|
|
878
|
-
.get(entryId);
|
|
879
|
-
if (!row)
|
|
880
|
-
return undefined;
|
|
881
|
-
return {
|
|
882
|
-
entryId: row.entry_id,
|
|
883
|
-
utility: row.utility,
|
|
884
|
-
showCount: row.show_count,
|
|
885
|
-
searchCount: row.search_count,
|
|
886
|
-
selectRate: row.select_rate,
|
|
887
|
-
lastUsedAt: row.last_used_at ?? undefined,
|
|
888
|
-
updatedAt: row.updated_at,
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Batch-load utility scores for multiple entry IDs in a single query.
|
|
893
|
-
* Returns a `{ global, scoped }` pair, both Maps keyed by entry_id.
|
|
894
|
-
*
|
|
895
|
-
* When `scopeKey` is provided a second query runs against
|
|
896
|
-
* `utility_scores_scoped` and the result is returned as `scoped`.
|
|
897
|
-
* Both maps are always present; `scoped` is empty when `scopeKey` is absent.
|
|
898
|
-
*/
|
|
899
|
-
export function getUtilityScoresByIds(db, ids, scopeKey) {
|
|
900
|
-
const global = new Map();
|
|
901
|
-
const scoped = new Map();
|
|
902
|
-
if (ids.length === 0)
|
|
903
|
-
return { global, scoped };
|
|
904
|
-
// Process in chunks to stay within SQLITE_MAX_VARIABLE_NUMBER
|
|
905
|
-
for (let i = 0; i < ids.length; i += SQLITE_CHUNK_SIZE) {
|
|
906
|
-
const chunk = ids.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
907
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
908
|
-
const rows = db
|
|
909
|
-
.prepare(`SELECT entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at FROM utility_scores WHERE entry_id IN (${placeholders})`)
|
|
910
|
-
.all(...chunk);
|
|
911
|
-
for (const row of rows) {
|
|
912
|
-
global.set(row.entry_id, {
|
|
913
|
-
entryId: row.entry_id,
|
|
914
|
-
utility: row.utility,
|
|
915
|
-
showCount: row.show_count,
|
|
916
|
-
searchCount: row.search_count,
|
|
917
|
-
selectRate: row.select_rate,
|
|
918
|
-
lastUsedAt: row.last_used_at ?? undefined,
|
|
919
|
-
updatedAt: row.updated_at,
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
if (scopeKey) {
|
|
923
|
-
const scopedRows = db
|
|
924
|
-
.prepare(`SELECT entry_id, scope_key, utility, last_used_at FROM utility_scores_scoped WHERE scope_key = ? AND entry_id IN (${placeholders})`)
|
|
925
|
-
.all(scopeKey, ...chunk);
|
|
926
|
-
for (const row of scopedRows) {
|
|
927
|
-
scoped.set(row.entry_id, {
|
|
928
|
-
entryId: row.entry_id,
|
|
929
|
-
scopeKey: row.scope_key,
|
|
930
|
-
utility: row.utility,
|
|
931
|
-
lastUsedAt: row.last_used_at,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return { global, scoped };
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Insert or update a utility score for an entry.
|
|
940
|
-
*/
|
|
941
|
-
export function upsertUtilityScore(db, entryId, data) {
|
|
942
|
-
// Pre-flight FK guard (mirrors `upsertEmbedding`): when an entry is
|
|
943
|
-
// deleted between when its id is aggregated from usage_events and when
|
|
944
|
-
// this INSERT runs, the FK constraint fails and rolls back the entire
|
|
945
|
-
// finalize transaction. A cheap SELECT here turns the race into a
|
|
946
|
-
// clean skip. Returns false when the entry no longer exists.
|
|
947
|
-
const exists = db.prepare("SELECT 1 FROM entries WHERE id = ?").get(entryId);
|
|
948
|
-
if (!exists)
|
|
949
|
-
return false;
|
|
950
|
-
db.prepare(`
|
|
951
|
-
INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
|
|
952
|
-
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
953
|
-
ON CONFLICT(entry_id) DO UPDATE SET
|
|
954
|
-
utility = excluded.utility,
|
|
955
|
-
show_count = excluded.show_count,
|
|
956
|
-
search_count = excluded.search_count,
|
|
957
|
-
select_rate = excluded.select_rate,
|
|
958
|
-
last_used_at = excluded.last_used_at,
|
|
959
|
-
updated_at = datetime('now')
|
|
960
|
-
`).run(entryId, data.utility, data.showCount, data.searchCount, data.selectRate, data.lastUsedAt ?? null);
|
|
961
|
-
return true;
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* Look up a cached LLM result for the given asset_ref.
|
|
965
|
-
*
|
|
966
|
-
* Returns `undefined` when no entry exists OR when the stored body_hash
|
|
967
|
-
* doesn't match `currentBodyHash` (body has changed since the result was
|
|
968
|
-
* cached). In both cases the caller should invoke the LLM and write a new
|
|
969
|
-
* cache entry.
|
|
970
|
-
*/
|
|
971
|
-
export function getLlmCacheEntry(db, assetRef, currentBodyHash, cacheVariant = "") {
|
|
972
|
-
const row = db
|
|
973
|
-
.prepare("SELECT asset_ref, cache_variant, body_hash, result_json, updated_at FROM llm_enrichment_cache WHERE asset_ref = ? AND cache_variant = ?")
|
|
974
|
-
.get(assetRef, cacheVariant);
|
|
975
|
-
if (!row)
|
|
976
|
-
return undefined;
|
|
977
|
-
// Hash mismatch → body changed, treat as cache miss.
|
|
978
|
-
if (row.body_hash !== currentBodyHash)
|
|
979
|
-
return undefined;
|
|
980
|
-
return {
|
|
981
|
-
assetRef: row.asset_ref,
|
|
982
|
-
cacheVariant: row.cache_variant,
|
|
983
|
-
bodyHash: row.body_hash,
|
|
984
|
-
resultJson: row.result_json,
|
|
985
|
-
updatedAt: row.updated_at,
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Batched variant of {@link getLlmCacheEntry}. Fetches every cache row whose
|
|
990
|
-
* `asset_ref` is in `refs` with a single `IN (...)` query (chunked to respect
|
|
991
|
-
* SQLITE_MAX_VARIABLE_NUMBER), returning a `Map<assetRef, LlmCacheEntry>`.
|
|
992
|
-
*
|
|
993
|
-
* Unlike `getLlmCacheEntry`, this does NOT filter by body hash — callers must
|
|
994
|
-
* compare `entry.bodyHash` against the current body hash themselves. This lets
|
|
995
|
-
* the batch path issue one DB query per chunk instead of one per file.
|
|
996
|
-
*/
|
|
997
|
-
export function getLlmCacheEntriesByRefs(db, refs, cacheVariant = "") {
|
|
998
|
-
const result = new Map();
|
|
999
|
-
if (refs.length === 0)
|
|
1000
|
-
return result;
|
|
1001
|
-
for (let i = 0; i < refs.length; i += SQLITE_CHUNK_SIZE) {
|
|
1002
|
-
const chunk = refs.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
1003
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
1004
|
-
const rows = db
|
|
1005
|
-
.prepare(`SELECT asset_ref, cache_variant, body_hash, result_json, updated_at FROM llm_enrichment_cache
|
|
1006
|
-
WHERE cache_variant = ? AND asset_ref IN (${placeholders})`)
|
|
1007
|
-
.all(cacheVariant, ...chunk);
|
|
1008
|
-
for (const row of rows) {
|
|
1009
|
-
result.set(row.asset_ref, {
|
|
1010
|
-
assetRef: row.asset_ref,
|
|
1011
|
-
cacheVariant: row.cache_variant,
|
|
1012
|
-
bodyHash: row.body_hash,
|
|
1013
|
-
resultJson: row.result_json,
|
|
1014
|
-
updatedAt: row.updated_at,
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
return result;
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Insert or update a cached LLM result for the given asset_ref.
|
|
1022
|
-
*/
|
|
1023
|
-
export function upsertLlmCacheEntry(db, assetRef, bodyHash, resultJson, cacheVariant = "") {
|
|
1024
|
-
db.prepare(`INSERT INTO llm_enrichment_cache (asset_ref, cache_variant, body_hash, result_json, updated_at)
|
|
1025
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1026
|
-
ON CONFLICT(asset_ref, cache_variant) DO UPDATE SET
|
|
1027
|
-
body_hash = excluded.body_hash,
|
|
1028
|
-
result_json = excluded.result_json,
|
|
1029
|
-
updated_at = excluded.updated_at`).run(assetRef, cacheVariant, bodyHash, resultJson, Date.now());
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Delete LLM cache entries whose asset_ref is no longer present in the
|
|
1033
|
-
* `entries` table. Should be called during the cleanup phase of each index
|
|
1034
|
-
* run to prevent the cache from growing unboundedly as assets are removed.
|
|
1035
|
-
*
|
|
1036
|
-
* The join uses a LIKE match against the entries `file_path` column because
|
|
1037
|
-
* graph/memory cache refs are absolute file paths, while enrichment cache
|
|
1038
|
-
* refs are entry_key strings — we preserve any entry that still has a
|
|
1039
|
-
* corresponding row in either the entries table (by entry_key) or that
|
|
1040
|
-
* matches a live file_path.
|
|
1041
|
-
*/
|
|
1042
|
-
export function clearStaleCacheEntries(db) {
|
|
1043
|
-
bestEffort(() => {
|
|
1044
|
-
db.exec(`
|
|
1045
|
-
DELETE FROM llm_enrichment_cache
|
|
1046
|
-
WHERE asset_ref NOT IN (SELECT file_path FROM entries)
|
|
1047
|
-
AND asset_ref NOT IN (SELECT entry_key FROM entries)
|
|
1048
|
-
`);
|
|
1049
|
-
}, "llm_enrichment_cache may not exist in very old DBs opened without ensureSchema");
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Compute a stable SHA-256 hex digest of a UTF-8 string. Used as the body_hash
|
|
1053
|
-
* key in `llm_enrichment_cache`. Routed through the runtime boundary so the
|
|
1054
|
-
* SQLite layer stays free of direct runtime-specific references.
|
|
1055
|
-
*/
|
|
1056
|
-
export function computeBodyHash(body) {
|
|
1057
|
-
return sha256Hex(body);
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
|
-
* Reduce a ref to its bare `type:name` form, dropping any `origin//` prefix.
|
|
1061
|
-
*
|
|
1062
|
-
* usage_events store entry_ref inconsistently: search/show writers persist
|
|
1063
|
-
* whatever ref the result carried, which is sometimes stash-prefixed
|
|
1064
|
-
* (`origin//type:name`) and sometimes bare (`type:name`). Retrieval counting
|
|
1065
|
-
* keys on the bare form so both spellings of the same asset collapse together.
|
|
1066
|
-
*
|
|
1067
|
-
* Returns the bare form, or the original string when it cannot be parsed (best
|
|
1068
|
-
* effort — never throws so a malformed stored ref can't break counting).
|
|
1069
|
-
*/
|
|
1070
|
-
function bareRef(ref) {
|
|
1071
|
-
try {
|
|
1072
|
-
const parsed = parseAssetRef(ref);
|
|
1073
|
-
return `${parsed.type}:${parsed.name}`;
|
|
1074
|
-
}
|
|
1075
|
-
catch {
|
|
1076
|
-
return ref;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Count retrieval events for the given entry refs.
|
|
1081
|
-
*
|
|
1082
|
-
* Counts `search`, `show`, and `curate` usage events. Returns a
|
|
1083
|
-
* Map<inputRef, count> keyed by the *input* ref strings (only those with at
|
|
1084
|
-
* least one matching event appear). Used by the improve loop to find
|
|
1085
|
-
* high-retrieval assets without feedback.
|
|
1086
|
-
*
|
|
1087
|
-
* Matching is normalization-aware: each stored `entry_ref` is reduced to its
|
|
1088
|
-
* bare `type:name` form before comparison, so a stash-prefixed stored ref
|
|
1089
|
-
* (`origin//type:name`) still matches a bare input ref (`type:name`) and vice
|
|
1090
|
-
* versa. Previously the raw `entry_ref IN (...)` comparison silently dropped
|
|
1091
|
-
* roughly half the signal whenever the two spellings disagreed.
|
|
1092
|
-
*
|
|
1093
|
-
* `curate` events are included: their per-item rows are written with
|
|
1094
|
-
* entry_ref populated (see logCurateEvent), so curation is a real retrieval
|
|
1095
|
-
* signal here. Legacy summary-only curate rows with a NULL entry_ref simply
|
|
1096
|
-
* contribute nothing.
|
|
1097
|
-
*
|
|
1098
|
-
* Machine-sourced events (`source` = 'improve' or 'task') are EXCLUDED: this
|
|
1099
|
-
* count feeds salience/ranking, and pipeline probe traffic counting as demand
|
|
1100
|
-
* creates a self-reinforcing loop (meta-review 05 DRIFT-6). NULL sources
|
|
1101
|
-
* (pre-column rows) count as user demand.
|
|
1102
|
-
*/
|
|
1103
|
-
export function getRetrievalCounts(db, refs) {
|
|
1104
|
-
if (refs.length === 0)
|
|
1105
|
-
return new Map();
|
|
1106
|
-
// Map each distinct bare form back to the input ref(s) that produced it so we
|
|
1107
|
-
// can re-key DB results (grouped by bare form) onto the caller's ref strings.
|
|
1108
|
-
const bareToInputs = new Map();
|
|
1109
|
-
for (const ref of refs) {
|
|
1110
|
-
const bare = bareRef(ref);
|
|
1111
|
-
const existing = bareToInputs.get(bare);
|
|
1112
|
-
if (existing)
|
|
1113
|
-
existing.push(ref);
|
|
1114
|
-
else
|
|
1115
|
-
bareToInputs.set(bare, [ref]);
|
|
1116
|
-
}
|
|
1117
|
-
const bareForms = [...bareToInputs.keys()];
|
|
1118
|
-
// Accumulate counts per bare form across chunks before re-keying.
|
|
1119
|
-
const countsByBare = new Map();
|
|
1120
|
-
// Chunk to stay within SQLITE_MAX_VARIABLE_NUMBER (same pattern as getUtilityScoresByIds).
|
|
1121
|
-
for (let i = 0; i < bareForms.length; i += SQLITE_CHUNK_SIZE) {
|
|
1122
|
-
const chunk = bareForms.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
1123
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
1124
|
-
// Normalize the stored entry_ref to its bare form inside SQL by stripping
|
|
1125
|
-
// everything up to and including the last `//` separator. SQLite has no
|
|
1126
|
-
// rfind, but stored origins never themselves contain `//`, so a stash ref
|
|
1127
|
-
// has exactly one `//` and `substr(... instr ...)` is exact; bare refs have
|
|
1128
|
-
// no `//` and pass through unchanged.
|
|
1129
|
-
const rows = db
|
|
1130
|
-
.prepare(`SELECT
|
|
1131
|
-
CASE
|
|
1132
|
-
WHEN instr(entry_ref, '//') > 0
|
|
1133
|
-
THEN substr(entry_ref, instr(entry_ref, '//') + 2)
|
|
1134
|
-
ELSE entry_ref
|
|
1135
|
-
END AS bare_ref,
|
|
1136
|
-
COUNT(*) AS cnt
|
|
1137
|
-
FROM usage_events
|
|
1138
|
-
WHERE event_type IN ('search','show','curate')
|
|
1139
|
-
AND entry_ref IS NOT NULL
|
|
1140
|
-
AND (source IS NULL OR source NOT IN ('improve','task'))
|
|
1141
|
-
AND CASE
|
|
1142
|
-
WHEN instr(entry_ref, '//') > 0
|
|
1143
|
-
THEN substr(entry_ref, instr(entry_ref, '//') + 2)
|
|
1144
|
-
ELSE entry_ref
|
|
1145
|
-
END IN (${placeholders})
|
|
1146
|
-
GROUP BY bare_ref`)
|
|
1147
|
-
.all(...chunk);
|
|
1148
|
-
for (const r of rows) {
|
|
1149
|
-
countsByBare.set(r.bare_ref, (countsByBare.get(r.bare_ref) ?? 0) + r.cnt);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
// Re-key bare-form counts onto every input ref that maps to that bare form.
|
|
1153
|
-
const result = new Map();
|
|
1154
|
-
for (const [bare, count] of countsByBare) {
|
|
1155
|
-
for (const input of bareToInputs.get(bare) ?? []) {
|
|
1156
|
-
result.set(input, count);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return result;
|
|
1160
|
-
}
|
|
1161
|
-
/**
|
|
1162
|
-
* Apply a MemRL reward signal to a batch of entries via exponential moving
|
|
1163
|
-
* average (EMA): next = clamp(current + lr * (reward - current), 0, 1).
|
|
1164
|
-
*
|
|
1165
|
-
* Wrapped in a single transaction so all bumps succeed or fail together.
|
|
1166
|
-
* The indexer (`akm index`) will overwrite these values at next reindex run;
|
|
1167
|
-
* bumps are intentionally temporary hints between index runs, not permanent
|
|
1168
|
-
* overrides.
|
|
1169
|
-
*
|
|
1170
|
-
* When `scopeKey` is provided, also writes a scoped bump to
|
|
1171
|
-
* `utility_scores_scoped` so per-project usage signals accumulate alongside
|
|
1172
|
-
* the global ones. The global table is always updated regardless.
|
|
1173
|
-
*/
|
|
1174
|
-
export function bumpUtilityScoresBatch(db, entryIds, reward, lr = 0.1, scopeKey) {
|
|
1175
|
-
if (entryIds.length === 0)
|
|
1176
|
-
return;
|
|
1177
|
-
db.transaction(() => {
|
|
1178
|
-
const { global: scoreMap } = getUtilityScoresByIds(db, entryIds);
|
|
1179
|
-
const now = new Date().toISOString();
|
|
1180
|
-
const nowMs = Date.now();
|
|
1181
|
-
const stmt = db.prepare(`INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
|
|
1182
|
-
VALUES (?, ?, 0, 0, 0, ?, ?)
|
|
1183
|
-
ON CONFLICT(entry_id) DO UPDATE SET
|
|
1184
|
-
utility = excluded.utility,
|
|
1185
|
-
updated_at = excluded.updated_at`);
|
|
1186
|
-
// Prepare scoped upsert once outside the loop when scopeKey is present.
|
|
1187
|
-
const scopedStmt = scopeKey
|
|
1188
|
-
? db.prepare(`INSERT INTO utility_scores_scoped (entry_id, scope_key, utility, last_used_at)
|
|
1189
|
-
VALUES (?, ?, ?, ?)
|
|
1190
|
-
ON CONFLICT(entry_id, scope_key) DO UPDATE SET
|
|
1191
|
-
utility = excluded.utility,
|
|
1192
|
-
last_used_at = excluded.last_used_at`)
|
|
1193
|
-
: null;
|
|
1194
|
-
for (const entryId of entryIds) {
|
|
1195
|
-
const existing = scoreMap.get(entryId);
|
|
1196
|
-
const current = existing?.utility ?? 0;
|
|
1197
|
-
const next = Math.max(0, Math.min(1, current + lr * (reward - current)));
|
|
1198
|
-
stmt.run(entryId, next, now, now);
|
|
1199
|
-
if (scopedStmt && scopeKey) {
|
|
1200
|
-
// Retrieve the current scoped utility so we can apply the same EMA.
|
|
1201
|
-
const scopedCurrent = getScopedUtility(db, entryId, scopeKey);
|
|
1202
|
-
const scopedNext = Math.max(0, Math.min(1, scopedCurrent + lr * (reward - scopedCurrent)));
|
|
1203
|
-
scopedStmt.run(entryId, scopeKey, scopedNext, nowMs);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
})();
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Return the current utility value for a single (entry_id, scope_key) pair.
|
|
1210
|
-
* Returns 0 when no row exists yet.
|
|
1211
|
-
*/
|
|
1212
|
-
function getScopedUtility(db, entryId, scopeKey) {
|
|
1213
|
-
const row = db
|
|
1214
|
-
.prepare("SELECT utility FROM utility_scores_scoped WHERE entry_id = ? AND scope_key = ?")
|
|
1215
|
-
.get(entryId, scopeKey);
|
|
1216
|
-
return row?.utility ?? 0;
|
|
1217
|
-
}
|
|
1218
|
-
// ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
|
|
1219
|
-
/**
|
|
1220
|
-
* Return all entries that do not yet have an embedding row.
|
|
1221
|
-
* Used by the embedding phase to determine which entries need vectors generated.
|
|
1222
|
-
*/
|
|
1223
|
-
export function getAllEntriesForEmbedding(db) {
|
|
1224
|
-
return db
|
|
1225
|
-
.prepare(`
|
|
1226
|
-
SELECT e.id, e.search_text AS searchText, e.entry_key AS entryKey, e.file_path AS filePath FROM entries e
|
|
1227
|
-
WHERE NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)
|
|
1228
|
-
`)
|
|
1229
|
-
.all();
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Upsert a workflow document record for an indexed entry.
|
|
1233
|
-
* Persists the parsed workflow AST as JSON alongside a FNV-1a hash of the
|
|
1234
|
-
* source content for future incremental fast-paths.
|
|
1235
|
-
*/
|
|
1236
|
-
export function upsertWorkflowDocument(db, entryId, doc, content) {
|
|
1237
|
-
const sourceHash = computeSourceHash(content);
|
|
1238
|
-
db.prepare(`INSERT INTO workflow_documents (entry_id, schema_version, document_json, source_path, source_hash, updated_at)
|
|
1239
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
1240
|
-
ON CONFLICT(entry_id) DO UPDATE SET
|
|
1241
|
-
schema_version = excluded.schema_version,
|
|
1242
|
-
document_json = excluded.document_json,
|
|
1243
|
-
source_path = excluded.source_path,
|
|
1244
|
-
source_hash = excluded.source_hash,
|
|
1245
|
-
updated_at = excluded.updated_at`).run(entryId, doc.schemaVersion, JSON.stringify(doc), doc.source.path, sourceHash, new Date().toISOString());
|
|
1246
|
-
}
|
|
1247
|
-
/**
|
|
1248
|
-
* Compute a cheap FNV-1a hash of a buffer for source-identity tracking.
|
|
1249
|
-
* Not security-sensitive; used as an incremental fast-path skip key.
|
|
1250
|
-
*/
|
|
1251
|
-
export function computeSourceHash(content) {
|
|
1252
|
-
let hash = 0x811c9dc5;
|
|
1253
|
-
for (let i = 0; i < content.length; i++) {
|
|
1254
|
-
hash ^= content[i];
|
|
1255
|
-
hash = Math.imul(hash, 0x01000193);
|
|
1256
|
-
}
|
|
1257
|
-
return (hash >>> 0).toString(16);
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Return distinct zero-result search queries from the `usage_events` table
|
|
1261
|
-
* within the given lookback window.
|
|
1262
|
-
*
|
|
1263
|
-
* Reads from `usage_events` (event_type = 'search') where the metadata JSON
|
|
1264
|
-
* blob contains `resultCount = 0`. The `search_events` table never existed;
|
|
1265
|
-
* all errors are caught and an empty array is returned so callers never need
|
|
1266
|
-
* to guard against DB schema differences.
|
|
1267
|
-
*/
|
|
1268
|
-
export function getZeroResultSearches(db, sinceDays = 30) {
|
|
1269
|
-
const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();
|
|
1270
|
-
try {
|
|
1271
|
-
const rows = db
|
|
1272
|
-
.prepare(`SELECT DISTINCT json_extract(metadata, '$.query') AS query
|
|
1273
|
-
FROM usage_events
|
|
1274
|
-
WHERE event_type = 'search'
|
|
1275
|
-
AND created_at >= ?
|
|
1276
|
-
AND json_extract(metadata, '$.resultCount') = 0
|
|
1277
|
-
ORDER BY created_at DESC LIMIT 20`)
|
|
1278
|
-
.all(since);
|
|
1279
|
-
return rows.map((r) => r.query).filter((q) => q !== null);
|
|
1280
|
-
}
|
|
1281
|
-
catch {
|
|
1282
|
-
return []; // table may not exist in older DBs
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Look up an entry by its integer numeric id.
|
|
1287
|
-
* Returns null when no matching row is found.
|
|
1288
|
-
*/
|
|
1289
|
-
export function getEntryByRef(db, type, name) {
|
|
1290
|
-
return db.prepare("SELECT id FROM entries WHERE entry_type = ? AND entry_key = ?").get(type, `${type}:${name}`);
|
|
1291
|
-
}
|
|
1292
|
-
/**
|
|
1293
|
-
* Apply accumulated feedback counts to the utility score of an entry, persisting
|
|
1294
|
-
* the result. The bounded-step EMA policy itself (MemRL, F-5 / #386,
|
|
1295
|
-
* arXiv:2601.03192) lives in {@link computeNextUtility} (feedback/utility-policy);
|
|
1296
|
-
* this function only reads the current utility, applies the policy, and writes
|
|
1297
|
-
* the new value.
|
|
1298
|
-
*
|
|
1299
|
-
* A new entry starts at 0.5 (neutral midpoint) before the EMA step is applied.
|
|
1300
|
-
* When there is no feedback (both counts zero) the score is left untouched — no
|
|
1301
|
-
* DB write. Returns a {@link FeedbackUtilityResult} so the caller can detect a
|
|
1302
|
-
* previously high-utility asset crossing below the review threshold and escalate.
|
|
1303
|
-
*/
|
|
1304
|
-
export function applyFeedbackToUtilityScore(db, entryId, positiveCount, negativeCount) {
|
|
1305
|
-
const existing = getUtilityScore(db, entryId);
|
|
1306
|
-
const previousUtility = existing?.utility ?? 0.5;
|
|
1307
|
-
const result = computeNextUtility(previousUtility, positiveCount, negativeCount);
|
|
1308
|
-
if (positiveCount === 0 && negativeCount === 0) {
|
|
1309
|
-
return result;
|
|
1310
|
-
}
|
|
1311
|
-
const now = new Date().toISOString();
|
|
1312
|
-
db.prepare(`
|
|
1313
|
-
INSERT INTO utility_scores (entry_id, utility, show_count, search_count, select_rate, last_used_at, updated_at)
|
|
1314
|
-
VALUES (?, ?, 0, 0, 0, ?, ?)
|
|
1315
|
-
ON CONFLICT(entry_id) DO UPDATE SET
|
|
1316
|
-
utility = ?,
|
|
1317
|
-
updated_at = ?
|
|
1318
|
-
`).run(entryId, result.nextUtility, now, now, result.nextUtility, now);
|
|
1319
|
-
return result;
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Re-link detached usage_events to their current entry_ids via entry_ref.
|
|
1323
|
-
*
|
|
1324
|
-
* After a full rebuild, entry IDs change. This restores each event's link
|
|
1325
|
-
* using the stable `entry_ref` column so usage history survives a reindex.
|
|
1326
|
-
*/
|
|
1327
|
-
export function relinkUsageEvents(db) {
|
|
1328
|
-
bestEffort(() => {
|
|
1329
|
-
// Step 1: null out stale entry_ids (entry was deleted, re-keyed, etc).
|
|
1330
|
-
// Leaving them in place would let `recomputeUtilityScores` aggregate
|
|
1331
|
-
// by an entry_id that no longer exists in `entries`, then trip the FK
|
|
1332
|
-
// constraint on the utility_scores INSERT and roll back the entire
|
|
1333
|
-
// finalize transaction. Nulled rows can be re-resolved by step 2 below;
|
|
1334
|
-
// events whose entry is permanently gone simply stay null and age out
|
|
1335
|
-
// via the 90-day retention policy.
|
|
1336
|
-
db.exec(`
|
|
1337
|
-
UPDATE usage_events
|
|
1338
|
-
SET entry_id = NULL
|
|
1339
|
-
WHERE entry_id IS NOT NULL
|
|
1340
|
-
AND entry_id NOT IN (SELECT id FROM entries)
|
|
1341
|
-
`);
|
|
1342
|
-
// Step 2: re-resolve any null entry_id from entry_ref against the current
|
|
1343
|
-
// entries table, reusing the SAME canonical resolver the read path uses at
|
|
1344
|
-
// insert time (`findEntryIdByRef` → `parseAssetRef`). Resolving per DISTINCT
|
|
1345
|
-
// ref keeps this O(distinct-refs) indexed lookups instead of the previous
|
|
1346
|
-
// O(events × entries) non-indexable `substr(entry_key, …)` scan. It also
|
|
1347
|
-
// fixes a silent correctness bug: the old suffix match compared the RAW
|
|
1348
|
-
// `entry_ref`, so origin-qualified refs ("source//type:name") never matched
|
|
1349
|
-
// an `entry_key` and lost their usage history on every full rebuild.
|
|
1350
|
-
const refs = db
|
|
1351
|
-
.prepare("SELECT DISTINCT entry_ref AS ref FROM usage_events WHERE entry_id IS NULL AND entry_ref IS NOT NULL")
|
|
1352
|
-
.all();
|
|
1353
|
-
const update = db.prepare("UPDATE usage_events SET entry_id = ? WHERE entry_ref = ? AND entry_id IS NULL");
|
|
1354
|
-
const relinkTx = db.transaction(() => {
|
|
1355
|
-
for (const { ref } of refs) {
|
|
1356
|
-
let id;
|
|
1357
|
-
try {
|
|
1358
|
-
id = findEntryIdByRef(db, ref);
|
|
1359
|
-
}
|
|
1360
|
-
catch (err) {
|
|
1361
|
-
if (err instanceof Error && err.name === "UsageError")
|
|
1362
|
-
continue;
|
|
1363
|
-
throw err;
|
|
1364
|
-
}
|
|
1365
|
-
if (id !== undefined)
|
|
1366
|
-
update.run(id, ref);
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
relinkTx();
|
|
1370
|
-
}, "usage_events table may not exist yet during entry_id re-resolution");
|
|
1371
|
-
}
|
|
1372
|
-
// ── registry_index_cache helpers ─────────────────────────────────────────────
|
|
1373
|
-
// The raw SQL for the `registry_index_cache` table now lives in the storage
|
|
1374
|
-
// layer (`src/storage/repositories/registry-index-cache-repository.ts`) so the
|
|
1375
|
-
// dependency arrow points indexer → storage. These thin re-exports preserve the
|
|
1376
|
-
// previously-public symbols for any importer of this module.
|
|
1377
|
-
export { getRegistryIndexCache, upsertRegistryIndexCache, } from "../../storage/repositories/registry-index-cache-repository.js";
|
|
1378
|
-
/**
|
|
1379
|
-
* Walk indexed entries and collect a deduplicated set of tags. When
|
|
1380
|
-
* `entryType` is provided, only entries of that type contribute tags.
|
|
1381
|
-
*
|
|
1382
|
-
* Pure read; never mutates the DB. Used by `akm lessons coverage` (Phase 7A)
|
|
1383
|
-
* to compute the diff between all-asset tags and lesson tags. Tags are
|
|
1384
|
-
* normalised by trimming and lower-casing, and blank tags are dropped.
|
|
1385
|
-
*
|
|
1386
|
-
* SQL owner: this module owns ALL raw SQL against the `entries` table (WS5),
|
|
1387
|
-
* so the `lessons coverage` read lives here rather than leaking into cli.ts.
|
|
1388
|
-
* The result set is fully materialised (`.all()` then iterate) before return.
|
|
1389
|
-
*/
|
|
1390
|
-
export function collectTagSetFromEntries(db, entryType) {
|
|
1391
|
-
const tags = new Set();
|
|
1392
|
-
const stmt = entryType
|
|
1393
|
-
? db.prepare("SELECT entry_json FROM entries WHERE entry_type = ?")
|
|
1394
|
-
: db.prepare("SELECT entry_json FROM entries");
|
|
1395
|
-
const rows = (entryType ? stmt.all(entryType) : stmt.all());
|
|
1396
|
-
for (const row of rows) {
|
|
1397
|
-
let parsed;
|
|
1398
|
-
try {
|
|
1399
|
-
parsed = JSON.parse(row.entry_json);
|
|
1400
|
-
}
|
|
1401
|
-
catch {
|
|
1402
|
-
continue;
|
|
1403
|
-
}
|
|
1404
|
-
if (!Array.isArray(parsed.tags))
|
|
1405
|
-
continue;
|
|
1406
|
-
for (const tag of parsed.tags) {
|
|
1407
|
-
if (typeof tag === "string" && tag.trim().length > 0) {
|
|
1408
|
-
tags.add(tag.trim().toLowerCase());
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
return tags;
|
|
1413
|
-
}
|