akm-cli 0.9.16 → 0.9.17-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -1,1108 +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
|
-
/**
|
|
5
|
-
* Graph-extraction pass for `akm index` (#207).
|
|
6
|
-
*
|
|
7
|
-
* Walks the primary stash for `memory:` and `knowledge:` assets, asks the
|
|
8
|
-
* configured LLM to extract entities and relations from each one, and
|
|
9
|
-
* persists the result to stash-local SQLite graph tables keyed by stash root.
|
|
10
|
-
* The artifact is consumed by the search
|
|
11
|
-
* pipeline (see `src/indexer/graph/graph-boost.ts`) as a single boost component
|
|
12
|
-
* inside the existing FTS5+boosts loop — there is NO second SearchHit
|
|
13
|
-
* scorer and no parallel ranking track.
|
|
14
|
-
*
|
|
15
|
-
* Disabling — three preconditions must ALL hold for the pass to run:
|
|
16
|
-
* 1. An LLM profile must be configured (no provider = no extraction). When
|
|
17
|
-
* absent, `resolveIndexPassExecution("graph", config).runner` is
|
|
18
|
-
* `undefined` and the pass short-circuits.
|
|
19
|
-
* 2. The selected strategy's `processes.graphExtraction.enabled !== false`
|
|
20
|
-
* — the feature-gate layer (historically v1 spec §14, since superseded by
|
|
21
|
-
* the 0.8.0 profile shape). Set to `false` to block the pass at the
|
|
22
|
-
* feature-gate layer (no network call may ever issue).
|
|
23
|
-
* 3. `index.graph.llm !== false` — the per-pass opt-out layer (#208).
|
|
24
|
-
* Set to `false` to skip just this pass while leaving other passes
|
|
25
|
-
* that share the same LLM profile enabled.
|
|
26
|
-
* Toggling any one off does NOT delete the existing persisted graph — the
|
|
27
|
-
* user keeps the boost component they already have, it just stops
|
|
28
|
-
* refreshing.
|
|
29
|
-
*
|
|
30
|
-
* Locked v1 contract:
|
|
31
|
-
* - LLM access is exclusively via the frozen runner returned by
|
|
32
|
-
* `resolveIndexPassExecution("graph", config)`.
|
|
33
|
-
* - The graph rows are an indexer artifact, NOT a user-visible
|
|
34
|
-
* asset. It does not have an asset ref, does not appear in search
|
|
35
|
-
* hits, and is not addressable via `akm show`. The persisted artifact
|
|
36
|
-
* lives in indexer-owned SQLite tables (`replaceStoredGraph` /
|
|
37
|
-
* `loadStoredGraphSnapshot` in `../db/graph-db.ts`), NOT as a file on
|
|
38
|
-
* disk (R-065 #3 — this comment previously described a retired
|
|
39
|
-
* `fs.writeFile`-based storage layout) — `writeAssetToSource` is
|
|
40
|
-
* reserved for asset writes (CLAUDE.md / spec §10 step 5).
|
|
41
|
-
*/
|
|
42
|
-
import fs from "node:fs";
|
|
43
|
-
import path from "node:path";
|
|
44
|
-
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
45
|
-
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
46
|
-
import { concurrentMap } from "../../core/concurrent.js";
|
|
47
|
-
import { getIndexPassConfig, loadConfig, resolveBatchSize } from "../../core/config/config.js";
|
|
48
|
-
import { ConfigError, rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
49
|
-
import { warn, warnVerbose } from "../../core/warn.js";
|
|
50
|
-
import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
|
|
51
|
-
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
52
|
-
import * as graphExtract from "../../llm/graph-extract.js";
|
|
53
|
-
import { resolveIndexPassExecution } from "../../llm/index-passes.js";
|
|
54
|
-
import { preflightStructuredLlmRunner } from "../../llm/structured-call.js";
|
|
55
|
-
import { computeBodyHash, getLlmCacheEntriesByRefs, upsertLlmCacheEntry, } from "../../storage/repositories/index-llm-cache-repository.js";
|
|
56
|
-
import { GRAPH_SCHEMA_VERSION } from "../../storage/repositories/index-schema.js";
|
|
57
|
-
import { acknowledgeExtractionQueueEntry, enqueueGraphExtraction, loadStoredGraphSnapshot, peekExtractionQueue, replaceStoredGraph, } from "../db/graph-db.js";
|
|
58
|
-
import { walkMarkdownFiles } from "../walk/walker.js";
|
|
59
|
-
import { deduplicateGraph } from "./graph-dedup.js";
|
|
60
|
-
/** Schema version for the persisted artifact — bumps trigger a full rebuild. */
|
|
61
|
-
export const GRAPH_FILE_SCHEMA_VERSION = GRAPH_SCHEMA_VERSION;
|
|
62
|
-
function graphExecutionForContext(ctx, config) {
|
|
63
|
-
if (Object.hasOwn(ctx, "llmRunner")) {
|
|
64
|
-
return Object.freeze({ runner: ctx.llmRunner ?? undefined, notices: Object.freeze([]) });
|
|
65
|
-
}
|
|
66
|
-
return resolveIndexPassExecution("graph", config);
|
|
67
|
-
}
|
|
68
|
-
const EMPTY_QUALITY = {
|
|
69
|
-
consideredFiles: 0,
|
|
70
|
-
extractedFiles: 0,
|
|
71
|
-
entityCount: 0,
|
|
72
|
-
relationCount: 0,
|
|
73
|
-
extractionCoverage: 0,
|
|
74
|
-
density: 0,
|
|
75
|
-
};
|
|
76
|
-
const EMPTY_RESULT = {
|
|
77
|
-
considered: 0,
|
|
78
|
-
extracted: 0,
|
|
79
|
-
totalEntities: 0,
|
|
80
|
-
totalRelations: 0,
|
|
81
|
-
written: false,
|
|
82
|
-
quality: { ...EMPTY_QUALITY },
|
|
83
|
-
telemetry: {
|
|
84
|
-
cacheHits: 0,
|
|
85
|
-
cacheMisses: 0,
|
|
86
|
-
truncationCount: 0,
|
|
87
|
-
failureCount: 0,
|
|
88
|
-
retryAttempts: 0,
|
|
89
|
-
},
|
|
90
|
-
warnings: [],
|
|
91
|
-
};
|
|
92
|
-
function roundMetric(value) {
|
|
93
|
-
return Number(value.toFixed(4));
|
|
94
|
-
}
|
|
95
|
-
function computeGraphQualityTelemetry(consideredFiles, extractedFiles, entityCount, relationCount) {
|
|
96
|
-
const extractionCoverage = consideredFiles > 0 ? extractedFiles / consideredFiles : 0;
|
|
97
|
-
const maxEdges = entityCount > 1 ? (entityCount * (entityCount - 1)) / 2 : 0;
|
|
98
|
-
const density = maxEdges > 0 ? relationCount / maxEdges : 0;
|
|
99
|
-
return {
|
|
100
|
-
consideredFiles,
|
|
101
|
-
extractedFiles,
|
|
102
|
-
entityCount,
|
|
103
|
-
relationCount,
|
|
104
|
-
extractionCoverage: roundMetric(extractionCoverage),
|
|
105
|
-
density: roundMetric(density),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
export const DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES = ["memory", "knowledge"];
|
|
109
|
-
/**
|
|
110
|
-
* Max number of lazy-extraction queue rows drained per pass (#624-P3). Bounds
|
|
111
|
-
* per-run work so a large backlog is spread across runs rather than processed
|
|
112
|
-
* all at once. Generous default — the queue is normally near-empty.
|
|
113
|
-
*/
|
|
114
|
-
const GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT = 100;
|
|
115
|
-
const SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES = new Set([
|
|
116
|
-
"memory",
|
|
117
|
-
"knowledge",
|
|
118
|
-
"skill",
|
|
119
|
-
"command",
|
|
120
|
-
"agent",
|
|
121
|
-
"workflow",
|
|
122
|
-
"lesson",
|
|
123
|
-
"task",
|
|
124
|
-
]);
|
|
125
|
-
const GRAPH_CACHE_VARIANT_PREFIX = "graph-extraction";
|
|
126
|
-
function normalizeConfidence(raw) {
|
|
127
|
-
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
128
|
-
return undefined;
|
|
129
|
-
return Math.max(0, Math.min(1, raw));
|
|
130
|
-
}
|
|
131
|
-
function getGraphExtractorId(config) {
|
|
132
|
-
const fingerprint = computeBodyHash(JSON.stringify({
|
|
133
|
-
promptVersion: graphExtract.GRAPH_EXTRACT_PROMPT_VERSION,
|
|
134
|
-
model: config.model,
|
|
135
|
-
batchSize: config.batchSize,
|
|
136
|
-
includeTypes: config.includeTypes,
|
|
137
|
-
maxChunkBodyChars: 1600,
|
|
138
|
-
maxBatchBodyChars: 1600,
|
|
139
|
-
})).slice(0, 16);
|
|
140
|
-
return `${GRAPH_CACHE_VARIANT_PREFIX}:${graphExtract.GRAPH_EXTRACT_PROMPT_VERSION}:${config.model}:${fingerprint}`;
|
|
141
|
-
}
|
|
142
|
-
function buildLowQualityWarnings(quality, telemetry) {
|
|
143
|
-
const warnings = [];
|
|
144
|
-
if (quality.consideredFiles >= 5 && quality.extractionCoverage < 0.3) {
|
|
145
|
-
warnings.push(`Low graph extraction coverage (${quality.extractedFiles}/${quality.consideredFiles}, ${quality.extractionCoverage}).`);
|
|
146
|
-
}
|
|
147
|
-
if (quality.entityCount >= 8 && quality.relationCount === 0) {
|
|
148
|
-
warnings.push("Graph extraction produced many entities but no relations.");
|
|
149
|
-
}
|
|
150
|
-
if (telemetry.failureCount > 0) {
|
|
151
|
-
warnings.push(`Graph extraction encountered ${telemetry.failureCount} failed file extraction(s).`);
|
|
152
|
-
}
|
|
153
|
-
return warnings;
|
|
154
|
-
}
|
|
155
|
-
export function getGraphExtractionIncludeTypes(config) {
|
|
156
|
-
const configured = getIndexPassConfig(config.index, "graph")?.graphExtractionIncludeTypes;
|
|
157
|
-
if (!configured || configured.length === 0)
|
|
158
|
-
return [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES];
|
|
159
|
-
const out = [];
|
|
160
|
-
const seen = new Set();
|
|
161
|
-
for (const rawType of configured) {
|
|
162
|
-
const type = rawType.trim().toLowerCase();
|
|
163
|
-
if (!type || seen.has(type))
|
|
164
|
-
continue;
|
|
165
|
-
if (!SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES.has(type))
|
|
166
|
-
continue;
|
|
167
|
-
seen.add(type);
|
|
168
|
-
out.push(type);
|
|
169
|
-
}
|
|
170
|
-
return out.length > 0 ? out : [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES];
|
|
171
|
-
}
|
|
172
|
-
function validateGraphCacheShape(raw) {
|
|
173
|
-
if (!raw || typeof raw !== "object")
|
|
174
|
-
return undefined;
|
|
175
|
-
const obj = raw;
|
|
176
|
-
if (!Array.isArray(obj.entities) || !obj.entities.every((e) => typeof e === "string"))
|
|
177
|
-
return undefined;
|
|
178
|
-
if (obj.relations !== undefined &&
|
|
179
|
-
(!Array.isArray(obj.relations) ||
|
|
180
|
-
!obj.relations.every((r) => {
|
|
181
|
-
if (!r || typeof r !== "object")
|
|
182
|
-
return false;
|
|
183
|
-
const rel = r;
|
|
184
|
-
if (typeof rel.from !== "string" || typeof rel.to !== "string")
|
|
185
|
-
return false;
|
|
186
|
-
if (rel.type !== undefined && typeof rel.type !== "string")
|
|
187
|
-
return false;
|
|
188
|
-
if (rel.confidence !== undefined && (typeof rel.confidence !== "number" || !Number.isFinite(rel.confidence))) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
return true;
|
|
192
|
-
}))) {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
entities: obj.entities,
|
|
197
|
-
relations: Array.isArray(obj.relations) ? obj.relations : [],
|
|
198
|
-
confidence: normalizeConfidence(obj.confidence),
|
|
199
|
-
...(typeof obj.status === "string" ? { status: obj.status } : {}),
|
|
200
|
-
...(typeof obj.reason === "string" ? { reason: obj.reason } : {}),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function loadGraphFile(stashRoot, db) {
|
|
204
|
-
if (!db)
|
|
205
|
-
return { files: [] };
|
|
206
|
-
const graph = loadStoredGraphSnapshot(stashRoot, db);
|
|
207
|
-
if (!graph)
|
|
208
|
-
return { files: [] };
|
|
209
|
-
const out = [];
|
|
210
|
-
for (const node of graph.files) {
|
|
211
|
-
const cacheShape = validateGraphCacheShape({ entities: node.entities, relations: node.relations });
|
|
212
|
-
if (!cacheShape)
|
|
213
|
-
continue;
|
|
214
|
-
out.push({
|
|
215
|
-
path: node.path,
|
|
216
|
-
type: node.type,
|
|
217
|
-
bodyHash: node.bodyHash,
|
|
218
|
-
entities: cacheShape.entities,
|
|
219
|
-
relations: cacheShape.relations,
|
|
220
|
-
confidence: normalizeConfidence(node.confidence),
|
|
221
|
-
...(node.status ? { status: node.status } : {}),
|
|
222
|
-
...(node.reason ? { reason: node.reason } : {}),
|
|
223
|
-
...(node.extractionRunId ? { extractionRunId: node.extractionRunId } : {}),
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
files: out,
|
|
228
|
-
...(graph.telemetry ? { telemetry: graph.telemetry } : {}),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function mergeGraphNodes(previousNodes, refreshedNodes, candidatePaths) {
|
|
232
|
-
if (!candidatePaths)
|
|
233
|
-
return refreshedNodes;
|
|
234
|
-
const refreshedByPath = new Map(refreshedNodes.map((node) => [node.path, node]));
|
|
235
|
-
const merged = [];
|
|
236
|
-
for (const node of previousNodes) {
|
|
237
|
-
if (candidatePaths.has(node.path))
|
|
238
|
-
continue;
|
|
239
|
-
merged.push(node);
|
|
240
|
-
}
|
|
241
|
-
for (const node of refreshedNodes)
|
|
242
|
-
merged.push(refreshedByPath.get(node.path) ?? node);
|
|
243
|
-
return merged;
|
|
244
|
-
}
|
|
245
|
-
function reuseGraphNode(previousNodes, candidate, bodyHash) {
|
|
246
|
-
const node = previousNodes.get(candidate.absPath);
|
|
247
|
-
if (!node)
|
|
248
|
-
return undefined;
|
|
249
|
-
if (node.type !== candidate.type)
|
|
250
|
-
return undefined;
|
|
251
|
-
if (typeof node.bodyHash !== "string" || node.bodyHash.length === 0)
|
|
252
|
-
return undefined;
|
|
253
|
-
if (node.bodyHash !== bodyHash)
|
|
254
|
-
return undefined;
|
|
255
|
-
const validated = validateGraphCacheShape({ entities: node.entities, relations: node.relations });
|
|
256
|
-
if (!validated)
|
|
257
|
-
return undefined;
|
|
258
|
-
return {
|
|
259
|
-
entities: validated.entities,
|
|
260
|
-
relations: validated.relations,
|
|
261
|
-
confidence: normalizeConfidence(node.confidence),
|
|
262
|
-
...(node.status ? { status: node.status } : {}),
|
|
263
|
-
...(node.reason ? { reason: node.reason } : {}),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
function planEligibleGraphExtractions(args) {
|
|
267
|
-
const { eligible, db, reEnrich, cacheVariant, previousNodes, canReusePreviousGraph } = args;
|
|
268
|
-
const bodyHashes = eligible.map((candidate) => computeBodyHash(candidate.body));
|
|
269
|
-
const cacheEntries = db && !reEnrich
|
|
270
|
-
? getLlmCacheEntriesByRefs(db, eligible.map((candidate) => candidate.absPath), cacheVariant)
|
|
271
|
-
: new Map();
|
|
272
|
-
return eligible.map((candidate, index) => {
|
|
273
|
-
const bodyHash = bodyHashes[index] ?? "";
|
|
274
|
-
if (!reEnrich && db) {
|
|
275
|
-
const entry = cacheEntries.get(candidate.absPath);
|
|
276
|
-
if (entry?.bodyHash === bodyHash) {
|
|
277
|
-
try {
|
|
278
|
-
const cached = validateGraphCacheShape(JSON.parse(entry.resultJson));
|
|
279
|
-
if (cached)
|
|
280
|
-
return { kind: "cache-hit", candidate, bodyHash, cached, persistCache: false };
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// Corrupt cache rows are immutable model plans for this pass.
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (!reEnrich && (!db || canReusePreviousGraph)) {
|
|
288
|
-
const cached = reuseGraphNode(previousNodes, candidate, bodyHash);
|
|
289
|
-
if (cached)
|
|
290
|
-
return { kind: "cache-hit", candidate, bodyHash, cached, persistCache: Boolean(db) };
|
|
291
|
-
}
|
|
292
|
-
return { kind: "model", candidate, bodyHash };
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
function graphRecordFromCachePlan(plan) {
|
|
296
|
-
return {
|
|
297
|
-
absPath: plan.candidate.absPath,
|
|
298
|
-
type: plan.candidate.type,
|
|
299
|
-
bodyHash: plan.bodyHash,
|
|
300
|
-
entities: plan.cached.entities,
|
|
301
|
-
relations: plan.cached.relations,
|
|
302
|
-
...(plan.cached.confidence !== undefined ? { confidence: plan.cached.confidence } : {}),
|
|
303
|
-
...(plan.cached.status ? { status: plan.cached.status } : {}),
|
|
304
|
-
...(plan.cached.reason ? { reason: plan.cached.reason } : {}),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
async function extractGraphBatches(args) {
|
|
308
|
-
const { plans, batchSize, signal, db, cacheVariant, telemetry, llmRunner, lease, featureConfig, onFallback, batchState, runtimeTelemetry, onNotices, reportProgress, } = args;
|
|
309
|
-
const results = new Array(plans.length).fill(undefined);
|
|
310
|
-
const chunkStarts = [];
|
|
311
|
-
for (let start = 0; start < plans.length; start += batchSize)
|
|
312
|
-
chunkStarts.push(start);
|
|
313
|
-
let configFailure;
|
|
314
|
-
await concurrentMap(chunkStarts, async (start) => {
|
|
315
|
-
if (signal?.aborted)
|
|
316
|
-
return;
|
|
317
|
-
const chunk = plans.slice(start, start + batchSize);
|
|
318
|
-
const reportChunkProgress = () => {
|
|
319
|
-
for (let j = 0; j < chunk.length; j++) {
|
|
320
|
-
const plan = chunk[j];
|
|
321
|
-
if (plan)
|
|
322
|
-
reportProgress(plan.candidate.absPath, results[start + j]);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
for (let index = 0; index < chunk.length; index++) {
|
|
326
|
-
const plan = chunk[index];
|
|
327
|
-
if (!plan || plan.kind !== "cache-hit")
|
|
328
|
-
continue;
|
|
329
|
-
telemetry.cacheHits += 1;
|
|
330
|
-
results[start + index] = graphRecordFromCachePlan(plan);
|
|
331
|
-
if (db && plan.persistCache) {
|
|
332
|
-
upsertLlmCacheEntry(db, plan.candidate.absPath, plan.bodyHash, JSON.stringify(plan.cached), cacheVariant);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const modelPlans = chunk.filter((plan) => plan.kind === "model");
|
|
336
|
-
if (modelPlans.length === 0) {
|
|
337
|
-
reportChunkProgress();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
telemetry.cacheMisses += modelPlans.length;
|
|
341
|
-
let batchExtractions;
|
|
342
|
-
try {
|
|
343
|
-
batchExtractions = await graphExtract.extractGraphFromBodies(llmRunner, modelPlans.map((plan) => plan.candidate.body), signal, featureConfig, onFallback, {
|
|
344
|
-
batchState,
|
|
345
|
-
telemetry: runtimeTelemetry,
|
|
346
|
-
onNotices,
|
|
347
|
-
...(lease ? { lease } : {}),
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
if (error instanceof ConfigError) {
|
|
352
|
-
configFailure ??= error;
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
throw error;
|
|
356
|
-
}
|
|
357
|
-
let llmIndex = 0;
|
|
358
|
-
for (let index = 0; index < chunk.length; index++) {
|
|
359
|
-
const plan = chunk[index];
|
|
360
|
-
if (!plan || plan.kind !== "model")
|
|
361
|
-
continue;
|
|
362
|
-
const extraction = batchExtractions[llmIndex++];
|
|
363
|
-
if (!extraction)
|
|
364
|
-
continue;
|
|
365
|
-
const cacheShape = {
|
|
366
|
-
entities: extraction.entities,
|
|
367
|
-
relations: extraction.relations,
|
|
368
|
-
...(extraction.confidence !== undefined ? { confidence: extraction.confidence } : {}),
|
|
369
|
-
...(extraction.status ? { status: extraction.status } : {}),
|
|
370
|
-
...(extraction.reason ? { reason: extraction.reason } : {}),
|
|
371
|
-
};
|
|
372
|
-
if (db) {
|
|
373
|
-
upsertLlmCacheEntry(db, plan.candidate.absPath, plan.bodyHash, JSON.stringify(cacheShape), cacheVariant);
|
|
374
|
-
}
|
|
375
|
-
results[start + index] = {
|
|
376
|
-
absPath: plan.candidate.absPath,
|
|
377
|
-
type: plan.candidate.type,
|
|
378
|
-
bodyHash: plan.bodyHash,
|
|
379
|
-
...cacheShape,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
reportChunkProgress();
|
|
383
|
-
}, llmRunner.connection.concurrency ?? 1);
|
|
384
|
-
return { results, ...(configFailure ? { configFailure } : {}) };
|
|
385
|
-
}
|
|
386
|
-
function readCurrentGraphBodyHash(filePath) {
|
|
387
|
-
try {
|
|
388
|
-
const body = parseFrontmatter(fs.readFileSync(filePath, "utf8")).content.trim();
|
|
389
|
-
return body ? computeBodyHash(body) : undefined;
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
return undefined;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function planQueuedGraphExtractions(args) {
|
|
396
|
-
const { db, stashRoot, previousNodes, signal, reEnrich } = args;
|
|
397
|
-
if (!db)
|
|
398
|
-
return [];
|
|
399
|
-
return peekExtractionQueue(db, stashRoot, GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT).map((queued) => {
|
|
400
|
-
if (signal?.aborted) {
|
|
401
|
-
return {
|
|
402
|
-
kind: "deferred",
|
|
403
|
-
filePath: queued.filePath,
|
|
404
|
-
queuedBodyHash: queued.bodyHash,
|
|
405
|
-
priority: queued.priority,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
let raw;
|
|
409
|
-
try {
|
|
410
|
-
raw = fs.readFileSync(queued.filePath, "utf8");
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
return { kind: "discard", filePath: queued.filePath, queuedBodyHash: queued.bodyHash, priority: queued.priority };
|
|
414
|
-
}
|
|
415
|
-
const body = parseFrontmatter(raw).content.trim();
|
|
416
|
-
if (!body) {
|
|
417
|
-
return { kind: "discard", filePath: queued.filePath, queuedBodyHash: queued.bodyHash, priority: queued.priority };
|
|
418
|
-
}
|
|
419
|
-
const currentBodyHash = computeBodyHash(body);
|
|
420
|
-
const type = inferGraphTypeForPath(stashRoot, queued.filePath) ?? "memory";
|
|
421
|
-
if (!reEnrich && reuseGraphNode(previousNodes, { absPath: queued.filePath, type }, currentBodyHash)) {
|
|
422
|
-
return {
|
|
423
|
-
kind: "hit",
|
|
424
|
-
filePath: queued.filePath,
|
|
425
|
-
queuedBodyHash: queued.bodyHash,
|
|
426
|
-
currentBodyHash,
|
|
427
|
-
priority: queued.priority,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
return {
|
|
431
|
-
kind: "model",
|
|
432
|
-
filePath: queued.filePath,
|
|
433
|
-
queuedBodyHash: queued.bodyHash,
|
|
434
|
-
currentBodyHash,
|
|
435
|
-
priority: queued.priority,
|
|
436
|
-
};
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
async function executeQueuedGraphPlans(args) {
|
|
440
|
-
const { plans, db, stashRoot, featureConfig, signal, llmRunner, lease, onNotices } = args;
|
|
441
|
-
if (!db)
|
|
442
|
-
return { graphChanged: false, acknowledgements: [] };
|
|
443
|
-
let graphChanged = false;
|
|
444
|
-
const acknowledgements = [];
|
|
445
|
-
for (const plan of plans) {
|
|
446
|
-
if (signal?.aborted || plan.kind === "deferred")
|
|
447
|
-
break;
|
|
448
|
-
if (plan.kind === "discard") {
|
|
449
|
-
const currentBodyHash = readCurrentGraphBodyHash(plan.filePath);
|
|
450
|
-
if (currentBodyHash) {
|
|
451
|
-
enqueueGraphExtraction(db, stashRoot, plan.filePath, currentBodyHash, plan.priority);
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
acknowledgements.push({ filePath: plan.filePath, queuedBodyHash: plan.queuedBodyHash });
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
if (plan.kind === "hit") {
|
|
458
|
-
const currentBodyHash = readCurrentGraphBodyHash(plan.filePath);
|
|
459
|
-
if (currentBodyHash !== plan.currentBodyHash) {
|
|
460
|
-
if (currentBodyHash)
|
|
461
|
-
enqueueGraphExtraction(db, stashRoot, plan.filePath, currentBodyHash, plan.priority);
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
acknowledgements.push({ filePath: plan.filePath, queuedBodyHash: plan.queuedBodyHash });
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (plan.kind === "model") {
|
|
468
|
-
const outcome = await extractGraphForSingleFileRevision(db, stashRoot, plan.filePath, {
|
|
469
|
-
config: featureConfig,
|
|
470
|
-
signal,
|
|
471
|
-
llmRunner,
|
|
472
|
-
lease,
|
|
473
|
-
onNotices,
|
|
474
|
-
});
|
|
475
|
-
if (!outcome.written)
|
|
476
|
-
continue;
|
|
477
|
-
graphChanged = true;
|
|
478
|
-
const currentBodyHash = readCurrentGraphBodyHash(plan.filePath);
|
|
479
|
-
if (currentBodyHash !== outcome.bodyHash) {
|
|
480
|
-
if (currentBodyHash)
|
|
481
|
-
enqueueGraphExtraction(db, stashRoot, plan.filePath, currentBodyHash, plan.priority);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
acknowledgements.push({ filePath: plan.filePath, queuedBodyHash: plan.queuedBodyHash });
|
|
486
|
-
}
|
|
487
|
-
return { graphChanged, acknowledgements };
|
|
488
|
-
}
|
|
489
|
-
function acknowledgeQueuedGraphPlans(db, stashRoot, execution) {
|
|
490
|
-
if (!db)
|
|
491
|
-
return;
|
|
492
|
-
for (const intent of execution.acknowledgements) {
|
|
493
|
-
acknowledgeExtractionQueueEntry(db, stashRoot, intent.filePath, intent.queuedBodyHash);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Top-level entry point. Returns a no-op result when the pass is disabled.
|
|
498
|
-
*
|
|
499
|
-
* Three preconditions — ALL must hold for the pass to run:
|
|
500
|
-
*
|
|
501
|
-
* 1. **Provider configured** — an LLM profile must be selectable. Without a
|
|
502
|
-
* configured provider, `resolveIndexPassExecution("graph", config).runner`
|
|
503
|
-
* is `undefined` (the pass cannot run because there is no model to call).
|
|
504
|
-
* 2. **Feature gate** — the selected strategy's `processes.graphExtraction.enabled`
|
|
505
|
-
* (defaults to `true`). When `false`, no network call may issue regardless
|
|
506
|
-
* of per-pass settings.
|
|
507
|
-
* 3. **Per-pass gate** — `index.graph.llm` (defaults to `true`). When
|
|
508
|
-
* `false`, the indexer simply skips this pass for the current run.
|
|
509
|
-
*
|
|
510
|
-
* If any of the three is missing or `false`, this function short-circuits
|
|
511
|
-
* to an empty no-op result, leaving any existing persisted graph untouched.
|
|
512
|
-
*
|
|
513
|
-
* When `config.index.graph.graphExtractionBatchSize > 1`, eligible files are
|
|
514
|
-
* chunked into batches and each chunk is processed with a single LLM call via
|
|
515
|
-
* `extractGraphFromBodies`. Default batch size is 1 (one call per asset —
|
|
516
|
-
* preserves existing behaviour, fully opt-in).
|
|
517
|
-
*/
|
|
518
|
-
export async function runGraphExtractionPass(ctx) {
|
|
519
|
-
const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
|
|
520
|
-
const invocationOwnsRunner = Object.hasOwn(ctx, "llmRunner");
|
|
521
|
-
// Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
|
|
522
|
-
// (selected strategy's processes.graphExtraction.enabled). Defaults to
|
|
523
|
-
// enabled when the key is absent.
|
|
524
|
-
if (!invocationOwnsRunner && !isProcessEnabled("index", "graph_extraction", config))
|
|
525
|
-
return { ...EMPTY_RESULT };
|
|
526
|
-
const noticesByKey = new Map();
|
|
527
|
-
const onNotices = (notices) => {
|
|
528
|
-
for (const notice of notices)
|
|
529
|
-
noticesByKey.set(JSON.stringify(notice), notice);
|
|
530
|
-
};
|
|
531
|
-
const emptyResult = () => ({
|
|
532
|
-
...EMPTY_RESULT,
|
|
533
|
-
...(noticesByKey.size > 0 ? { notices: Object.freeze([...noticesByKey.values()]) } : {}),
|
|
534
|
-
});
|
|
535
|
-
// Gate 2 — per-pass opt-out (#208). Retain the whole frozen resolution so
|
|
536
|
-
// selection-time lowering notices cannot be separated from the runner.
|
|
537
|
-
const execution = graphExecutionForContext(ctx, config);
|
|
538
|
-
onNotices(execution.notices);
|
|
539
|
-
const llmRunner = execution.runner;
|
|
540
|
-
if (!llmRunner) {
|
|
541
|
-
const reason = getIndexPassConfig(config.index, "graph")?.enabled === false
|
|
542
|
-
? "index.graph.enabled is false"
|
|
543
|
-
: "no LLM engine is configured";
|
|
544
|
-
warnVerbose(`graph extraction: skipped because ${reason}.`);
|
|
545
|
-
return emptyResult();
|
|
546
|
-
}
|
|
547
|
-
const featureConfig = invocationOwnsRunner
|
|
548
|
-
? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
|
|
549
|
-
: config;
|
|
550
|
-
// The pass only writes to the primary (working) stash. Read-only caches
|
|
551
|
-
// (git, npm, website) are deliberately untouched — the graph artifact for
|
|
552
|
-
// those sources would be clobbered by the next sync().
|
|
553
|
-
const primary = sources[0];
|
|
554
|
-
if (!primary) {
|
|
555
|
-
warnVerbose("graph extraction: skipped because no primary stash source is available.");
|
|
556
|
-
return emptyResult();
|
|
557
|
-
}
|
|
558
|
-
const includeTypes = options.includeTypes ?? getGraphExtractionIncludeTypes(config);
|
|
559
|
-
let previousGraph = loadGraphFile(primary.path, db);
|
|
560
|
-
const previousNodes = new Map(previousGraph.files.map((node) => [node.path, node]));
|
|
561
|
-
const batchSize = resolveBatchSize(options.batchSize ?? getIndexPassConfig(config.index, "graph")?.graphExtractionBatchSize, llmRunner.connection.contextLength);
|
|
562
|
-
const extractorId = getGraphExtractorId({ model: llmRunner.connection.model, batchSize, includeTypes });
|
|
563
|
-
const cacheVariant = extractorId;
|
|
564
|
-
const canReusePreviousGraph = previousGraph.telemetry?.extractorId === extractorId;
|
|
565
|
-
const queuePlans = planQueuedGraphExtractions({
|
|
566
|
-
db,
|
|
567
|
-
stashRoot: primary.path,
|
|
568
|
-
previousNodes,
|
|
569
|
-
signal,
|
|
570
|
-
reEnrich,
|
|
571
|
-
});
|
|
572
|
-
const queuedPaths = new Set(queuePlans.map((plan) => plan.filePath));
|
|
573
|
-
let eligible = collectEligibleFiles(primary.path, includeTypes).filter((candidate) => (!options.candidatePaths || options.candidatePaths.has(candidate.absPath)) && !queuedPaths.has(candidate.absPath));
|
|
574
|
-
// P2 (#624): when topN is set and a DB is available, rank the (already
|
|
575
|
-
// candidate-filtered) eligible set by utility_scores DESC and keep only the
|
|
576
|
-
// top-N. Default (topN unset) is byte-identical to today — no ranking query
|
|
577
|
-
// is issued and the eligible set is untouched. Ranking composes WITH the
|
|
578
|
-
// candidatePaths filter: scoped-then-ranked-then-sliced.
|
|
579
|
-
if (db && options.topN != null && options.topN >= 0) {
|
|
580
|
-
eligible = rankCandidatesByUtility(db, eligible, primary.path).slice(0, options.topN);
|
|
581
|
-
}
|
|
582
|
-
const considered = eligible.length;
|
|
583
|
-
const eligiblePlans = planEligibleGraphExtractions({
|
|
584
|
-
eligible,
|
|
585
|
-
db,
|
|
586
|
-
reEnrich,
|
|
587
|
-
cacheVariant,
|
|
588
|
-
previousNodes,
|
|
589
|
-
canReusePreviousGraph,
|
|
590
|
-
});
|
|
591
|
-
const queueNeedsModel = queuePlans.some((plan) => plan.kind === "model");
|
|
592
|
-
const eligibleNeedsModel = !signal?.aborted && eligiblePlans.some((plan) => plan.kind === "model");
|
|
593
|
-
if (signal?.aborted)
|
|
594
|
-
return emptyResult();
|
|
595
|
-
// Validate exactly once iff classification found real model work. Queue
|
|
596
|
-
// acknowledgements, cache writes, and graph replacement all happen after
|
|
597
|
-
// this boundary, so a missing credential cannot partially mutate a batch.
|
|
598
|
-
const dispatchLease = queueNeedsModel || eligibleNeedsModel ? await preflightStructuredLlmRunner(llmRunner) : undefined;
|
|
599
|
-
try {
|
|
600
|
-
const queueExecution = await executeQueuedGraphPlans({
|
|
601
|
-
plans: queuePlans,
|
|
602
|
-
db,
|
|
603
|
-
stashRoot: primary.path,
|
|
604
|
-
featureConfig,
|
|
605
|
-
signal,
|
|
606
|
-
llmRunner,
|
|
607
|
-
lease: dispatchLease,
|
|
608
|
-
onNotices,
|
|
609
|
-
});
|
|
610
|
-
if (queueExecution.graphChanged) {
|
|
611
|
-
previousGraph = loadGraphFile(primary.path, db);
|
|
612
|
-
}
|
|
613
|
-
if (considered === 0) {
|
|
614
|
-
acknowledgeQueuedGraphPlans(db, primary.path, queueExecution);
|
|
615
|
-
const scoped = options.candidatePaths ? ` matching ${options.candidatePaths.size} candidate path(s)` : "";
|
|
616
|
-
warnVerbose(`graph extraction: skipped because no eligible files${scoped} were found under ${primary.path}. ` +
|
|
617
|
-
`includeTypes=${includeTypes.join(",")}`);
|
|
618
|
-
return emptyResult();
|
|
619
|
-
}
|
|
620
|
-
const nodes = [];
|
|
621
|
-
let totalEntities = 0;
|
|
622
|
-
let totalRelations = 0;
|
|
623
|
-
let processed = 0;
|
|
624
|
-
let extracted = 0;
|
|
625
|
-
onProgress?.({ processed, total: considered, extracted, totalEntities, totalRelations });
|
|
626
|
-
const reportProgress = (currentPath, result) => {
|
|
627
|
-
processed += 1;
|
|
628
|
-
if (result) {
|
|
629
|
-
if (result.entities.length > 0)
|
|
630
|
-
extracted += 1;
|
|
631
|
-
totalEntities += result.entities.length;
|
|
632
|
-
totalRelations += result.relations.length;
|
|
633
|
-
}
|
|
634
|
-
onProgress?.({
|
|
635
|
-
processed,
|
|
636
|
-
total: considered,
|
|
637
|
-
extracted,
|
|
638
|
-
totalEntities,
|
|
639
|
-
totalRelations,
|
|
640
|
-
currentPath,
|
|
641
|
-
});
|
|
642
|
-
};
|
|
643
|
-
const extractionRunId = crypto.randomUUID();
|
|
644
|
-
const telemetry = {
|
|
645
|
-
extractorId,
|
|
646
|
-
extractionRunId,
|
|
647
|
-
model: llmRunner.connection.model,
|
|
648
|
-
promptVersion: graphExtract.GRAPH_EXTRACT_PROMPT_VERSION,
|
|
649
|
-
batchSize,
|
|
650
|
-
cacheHits: 0,
|
|
651
|
-
cacheMisses: 0,
|
|
652
|
-
truncationCount: 0,
|
|
653
|
-
failureCount: 0,
|
|
654
|
-
htmlErrorCount: 0,
|
|
655
|
-
retryAttempts: 0,
|
|
656
|
-
nonArrayBatchFailures: 0,
|
|
657
|
-
};
|
|
658
|
-
const runtimeTelemetry = {
|
|
659
|
-
truncationCount: 0,
|
|
660
|
-
failureCount: 0,
|
|
661
|
-
htmlErrorCount: 0,
|
|
662
|
-
retryAttempts: 0,
|
|
663
|
-
filteredGenericEntities: 0,
|
|
664
|
-
filteredInvalidRelations: 0,
|
|
665
|
-
filteredLowConfidenceRelations: 0,
|
|
666
|
-
contextBatchRetries: 0,
|
|
667
|
-
nonArrayBatchFailures: 0,
|
|
668
|
-
};
|
|
669
|
-
const batchState = {
|
|
670
|
-
batchingDisabled: false,
|
|
671
|
-
nonArrayBatchFailures: 0,
|
|
672
|
-
};
|
|
673
|
-
warnVerbose(`graph extraction: starting for ${considered} eligible file(s) under ${primary.path}; ` +
|
|
674
|
-
`includeTypes=${includeTypes.join(",")}, batchSize=${batchSize}, concurrency=${llmRunner.connection.concurrency ?? 1}, ` +
|
|
675
|
-
`reEnrich=${reEnrich === true}, candidateScoped=${options.candidatePaths ? "true" : "false"}.`);
|
|
676
|
-
const onFallback = (evt) => {
|
|
677
|
-
warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
678
|
-
};
|
|
679
|
-
let extractionResults;
|
|
680
|
-
let configFailure;
|
|
681
|
-
if (batchSize <= 1) {
|
|
682
|
-
// ── Original per-asset path (with incremental cache) ─────────────────
|
|
683
|
-
extractionResults = await concurrentMap(eligiblePlans, async (plan) => {
|
|
684
|
-
const { candidate, bodyHash } = plan;
|
|
685
|
-
if (signal?.aborted) {
|
|
686
|
-
reportProgress(candidate.absPath, undefined);
|
|
687
|
-
return undefined;
|
|
688
|
-
}
|
|
689
|
-
let cached;
|
|
690
|
-
if (plan.kind === "cache-hit") {
|
|
691
|
-
telemetry.cacheHits += 1;
|
|
692
|
-
cached = plan.cached;
|
|
693
|
-
if (db && plan.persistCache) {
|
|
694
|
-
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
telemetry.cacheMisses += 1;
|
|
699
|
-
let extraction;
|
|
700
|
-
try {
|
|
701
|
-
extraction = await graphExtract.extractGraphFromBody(llmRunner, candidate.body, signal, featureConfig, onFallback, {
|
|
702
|
-
batchState,
|
|
703
|
-
telemetry: runtimeTelemetry,
|
|
704
|
-
onNotices,
|
|
705
|
-
...(dispatchLease ? { lease: dispatchLease } : {}),
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
catch (err) {
|
|
709
|
-
if (err instanceof ConfigError) {
|
|
710
|
-
configFailure ??= err;
|
|
711
|
-
return undefined;
|
|
712
|
-
}
|
|
713
|
-
throw err;
|
|
714
|
-
}
|
|
715
|
-
cached = {
|
|
716
|
-
entities: extraction.entities,
|
|
717
|
-
relations: extraction.relations,
|
|
718
|
-
...(extraction.confidence !== undefined ? { confidence: extraction.confidence } : {}),
|
|
719
|
-
...(extraction.status ? { status: extraction.status } : {}),
|
|
720
|
-
...(extraction.reason ? { reason: extraction.reason } : {}),
|
|
721
|
-
};
|
|
722
|
-
if (db) {
|
|
723
|
-
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const result = {
|
|
727
|
-
absPath: candidate.absPath,
|
|
728
|
-
type: candidate.type,
|
|
729
|
-
bodyHash,
|
|
730
|
-
entities: cached.entities,
|
|
731
|
-
relations: cached.relations,
|
|
732
|
-
...(cached.confidence !== undefined ? { confidence: cached.confidence } : {}),
|
|
733
|
-
...(cached.status ? { status: cached.status } : {}),
|
|
734
|
-
...(cached.reason ? { reason: cached.reason } : {}),
|
|
735
|
-
};
|
|
736
|
-
reportProgress(candidate.absPath, result);
|
|
737
|
-
return result;
|
|
738
|
-
},
|
|
739
|
-
// Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
|
|
740
|
-
// not forward `engines.<name>.concurrency`, so config cannot raise this.
|
|
741
|
-
llmRunner.connection.concurrency ?? 1);
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
const batch = await extractGraphBatches({
|
|
745
|
-
plans: eligiblePlans,
|
|
746
|
-
batchSize,
|
|
747
|
-
signal,
|
|
748
|
-
db,
|
|
749
|
-
cacheVariant,
|
|
750
|
-
telemetry,
|
|
751
|
-
llmRunner,
|
|
752
|
-
lease: dispatchLease,
|
|
753
|
-
featureConfig,
|
|
754
|
-
onFallback,
|
|
755
|
-
batchState,
|
|
756
|
-
runtimeTelemetry,
|
|
757
|
-
onNotices,
|
|
758
|
-
reportProgress,
|
|
759
|
-
});
|
|
760
|
-
extractionResults = batch.results;
|
|
761
|
-
configFailure ??= batch.configFailure;
|
|
762
|
-
}
|
|
763
|
-
if (configFailure)
|
|
764
|
-
throw configFailure;
|
|
765
|
-
acknowledgeQueuedGraphPlans(db, primary.path, queueExecution);
|
|
766
|
-
for (const result of extractionResults) {
|
|
767
|
-
if (!result)
|
|
768
|
-
continue;
|
|
769
|
-
nodes.push({
|
|
770
|
-
path: result.absPath,
|
|
771
|
-
type: result.type,
|
|
772
|
-
bodyHash: result.bodyHash,
|
|
773
|
-
entities: [...new Set(result.entities.map((entity) => entity.trim()).filter(Boolean))],
|
|
774
|
-
relations: result.relations
|
|
775
|
-
.map((r) => ({
|
|
776
|
-
from: r.from.trim(),
|
|
777
|
-
to: r.to.trim(),
|
|
778
|
-
...(r.type ? { type: r.type.trim() } : {}),
|
|
779
|
-
...(normalizeConfidence(r.confidence) !== undefined
|
|
780
|
-
? { confidence: normalizeConfidence(r.confidence) }
|
|
781
|
-
: {}),
|
|
782
|
-
}))
|
|
783
|
-
.filter((relation) => relation.from && relation.to),
|
|
784
|
-
...(normalizeConfidence(result.confidence) !== undefined
|
|
785
|
-
? { confidence: normalizeConfidence(result.confidence) }
|
|
786
|
-
: {}),
|
|
787
|
-
status: result.status ?? (result.entities.length > 0 ? "extracted" : "empty"),
|
|
788
|
-
reason: result.reason ?? (result.entities.length > 0 ? "none" : "no_graph_content"),
|
|
789
|
-
extractionRunId,
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
const queuedNodes = options.candidatePaths
|
|
793
|
-
? []
|
|
794
|
-
: previousGraph.files.filter((node) => queuePlans.some((plan) => plan.filePath === node.path && plan.kind !== "discard") &&
|
|
795
|
-
!nodes.some((candidate) => candidate.path === node.path));
|
|
796
|
-
const mergedNodes = mergeGraphNodes(previousGraph.files, [...queuedNodes, ...nodes], options.candidatePaths);
|
|
797
|
-
const assetRefs = mergedNodes.map((node) => node.path);
|
|
798
|
-
const deduped = deduplicateGraph(mergedNodes.map((node) => ({ entities: node.entities, relations: node.relations })), assetRefs);
|
|
799
|
-
telemetry.truncationCount = runtimeTelemetry.truncationCount ?? 0;
|
|
800
|
-
telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
|
|
801
|
-
telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
|
|
802
|
-
telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
|
|
803
|
-
telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
|
|
804
|
-
const qualityConsidered = mergedNodes.length;
|
|
805
|
-
const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
|
|
806
|
-
const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
807
|
-
const warnings = buildLowQualityWarnings(quality, telemetry);
|
|
808
|
-
for (const warning of warnings)
|
|
809
|
-
warnVerbose(`graph extraction quality: ${warning}`);
|
|
810
|
-
const graph = {
|
|
811
|
-
schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
|
|
812
|
-
generatedAt: new Date().toISOString(),
|
|
813
|
-
stashRoot: primary.path,
|
|
814
|
-
files: mergedNodes,
|
|
815
|
-
entities: deduped.entities,
|
|
816
|
-
relations: deduped.relations,
|
|
817
|
-
quality,
|
|
818
|
-
telemetry,
|
|
819
|
-
};
|
|
820
|
-
const written = writeGraphFile(primary.path, graph, db);
|
|
821
|
-
warnVerbose(`graph extraction: ${written ? "persisted" : "did not persist"} graph for ${primary.path}; ` +
|
|
822
|
-
`considered=${considered}, extractedThisRun=${extracted}, storedFiles=${mergedNodes.length}, ` +
|
|
823
|
-
`entities=${deduped.entities.length}, relations=${deduped.relations.length}, coverage=${quality.extractionCoverage}.`);
|
|
824
|
-
return {
|
|
825
|
-
considered,
|
|
826
|
-
extracted,
|
|
827
|
-
totalEntities,
|
|
828
|
-
totalRelations,
|
|
829
|
-
written,
|
|
830
|
-
quality,
|
|
831
|
-
telemetry,
|
|
832
|
-
warnings,
|
|
833
|
-
...(noticesByKey.size > 0 ? { notices: Object.freeze([...noticesByKey.values()]) } : {}),
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
finally {
|
|
837
|
-
if (dispatchLease)
|
|
838
|
-
disposeLoweredExecutionDispatchLease(dispatchLease);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Infer the asset type (`memory`, `knowledge`, …) for a path from the stash
|
|
843
|
-
* directory segment it lives under. Returns the matching include-type, or
|
|
844
|
-
* `undefined` when the path is not under a known graph-eligible type dir.
|
|
845
|
-
*/
|
|
846
|
-
function inferGraphTypeForPath(stashRoot, absPath) {
|
|
847
|
-
const rel = path.relative(stashRoot, absPath);
|
|
848
|
-
const firstSeg = rel.split(path.sep)[0];
|
|
849
|
-
if (!firstSeg)
|
|
850
|
-
return undefined;
|
|
851
|
-
for (const type of SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES) {
|
|
852
|
-
if (stashDirFor(type) === firstSeg)
|
|
853
|
-
return type;
|
|
854
|
-
}
|
|
855
|
-
return undefined;
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* #624-P3 — extract graph data for a SINGLE file and merge it into the stored
|
|
859
|
-
* graph WITHOUT clobbering other files' rows.
|
|
860
|
-
*
|
|
861
|
-
* Re-reads the body from disk at call time (the queued body_hash is NOT trusted
|
|
862
|
-
* blindly — the file may have been deleted or changed since enqueue) and skips
|
|
863
|
-
* silently (returns `false`) when the file is gone or empty. Resolves a frozen
|
|
864
|
-
* LLM execution via {@link resolveIndexPassExecution} (model-available guard:
|
|
865
|
-
* returns `false` when no provider is configured) UNLESS a caller supplies
|
|
866
|
-
* `opts.llmRunner` or `opts.llmOverride`. Those seams let a command reuse an
|
|
867
|
-
* invocation-owned selection or provide a test extractor without re-resolving.
|
|
868
|
-
*
|
|
869
|
-
* Returns `true` when a graph row was written for the file, `false` on any
|
|
870
|
-
* skip (missing file, empty body, unknown type, no model, or extraction error).
|
|
871
|
-
*/
|
|
872
|
-
async function extractGraphForSingleFileRevision(db, stashRoot, filePath, opts) {
|
|
873
|
-
let ownedLease;
|
|
874
|
-
try {
|
|
875
|
-
// Re-read from disk — never trust a stale queued body.
|
|
876
|
-
let raw;
|
|
877
|
-
try {
|
|
878
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
879
|
-
}
|
|
880
|
-
catch {
|
|
881
|
-
return { written: false }; // file gone / unreadable → silent skip
|
|
882
|
-
}
|
|
883
|
-
const parsed = parseFrontmatter(raw);
|
|
884
|
-
const body = parsed.content.trim();
|
|
885
|
-
if (!body)
|
|
886
|
-
return { written: false };
|
|
887
|
-
const type = inferGraphTypeForPath(stashRoot, filePath) ?? "memory";
|
|
888
|
-
const effectiveHash = computeBodyHash(body);
|
|
889
|
-
// Extract — via the injected seam, or the real per-asset path.
|
|
890
|
-
let extraction;
|
|
891
|
-
if (opts?.llmOverride) {
|
|
892
|
-
const out = await opts.llmOverride(body);
|
|
893
|
-
extraction = {
|
|
894
|
-
entities: out.entities,
|
|
895
|
-
relations: out.relations,
|
|
896
|
-
...(out.confidence !== undefined ? { confidence: out.confidence } : {}),
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
900
|
-
const config = opts?.config ?? loadConfig();
|
|
901
|
-
const invocationOwnsRunner = Object.hasOwn(opts ?? {}, "llmRunner");
|
|
902
|
-
if (!invocationOwnsRunner && !isProcessEnabled("index", "graph_extraction", config))
|
|
903
|
-
return { written: false };
|
|
904
|
-
const execution = Object.hasOwn(opts ?? {}, "llmRunner")
|
|
905
|
-
? Object.freeze({ runner: opts?.llmRunner ?? undefined, notices: Object.freeze([]) })
|
|
906
|
-
: resolveIndexPassExecution("graph", config);
|
|
907
|
-
opts?.onNotices?.(execution.notices);
|
|
908
|
-
const llmRunner = execution.runner;
|
|
909
|
-
if (!llmRunner)
|
|
910
|
-
return { written: false }; // model-available guard
|
|
911
|
-
const featureConfig = invocationOwnsRunner
|
|
912
|
-
? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
|
|
913
|
-
: config;
|
|
914
|
-
const lease = opts?.lease ?? (await preflightStructuredLlmRunner(llmRunner));
|
|
915
|
-
if (!opts?.lease)
|
|
916
|
-
ownedLease = lease;
|
|
917
|
-
const result = await graphExtract.extractGraphFromBody(llmRunner, body, opts?.signal, featureConfig, undefined, {
|
|
918
|
-
...(opts?.onNotices ? { onNotices: opts.onNotices } : {}),
|
|
919
|
-
lease,
|
|
920
|
-
});
|
|
921
|
-
extraction = {
|
|
922
|
-
entities: result.entities,
|
|
923
|
-
relations: result.relations,
|
|
924
|
-
...(result.confidence !== undefined ? { confidence: result.confidence } : {}),
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
const entities = [...new Set(extraction.entities.map((e) => e.trim()).filter(Boolean))];
|
|
928
|
-
const relations = extraction.relations
|
|
929
|
-
.map((r) => ({
|
|
930
|
-
from: r.from.trim(),
|
|
931
|
-
to: r.to.trim(),
|
|
932
|
-
...(r.type ? { type: r.type.trim() } : {}),
|
|
933
|
-
...(normalizeConfidence(r.confidence) !== undefined ? { confidence: normalizeConfidence(r.confidence) } : {}),
|
|
934
|
-
}))
|
|
935
|
-
.filter((r) => r.from && r.to);
|
|
936
|
-
const node = {
|
|
937
|
-
path: filePath,
|
|
938
|
-
type,
|
|
939
|
-
bodyHash: effectiveHash,
|
|
940
|
-
entities,
|
|
941
|
-
relations,
|
|
942
|
-
...(normalizeConfidence(extraction.confidence) !== undefined
|
|
943
|
-
? { confidence: normalizeConfidence(extraction.confidence) }
|
|
944
|
-
: {}),
|
|
945
|
-
status: entities.length > 0 ? "extracted" : "empty",
|
|
946
|
-
reason: entities.length > 0 ? "none" : "no_graph_content",
|
|
947
|
-
extractionRunId: crypto.randomUUID(),
|
|
948
|
-
};
|
|
949
|
-
// Merge with the previously-stored nodes, scoping the refresh to JUST this
|
|
950
|
-
// path so other files' rows are preserved (and graph_meta counts refresh).
|
|
951
|
-
const previousGraph = loadGraphFile(stashRoot, db);
|
|
952
|
-
const candidatePaths = new Set([filePath]);
|
|
953
|
-
const mergedNodes = mergeGraphNodes(previousGraph.files, [node], candidatePaths);
|
|
954
|
-
const assetRefs = mergedNodes.map((n) => n.path);
|
|
955
|
-
const deduped = deduplicateGraph(mergedNodes.map((n) => ({ entities: n.entities, relations: n.relations })), assetRefs);
|
|
956
|
-
const qualityExtracted = mergedNodes.filter((n) => n.status === "extracted" && n.entities.length > 0).length;
|
|
957
|
-
const quality = computeGraphQualityTelemetry(mergedNodes.length, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
958
|
-
const graph = {
|
|
959
|
-
schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
|
|
960
|
-
generatedAt: new Date().toISOString(),
|
|
961
|
-
stashRoot,
|
|
962
|
-
files: mergedNodes,
|
|
963
|
-
entities: deduped.entities,
|
|
964
|
-
relations: deduped.relations,
|
|
965
|
-
quality,
|
|
966
|
-
...(previousGraph.telemetry ? { telemetry: previousGraph.telemetry } : {}),
|
|
967
|
-
};
|
|
968
|
-
return writeGraphFile(stashRoot, graph, db) ? { written: true, bodyHash: effectiveHash } : { written: false };
|
|
969
|
-
}
|
|
970
|
-
catch (err) {
|
|
971
|
-
if (err instanceof ConfigError)
|
|
972
|
-
throw err;
|
|
973
|
-
rethrowIfTestIsolationError(err);
|
|
974
|
-
// A genuine extraction/write failure, distinct from the deliberate
|
|
975
|
-
// "nothing to do" skips above (missing file, empty body, no model). Warn
|
|
976
|
-
// so it is visible instead of looking identical to a no-op skip; the
|
|
977
|
-
// entry stays queued and is retried on the next pass.
|
|
978
|
-
warn(`graph extraction: failed to extract graph for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
979
|
-
return { written: false };
|
|
980
|
-
}
|
|
981
|
-
finally {
|
|
982
|
-
if (ownedLease)
|
|
983
|
-
disposeLoweredExecutionDispatchLease(ownedLease);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
export async function extractGraphForSingleFile(db, stashRoot, filePath, _bodyHash, opts) {
|
|
987
|
-
return (await extractGraphForSingleFileRevision(db, stashRoot, filePath, opts)).written;
|
|
988
|
-
}
|
|
989
|
-
// ── Eligible-file detection ─────────────────────────────────────────────────
|
|
990
|
-
/**
|
|
991
|
-
* Rank eligible graph-extraction candidates by their entry `utility_scores`,
|
|
992
|
-
* highest first, for the incremental high-signal-first sweep (P2 of #624).
|
|
993
|
-
*
|
|
994
|
-
* The join is READ-ONLY (`entries.file_path = candidate.absPath`, then
|
|
995
|
-
* `entries.id -> utility_scores.entry_id`) and does NOT re-couple the graph
|
|
996
|
-
* rows to `entries`. It reads the GLOBAL `utility_scores` table (not the
|
|
997
|
-
* per-scope `utility_scores_scoped`), so ranking is corpus-wide; `stashRoot`
|
|
998
|
-
* is accepted for call-site symmetry/future scoping but is not used to filter
|
|
999
|
-
* (the global table has no `stash_root` column).
|
|
1000
|
-
*
|
|
1001
|
-
* Candidates with no matching `entries` row, or an entry with no
|
|
1002
|
-
* `utility_scores` row, get an effective utility of 0 (LEFT JOIN + COALESCE)
|
|
1003
|
-
* and sort LAST — they are deprioritized, never dropped, so a `topN >= total`
|
|
1004
|
-
* slice still includes them and they remain reachable on later runs.
|
|
1005
|
-
*
|
|
1006
|
-
* Ties (equal utility) break by `file_path` ASC for deterministic output.
|
|
1007
|
-
* Returns a NEW array; the input is not mutated. SQLite's ~999 bound-parameter
|
|
1008
|
-
* cap is respected by chunking the `IN (...)` lookup at 500.
|
|
1009
|
-
*
|
|
1010
|
-
* Exported for direct unit testing.
|
|
1011
|
-
*/
|
|
1012
|
-
export function rankCandidatesByUtility(db, candidates, _stashRoot) {
|
|
1013
|
-
// Cannot rank without a DB → return the input unranked rather than throw.
|
|
1014
|
-
// Keeps the DB-less code path (reuse-from-memory) working when topN is set.
|
|
1015
|
-
if (!db || candidates.length === 0)
|
|
1016
|
-
return candidates;
|
|
1017
|
-
const utilityByPath = new Map();
|
|
1018
|
-
const CHUNK = 500;
|
|
1019
|
-
for (let start = 0; start < candidates.length; start += CHUNK) {
|
|
1020
|
-
const chunk = candidates.slice(start, start + CHUNK);
|
|
1021
|
-
const paths = chunk.map((c) => c.absPath);
|
|
1022
|
-
const placeholders = paths.map(() => "?").join(", ");
|
|
1023
|
-
const rows = db
|
|
1024
|
-
.prepare(`SELECT e.file_path AS file_path, COALESCE(MAX(u.utility), 0) AS utility
|
|
1025
|
-
FROM entries e
|
|
1026
|
-
LEFT JOIN utility_scores u ON u.entry_id = e.id
|
|
1027
|
-
WHERE e.file_path IN (${placeholders})
|
|
1028
|
-
GROUP BY e.file_path`)
|
|
1029
|
-
.all(...paths);
|
|
1030
|
-
for (const row of rows) {
|
|
1031
|
-
utilityByPath.set(row.file_path, row.utility ?? 0);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
return [...candidates].sort((a, b) => {
|
|
1035
|
-
const ua = utilityByPath.get(a.absPath) ?? 0;
|
|
1036
|
-
const ub = utilityByPath.get(b.absPath) ?? 0;
|
|
1037
|
-
if (ub !== ua)
|
|
1038
|
-
return ub - ua; // utility DESC
|
|
1039
|
-
return a.absPath < b.absPath ? -1 : a.absPath > b.absPath ? 1 : 0; // tie-break: path ASC
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Scan the primary stash for `memory:` and `knowledge:` markdown files
|
|
1044
|
-
* suitable for graph extraction. The directory layout convention is the
|
|
1045
|
-
* same one the rest of the indexer uses: `<stashRoot>/<type>/...`.
|
|
1046
|
-
*
|
|
1047
|
-
* Inferred-child memories (frontmatter `inferred: true`) are skipped — they
|
|
1048
|
-
* are already derived summaries, with no additional internal graph structure worth
|
|
1049
|
-
* extracting.
|
|
1050
|
-
*
|
|
1051
|
-
* Exported for direct unit testing.
|
|
1052
|
-
*/
|
|
1053
|
-
export function collectEligibleFiles(stashRoot, includeTypes = [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES]) {
|
|
1054
|
-
const out = [];
|
|
1055
|
-
for (const rawType of includeTypes) {
|
|
1056
|
-
const type = rawType.trim().toLowerCase();
|
|
1057
|
-
if (!SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES.has(type))
|
|
1058
|
-
continue;
|
|
1059
|
-
const stashDir = stashDirFor(type);
|
|
1060
|
-
if (!stashDir)
|
|
1061
|
-
continue;
|
|
1062
|
-
const dir = path.join(stashRoot, stashDir);
|
|
1063
|
-
if (!fs.existsSync(dir))
|
|
1064
|
-
continue;
|
|
1065
|
-
const walked = walkMarkdownFiles(dir);
|
|
1066
|
-
if (!walked.complete) {
|
|
1067
|
-
warn(`graph extraction: directory scan under ${dir} is incomplete — some files may be missing`);
|
|
1068
|
-
}
|
|
1069
|
-
for (const filePath of walked.files) {
|
|
1070
|
-
let raw;
|
|
1071
|
-
try {
|
|
1072
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
1073
|
-
}
|
|
1074
|
-
catch (err) {
|
|
1075
|
-
warn(`graph extraction: failed to read candidate file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
const parsed = parseFrontmatter(raw);
|
|
1079
|
-
// Skip inferred memory children — they are atomic and there's no
|
|
1080
|
-
// graph to extract from a single-fact body.
|
|
1081
|
-
if (type === "memory" && parsed.data.inferred === true)
|
|
1082
|
-
continue;
|
|
1083
|
-
const body = parsed.content.trim();
|
|
1084
|
-
if (!body)
|
|
1085
|
-
continue;
|
|
1086
|
-
out.push({ absPath: filePath, type, body });
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return out;
|
|
1090
|
-
}
|
|
1091
|
-
// ── Persistence ─────────────────────────────────────────────────────────────
|
|
1092
|
-
/**
|
|
1093
|
-
* Persist graph rows into the SQLite index DB.
|
|
1094
|
-
*/
|
|
1095
|
-
function writeGraphFile(stashRoot, graph, db) {
|
|
1096
|
-
if (!db) {
|
|
1097
|
-
warn("graph extraction: no database handle available; skipping graph persistence.");
|
|
1098
|
-
return false;
|
|
1099
|
-
}
|
|
1100
|
-
try {
|
|
1101
|
-
replaceStoredGraph(db, graph);
|
|
1102
|
-
return true;
|
|
1103
|
-
}
|
|
1104
|
-
catch (err) {
|
|
1105
|
-
warn(`graph extraction: failed to persist graph for ${stashRoot}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1106
|
-
return false;
|
|
1107
|
-
}
|
|
1108
|
-
}
|