akm-cli 0.9.1 → 0.9.2-alpha.2
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 +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Disabling — three preconditions must ALL hold for the pass to run:
|
|
16
16
|
* 1. An LLM profile must be configured (no provider = no extraction). When
|
|
17
|
-
* absent, `
|
|
18
|
-
* and the pass short-circuits.
|
|
17
|
+
* absent, `resolveIndexPassExecution("graph", config).runner` is
|
|
18
|
+
* `undefined` and the pass short-circuits.
|
|
19
19
|
* 2. The selected strategy's `processes.graphExtraction.enabled !== false`
|
|
20
20
|
* — the feature-gate layer (historically v1 spec §14, since superseded by
|
|
21
21
|
* the 0.8.0 profile shape). Set to `false` to block the pass at the
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
* refreshing.
|
|
29
29
|
*
|
|
30
30
|
* Locked v1 contract:
|
|
31
|
-
* - LLM access is exclusively via
|
|
31
|
+
* - LLM access is exclusively via the frozen runner returned by
|
|
32
|
+
* `resolveIndexPassExecution("graph", config)`.
|
|
32
33
|
* - The graph rows are an indexer artifact, NOT a user-visible
|
|
33
34
|
* asset. It does not have an asset ref, does not appear in search
|
|
34
35
|
* hits, and is not addressable via `akm show`. The persisted artifact
|
|
@@ -44,18 +45,26 @@ import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
|
44
45
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
45
46
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
46
47
|
import { getIndexPassConfig, loadConfig, resolveBatchSize } from "../../core/config/config.js";
|
|
47
|
-
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
48
|
+
import { ConfigError, rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
48
49
|
import { warn, warnVerbose } from "../../core/warn.js";
|
|
50
|
+
import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
|
|
49
51
|
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
50
52
|
import * as graphExtract from "../../llm/graph-extract.js";
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
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";
|
|
53
56
|
import { GRAPH_SCHEMA_VERSION } from "../../storage/repositories/index-schema.js";
|
|
54
|
-
import {
|
|
57
|
+
import { acknowledgeExtractionQueueEntry, enqueueGraphExtraction, loadStoredGraphSnapshot, peekExtractionQueue, replaceStoredGraph, } from "../db/graph-db.js";
|
|
55
58
|
import { walkMarkdownFiles } from "../walk/walker.js";
|
|
56
59
|
import { deduplicateGraph } from "./graph-dedup.js";
|
|
57
60
|
/** Schema version for the persisted artifact — bumps trigger a full rebuild. */
|
|
58
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
|
+
}
|
|
59
68
|
const EMPTY_QUALITY = {
|
|
60
69
|
consideredFiles: 0,
|
|
61
70
|
extractedFiles: 0,
|
|
@@ -254,14 +263,244 @@ function reuseGraphNode(previousNodes, candidate, bodyHash) {
|
|
|
254
263
|
...(node.reason ? { reason: node.reason } : {}),
|
|
255
264
|
};
|
|
256
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
|
+
}
|
|
257
496
|
/**
|
|
258
497
|
* Top-level entry point. Returns a no-op result when the pass is disabled.
|
|
259
498
|
*
|
|
260
499
|
* Three preconditions — ALL must hold for the pass to run:
|
|
261
500
|
*
|
|
262
501
|
* 1. **Provider configured** — an LLM profile must be selectable. Without a
|
|
263
|
-
* configured provider, `
|
|
264
|
-
* `undefined` (the pass cannot run because there is no model to call).
|
|
502
|
+
* configured provider, `resolveIndexPassExecution("graph", config).runner`
|
|
503
|
+
* is `undefined` (the pass cannot run because there is no model to call).
|
|
265
504
|
* 2. **Feature gate** — the selected strategy's `processes.graphExtraction.enabled`
|
|
266
505
|
* (defaults to `true`). When `false`, no network call may issue regardless
|
|
267
506
|
* of per-pass settings.
|
|
@@ -278,23 +517,34 @@ function reuseGraphNode(previousNodes, candidate, bodyHash) {
|
|
|
278
517
|
*/
|
|
279
518
|
export async function runGraphExtractionPass(ctx) {
|
|
280
519
|
const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
|
|
281
|
-
const
|
|
520
|
+
const invocationOwnsRunner = Object.hasOwn(ctx, "llmRunner");
|
|
282
521
|
// Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
|
|
283
522
|
// (selected strategy's processes.graphExtraction.enabled). Defaults to
|
|
284
523
|
// enabled when the key is absent.
|
|
285
|
-
if (!
|
|
524
|
+
if (!invocationOwnsRunner && !isProcessEnabled("index", "graph_extraction", config))
|
|
286
525
|
return { ...EMPTY_RESULT };
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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) {
|
|
291
541
|
const reason = getIndexPassConfig(config.index, "graph")?.enabled === false
|
|
292
542
|
? "index.graph.enabled is false"
|
|
293
543
|
: "no LLM engine is configured";
|
|
294
544
|
warnVerbose(`graph extraction: skipped because ${reason}.`);
|
|
295
|
-
return
|
|
545
|
+
return emptyResult();
|
|
296
546
|
}
|
|
297
|
-
const featureConfig =
|
|
547
|
+
const featureConfig = invocationOwnsRunner
|
|
298
548
|
? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
|
|
299
549
|
: config;
|
|
300
550
|
// The pass only writes to the primary (working) stash. Read-only caches
|
|
@@ -303,27 +553,24 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
303
553
|
const primary = sources[0];
|
|
304
554
|
if (!primary) {
|
|
305
555
|
warnVerbose("graph extraction: skipped because no primary stash source is available.");
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
// #624-P3: drain the lazy-extraction queue BEFORE the ranked sweep, highest
|
|
309
|
-
// priority first. Queued paths are extracted individually (per-file merge,
|
|
310
|
-
// other files untouched) so they are processed even when they fall outside
|
|
311
|
-
// the normal candidate set. Default (empty queue) is a byte-identical no-op:
|
|
312
|
-
// drainExtractionQueue returns [] and the loop body never runs.
|
|
313
|
-
if (db) {
|
|
314
|
-
const drained = drainExtractionQueue(db, primary.path, GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT);
|
|
315
|
-
for (const queued of drained) {
|
|
316
|
-
if (signal?.aborted)
|
|
317
|
-
break;
|
|
318
|
-
await extractGraphForSingleFile(db, primary.path, queued.filePath, queued.bodyHash, {
|
|
319
|
-
config: featureConfig,
|
|
320
|
-
signal,
|
|
321
|
-
llmConfig,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
556
|
+
return emptyResult();
|
|
324
557
|
}
|
|
325
558
|
const includeTypes = options.includeTypes ?? getGraphExtractionIncludeTypes(config);
|
|
326
|
-
let
|
|
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));
|
|
327
574
|
// P2 (#624): when topN is set and a DB is available, rank the (already
|
|
328
575
|
// candidate-filtered) eligible set by utility_scores DESC and keep only the
|
|
329
576
|
// top-N. Default (topN unset) is byte-identical to today — no ranking query
|
|
@@ -333,341 +580,263 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
333
580
|
eligible = rankCandidatesByUtility(db, eligible, primary.path).slice(0, options.topN);
|
|
334
581
|
}
|
|
335
582
|
const considered = eligible.length;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
totalEntities,
|
|
363
|
-
totalRelations,
|
|
364
|
-
currentPath,
|
|
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,
|
|
365
609
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const runtimeTelemetry = {
|
|
390
|
-
truncationCount: 0,
|
|
391
|
-
failureCount: 0,
|
|
392
|
-
htmlErrorCount: 0,
|
|
393
|
-
retryAttempts: 0,
|
|
394
|
-
filteredGenericEntities: 0,
|
|
395
|
-
filteredInvalidRelations: 0,
|
|
396
|
-
filteredLowConfidenceRelations: 0,
|
|
397
|
-
contextBatchRetries: 0,
|
|
398
|
-
nonArrayBatchFailures: 0,
|
|
399
|
-
};
|
|
400
|
-
const batchState = {
|
|
401
|
-
batchingDisabled: false,
|
|
402
|
-
nonArrayBatchFailures: 0,
|
|
403
|
-
};
|
|
404
|
-
warnVerbose(`graph extraction: starting for ${considered} eligible file(s) under ${primary.path}; ` +
|
|
405
|
-
`includeTypes=${includeTypes.join(",")}, batchSize=${batchSize}, concurrency=${llmConfig.concurrency ?? 1}, ` +
|
|
406
|
-
`reEnrich=${reEnrich === true}, candidateScoped=${options.candidatePaths ? "true" : "false"}.`);
|
|
407
|
-
const onFallback = (evt) => {
|
|
408
|
-
warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
409
|
-
};
|
|
410
|
-
let extractionResults;
|
|
411
|
-
if (batchSize <= 1) {
|
|
412
|
-
// ── Original per-asset path (with incremental cache) ─────────────────
|
|
413
|
-
extractionResults = await concurrentMap(eligible, async (candidate) => {
|
|
414
|
-
if (signal?.aborted) {
|
|
415
|
-
reportProgress(candidate.absPath, undefined);
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
const bodyHash = computeBodyHash(candidate.body);
|
|
419
|
-
let cached;
|
|
420
|
-
if (db) {
|
|
421
|
-
if (!(reEnrich ?? false)) {
|
|
422
|
-
const cacheEntry = getLlmCacheEntry(db, candidate.absPath, bodyHash, cacheVariant);
|
|
423
|
-
if (cacheEntry) {
|
|
424
|
-
try {
|
|
425
|
-
cached = validateGraphCacheShape(JSON.parse(cacheEntry.resultJson));
|
|
426
|
-
if (cached)
|
|
427
|
-
telemetry.cacheHits += 1;
|
|
428
|
-
}
|
|
429
|
-
catch {
|
|
430
|
-
cached = undefined;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
else if (!(reEnrich ?? false)) {
|
|
436
|
-
// No DB — best-effort reuse from the previous in-memory graph.
|
|
437
|
-
cached = reuseGraphNode(previousNodes, candidate, bodyHash);
|
|
438
|
-
}
|
|
439
|
-
if (!cached && !(reEnrich ?? false) && canReusePreviousGraph) {
|
|
440
|
-
const reused = reuseGraphNode(previousNodes, candidate, bodyHash);
|
|
441
|
-
if (reused) {
|
|
442
|
-
cached = reused;
|
|
443
|
-
if (db) {
|
|
444
|
-
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(reused), cacheVariant);
|
|
445
|
-
}
|
|
446
|
-
telemetry.cacheHits += 1;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (!cached) {
|
|
450
|
-
telemetry.cacheMisses += 1;
|
|
451
|
-
const extraction = await graphExtract.extractGraphFromBody(llmConfig, candidate.body, signal, featureConfig, onFallback, { batchState, telemetry: runtimeTelemetry });
|
|
452
|
-
cached = {
|
|
453
|
-
entities: extraction.entities,
|
|
454
|
-
relations: extraction.relations,
|
|
455
|
-
...(extraction.confidence !== undefined ? { confidence: extraction.confidence } : {}),
|
|
456
|
-
...(extraction.status ? { status: extraction.status } : {}),
|
|
457
|
-
...(extraction.reason ? { reason: extraction.reason } : {}),
|
|
458
|
-
};
|
|
459
|
-
if (db) {
|
|
460
|
-
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
461
|
-
}
|
|
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;
|
|
462
633
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
...(cached.status ? { status: cached.status } : {}),
|
|
471
|
-
...(cached.reason ? { reason: cached.reason } : {}),
|
|
472
|
-
};
|
|
473
|
-
reportProgress(candidate.absPath, result);
|
|
474
|
-
return result;
|
|
475
|
-
},
|
|
476
|
-
// Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
|
|
477
|
-
// not forward `engines.<name>.concurrency`, so config cannot raise this.
|
|
478
|
-
llmConfig.concurrency ?? 1);
|
|
479
|
-
}
|
|
480
|
-
else {
|
|
481
|
-
// ── Batched path (with incremental cache) ────────────────────────────
|
|
482
|
-
// Chunk eligible files into groups of `batchSize` and call
|
|
483
|
-
// `extractGraphFromBodies` once per chunk. Cache hits are resolved
|
|
484
|
-
// before chunking so they don't consume LLM tokens in the batch call.
|
|
485
|
-
const rawResults = new Array(eligible.length).fill(undefined);
|
|
486
|
-
const chunkStarts = [];
|
|
487
|
-
for (let start = 0; start < eligible.length; start += batchSize)
|
|
488
|
-
chunkStarts.push(start);
|
|
489
|
-
await concurrentMap(chunkStarts, async (start) => {
|
|
490
|
-
if (signal?.aborted)
|
|
491
|
-
return;
|
|
492
|
-
const chunk = eligible.slice(start, start + batchSize);
|
|
493
|
-
const reportChunkProgress = () => {
|
|
494
|
-
for (let j = 0; j < chunk.length; j++) {
|
|
495
|
-
const candidate = chunk[j];
|
|
496
|
-
if (!candidate)
|
|
497
|
-
continue;
|
|
498
|
-
reportProgress(candidate.absPath, rawResults[start + j]);
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
// Pre-resolve cache hits for this chunk; track which positions need LLM.
|
|
502
|
-
const bodyHashes = chunk.map((c) => computeBodyHash(c.body));
|
|
503
|
-
// Batch the cache lookup: one IN(...) query for the whole chunk instead
|
|
504
|
-
// of N individual SELECTs. The map covers every ref in this chunk that
|
|
505
|
-
// has any cached row; the per-position hash check happens below.
|
|
506
|
-
const chunkCache = db && !reEnrich
|
|
507
|
-
? getLlmCacheEntriesByRefs(db, chunk.map((c) => c.absPath), cacheVariant)
|
|
508
|
-
: new Map();
|
|
509
|
-
const needsLlm = chunk.map((c, j) => {
|
|
510
|
-
if (!db || reEnrich)
|
|
511
|
-
return true;
|
|
512
|
-
const cached = chunkCache.get(c.absPath);
|
|
513
|
-
// Hash mismatch → body changed, treat as cache miss.
|
|
514
|
-
if (!cached || cached.bodyHash !== (bodyHashes[j] ?? ""))
|
|
515
|
-
return true;
|
|
516
|
-
try {
|
|
517
|
-
const parsed = validateGraphCacheShape(JSON.parse(cached.resultJson));
|
|
518
|
-
if (!parsed)
|
|
519
|
-
return true;
|
|
520
|
-
telemetry.cacheHits += 1;
|
|
521
|
-
rawResults[start + j] = {
|
|
522
|
-
absPath: c.absPath,
|
|
523
|
-
type: c.type,
|
|
524
|
-
bodyHash: bodyHashes[j] ?? "",
|
|
525
|
-
entities: parsed.entities,
|
|
526
|
-
relations: parsed.relations,
|
|
527
|
-
...(parsed.confidence !== undefined ? { confidence: parsed.confidence } : {}),
|
|
528
|
-
...(parsed.status ? { status: parsed.status } : {}),
|
|
529
|
-
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
530
|
-
};
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
634
|
+
onProgress?.({
|
|
635
|
+
processed,
|
|
636
|
+
total: considered,
|
|
637
|
+
extracted,
|
|
638
|
+
totalEntities,
|
|
639
|
+
totalRelations,
|
|
640
|
+
currentPath,
|
|
536
641
|
});
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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") {
|
|
549
691
|
telemetry.cacheHits += 1;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
bodyHash: bodyHashes[j] ?? "",
|
|
554
|
-
entities: reused.entities,
|
|
555
|
-
relations: reused.relations,
|
|
556
|
-
...(reused.confidence !== undefined ? { confidence: reused.confidence } : {}),
|
|
557
|
-
...(reused.status ? { status: reused.status } : {}),
|
|
558
|
-
...(reused.reason ? { reason: reused.reason } : {}),
|
|
559
|
-
};
|
|
560
|
-
if (db) {
|
|
561
|
-
upsertLlmCacheEntry(db, candidate.absPath, bodyHashes[j] ?? "", JSON.stringify(reused), cacheVariant);
|
|
692
|
+
cached = plan.cached;
|
|
693
|
+
if (db && plan.persistCache) {
|
|
694
|
+
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
562
695
|
}
|
|
563
|
-
needsLlm[j] = false;
|
|
564
696
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
continue;
|
|
585
|
-
if (db) {
|
|
586
|
-
upsertLlmCacheEntry(db, candidate.absPath, bodyHashes[j] ?? "", JSON.stringify({
|
|
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 = {
|
|
587
716
|
entities: extraction.entities,
|
|
588
717
|
relations: extraction.relations,
|
|
589
718
|
...(extraction.confidence !== undefined ? { confidence: extraction.confidence } : {}),
|
|
590
719
|
...(extraction.status ? { status: extraction.status } : {}),
|
|
591
720
|
...(extraction.reason ? { reason: extraction.reason } : {}),
|
|
592
|
-
}
|
|
721
|
+
};
|
|
722
|
+
if (db) {
|
|
723
|
+
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
724
|
+
}
|
|
593
725
|
}
|
|
594
|
-
|
|
726
|
+
const result = {
|
|
595
727
|
absPath: candidate.absPath,
|
|
596
728
|
type: candidate.type,
|
|
597
|
-
bodyHash
|
|
598
|
-
entities:
|
|
599
|
-
relations:
|
|
600
|
-
...(
|
|
601
|
-
...(
|
|
602
|
-
...(
|
|
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 } : {}),
|
|
603
735
|
};
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
+
};
|
|
608
835
|
}
|
|
609
|
-
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
nodes.push({
|
|
613
|
-
path: result.absPath,
|
|
614
|
-
type: result.type,
|
|
615
|
-
bodyHash: result.bodyHash,
|
|
616
|
-
entities: [...new Set(result.entities.map((entity) => entity.trim()).filter(Boolean))],
|
|
617
|
-
relations: result.relations
|
|
618
|
-
.map((r) => ({
|
|
619
|
-
from: r.from.trim(),
|
|
620
|
-
to: r.to.trim(),
|
|
621
|
-
...(r.type ? { type: r.type.trim() } : {}),
|
|
622
|
-
...(normalizeConfidence(r.confidence) !== undefined ? { confidence: normalizeConfidence(r.confidence) } : {}),
|
|
623
|
-
}))
|
|
624
|
-
.filter((relation) => relation.from && relation.to),
|
|
625
|
-
...(normalizeConfidence(result.confidence) !== undefined
|
|
626
|
-
? { confidence: normalizeConfidence(result.confidence) }
|
|
627
|
-
: {}),
|
|
628
|
-
status: result.status ?? (result.entities.length > 0 ? "extracted" : "empty"),
|
|
629
|
-
reason: result.reason ?? (result.entities.length > 0 ? "none" : "no_graph_content"),
|
|
630
|
-
extractionRunId,
|
|
631
|
-
});
|
|
836
|
+
finally {
|
|
837
|
+
if (dispatchLease)
|
|
838
|
+
disposeLoweredExecutionDispatchLease(dispatchLease);
|
|
632
839
|
}
|
|
633
|
-
const mergedNodes = mergeGraphNodes(previousGraph.files, nodes, options.candidatePaths);
|
|
634
|
-
const assetRefs = mergedNodes.map((node) => node.path);
|
|
635
|
-
const deduped = deduplicateGraph(mergedNodes.map((node) => ({ entities: node.entities, relations: node.relations })), assetRefs);
|
|
636
|
-
telemetry.truncationCount = runtimeTelemetry.truncationCount ?? 0;
|
|
637
|
-
telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
|
|
638
|
-
telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
|
|
639
|
-
telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
|
|
640
|
-
telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
|
|
641
|
-
const qualityConsidered = mergedNodes.length;
|
|
642
|
-
const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
|
|
643
|
-
const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
644
|
-
const warnings = buildLowQualityWarnings(quality, telemetry);
|
|
645
|
-
for (const warning of warnings)
|
|
646
|
-
warnVerbose(`graph extraction quality: ${warning}`);
|
|
647
|
-
const graph = {
|
|
648
|
-
schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
|
|
649
|
-
generatedAt: new Date().toISOString(),
|
|
650
|
-
stashRoot: primary.path,
|
|
651
|
-
files: mergedNodes,
|
|
652
|
-
entities: deduped.entities,
|
|
653
|
-
relations: deduped.relations,
|
|
654
|
-
quality,
|
|
655
|
-
telemetry,
|
|
656
|
-
};
|
|
657
|
-
const written = writeGraphFile(primary.path, graph, db);
|
|
658
|
-
warnVerbose(`graph extraction: ${written ? "persisted" : "did not persist"} graph for ${primary.path}; ` +
|
|
659
|
-
`considered=${considered}, extractedThisRun=${extracted}, storedFiles=${mergedNodes.length}, ` +
|
|
660
|
-
`entities=${deduped.entities.length}, relations=${deduped.relations.length}, coverage=${quality.extractionCoverage}.`);
|
|
661
|
-
return {
|
|
662
|
-
considered,
|
|
663
|
-
extracted,
|
|
664
|
-
totalEntities,
|
|
665
|
-
totalRelations,
|
|
666
|
-
written,
|
|
667
|
-
quality,
|
|
668
|
-
telemetry,
|
|
669
|
-
warnings,
|
|
670
|
-
};
|
|
671
840
|
}
|
|
672
841
|
/**
|
|
673
842
|
* Infer the asset type (`memory`, `knowledge`, …) for a path from the stash
|
|
@@ -691,16 +860,17 @@ function inferGraphTypeForPath(stashRoot, absPath) {
|
|
|
691
860
|
*
|
|
692
861
|
* Re-reads the body from disk at call time (the queued body_hash is NOT trusted
|
|
693
862
|
* blindly — the file may have been deleted or changed since enqueue) and skips
|
|
694
|
-
* silently (returns `false`) when the file is gone or empty. Resolves
|
|
695
|
-
* via {@link
|
|
696
|
-
* no provider is configured) UNLESS
|
|
697
|
-
*
|
|
698
|
-
*
|
|
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.
|
|
699
868
|
*
|
|
700
869
|
* Returns `true` when a graph row was written for the file, `false` on any
|
|
701
870
|
* skip (missing file, empty body, unknown type, no model, or extraction error).
|
|
702
871
|
*/
|
|
703
|
-
|
|
872
|
+
async function extractGraphForSingleFileRevision(db, stashRoot, filePath, opts) {
|
|
873
|
+
let ownedLease;
|
|
704
874
|
try {
|
|
705
875
|
// Re-read from disk — never trust a stale queued body.
|
|
706
876
|
let raw;
|
|
@@ -708,14 +878,14 @@ export async function extractGraphForSingleFile(db, stashRoot, filePath, bodyHas
|
|
|
708
878
|
raw = fs.readFileSync(filePath, "utf8");
|
|
709
879
|
}
|
|
710
880
|
catch {
|
|
711
|
-
return false; // file gone / unreadable → silent skip
|
|
881
|
+
return { written: false }; // file gone / unreadable → silent skip
|
|
712
882
|
}
|
|
713
883
|
const parsed = parseFrontmatter(raw);
|
|
714
884
|
const body = parsed.content.trim();
|
|
715
885
|
if (!body)
|
|
716
|
-
return false;
|
|
886
|
+
return { written: false };
|
|
717
887
|
const type = inferGraphTypeForPath(stashRoot, filePath) ?? "memory";
|
|
718
|
-
const effectiveHash =
|
|
888
|
+
const effectiveHash = computeBodyHash(body);
|
|
719
889
|
// Extract — via the injected seam, or the real per-asset path.
|
|
720
890
|
let extraction;
|
|
721
891
|
if (opts?.llmOverride) {
|
|
@@ -728,16 +898,26 @@ export async function extractGraphForSingleFile(db, stashRoot, filePath, bodyHas
|
|
|
728
898
|
}
|
|
729
899
|
else {
|
|
730
900
|
const config = opts?.config ?? loadConfig();
|
|
731
|
-
const
|
|
732
|
-
if (!
|
|
733
|
-
return false;
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
|
738
912
|
? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
|
|
739
913
|
: config;
|
|
740
|
-
const
|
|
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
|
+
});
|
|
741
921
|
extraction = {
|
|
742
922
|
entities: result.entities,
|
|
743
923
|
relations: result.relations,
|
|
@@ -785,13 +965,22 @@ export async function extractGraphForSingleFile(db, stashRoot, filePath, bodyHas
|
|
|
785
965
|
quality,
|
|
786
966
|
...(previousGraph.telemetry ? { telemetry: previousGraph.telemetry } : {}),
|
|
787
967
|
};
|
|
788
|
-
return writeGraphFile(stashRoot, graph, db);
|
|
968
|
+
return writeGraphFile(stashRoot, graph, db) ? { written: true, bodyHash: effectiveHash } : { written: false };
|
|
789
969
|
}
|
|
790
970
|
catch (err) {
|
|
971
|
+
if (err instanceof ConfigError)
|
|
972
|
+
throw err;
|
|
791
973
|
rethrowIfTestIsolationError(err);
|
|
792
|
-
return false;
|
|
974
|
+
return { written: false };
|
|
975
|
+
}
|
|
976
|
+
finally {
|
|
977
|
+
if (ownedLease)
|
|
978
|
+
disposeLoweredExecutionDispatchLease(ownedLease);
|
|
793
979
|
}
|
|
794
980
|
}
|
|
981
|
+
export async function extractGraphForSingleFile(db, stashRoot, filePath, _bodyHash, opts) {
|
|
982
|
+
return (await extractGraphForSingleFileRevision(db, stashRoot, filePath, opts)).written;
|
|
983
|
+
}
|
|
795
984
|
// ── Eligible-file detection ─────────────────────────────────────────────────
|
|
796
985
|
/**
|
|
797
986
|
* Rank eligible graph-extraction candidates by their entry `utility_scores`,
|