akm-cli 0.7.5 → 0.8.0-rc.11
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/{.github/CHANGELOG.md → CHANGELOG.md} +192 -2
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2569 -1449
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1075 -77
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1091 -73
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +148 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -981
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +178 -256
- package/dist/indexer/db.js +975 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +523 -301
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +214 -80
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +77 -124
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +77 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -320
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +300 -257
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -516
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1092
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +138 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +77 -92
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +30 -12
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -328
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Auto-index: silently run an incremental `akm index` when the local index
|
|
3
6
|
* is stale or absent, so that `search`, `show`, and `feedback` always operate
|
|
@@ -9,9 +12,67 @@
|
|
|
9
12
|
* behind a single entry point.
|
|
10
13
|
*/
|
|
11
14
|
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { ASSET_SPECS, TYPE_DIRS } from "../core/asset-spec";
|
|
12
17
|
import { getDbPath } from "../core/paths";
|
|
13
18
|
import { warn } from "../core/warn";
|
|
14
19
|
import { closeDatabase, getEntryCount, getMeta, openExistingDatabase } from "./db";
|
|
20
|
+
function getIndexableFiles(root, spec) {
|
|
21
|
+
if (!fs.existsSync(root))
|
|
22
|
+
return [];
|
|
23
|
+
const files = [];
|
|
24
|
+
const stack = [root];
|
|
25
|
+
while (stack.length > 0) {
|
|
26
|
+
const current = stack.pop();
|
|
27
|
+
if (!current)
|
|
28
|
+
continue;
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry.name === ".stash.json")
|
|
38
|
+
continue;
|
|
39
|
+
const fullPath = path.join(current, entry.name);
|
|
40
|
+
if (entry.isSymbolicLink())
|
|
41
|
+
continue;
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
if (entry.name.startsWith("."))
|
|
44
|
+
continue;
|
|
45
|
+
stack.push(fullPath);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (entry.isFile() && spec.isRelevantFile(entry.name)) {
|
|
49
|
+
files.push(fullPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
function hasNewerIndexableFiles(stashDir, builtAt) {
|
|
56
|
+
if (!builtAt)
|
|
57
|
+
return true;
|
|
58
|
+
const builtAtMs = new Date(builtAt).getTime();
|
|
59
|
+
if (!Number.isFinite(builtAtMs))
|
|
60
|
+
return true;
|
|
61
|
+
for (const [type, spec] of Object.entries(ASSET_SPECS)) {
|
|
62
|
+
const typeRoot = path.join(stashDir, TYPE_DIRS[type] ?? spec.stashDir);
|
|
63
|
+
const files = getIndexableFiles(typeRoot, spec);
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.statSync(file).mtimeMs > builtAtMs)
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
15
76
|
/**
|
|
16
77
|
* Check whether the local index is stale relative to the given stash directory.
|
|
17
78
|
* Returns `true` when the index is missing, empty, or was built against a
|
|
@@ -27,6 +88,9 @@ export function isIndexStale(stashDir) {
|
|
|
27
88
|
const entryCount = getEntryCount(db);
|
|
28
89
|
if (entryCount === 0)
|
|
29
90
|
return true;
|
|
91
|
+
const builtAt = getMeta(db, "builtAt");
|
|
92
|
+
if (hasNewerIndexableFiles(stashDir, builtAt))
|
|
93
|
+
return true;
|
|
30
94
|
const storedStashDir = getMeta(db, "stashDir");
|
|
31
95
|
if (storedStashDir !== stashDir) {
|
|
32
96
|
// Check if the incoming stashDir appears in the stored stashDirs array
|
|
@@ -1,24 +1,43 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { loadStoredGraphMeta, loadStoredGraphSnapshot } from "./graph-db";
|
|
5
|
+
function normalizeGraphName(value) {
|
|
6
|
+
return value.trim().toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
let cachedParsedGraph;
|
|
1
9
|
/**
|
|
2
|
-
*
|
|
10
|
+
* Clear the module-level parsed-graph cache.
|
|
3
11
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
12
|
+
* The cache keeps the most recently parsed `ParsedGraphContext` keyed by
|
|
13
|
+
* (stashPath, generatedAt) tuples so back-to-back search invocations within
|
|
14
|
+
* the same process don't re-read the SQLite snapshot from disk. The cache
|
|
15
|
+
* persists across calls — which is the desired behaviour in production but
|
|
16
|
+
* pathological for tests that swap the underlying stash directory between
|
|
17
|
+
* test cases without bumping `generatedAt`. Such tests can observe stale
|
|
18
|
+
* graph nodes from a previous test's stash.
|
|
9
19
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* graph contribute for this (query, entry) pair?".
|
|
16
|
-
* - Missing/stale/unparseable `graph.json` → boost is `0`. The pipeline
|
|
17
|
-
* degrades gracefully to its non-graph behaviour, exactly as today.
|
|
20
|
+
* Tests (and any tooling that swaps stash backings) should call this between
|
|
21
|
+
* setups to guarantee the next `loadGraphBoostContext` reads fresh state.
|
|
22
|
+
* Recommended placement: `beforeEach` for test files that mutate graph
|
|
23
|
+
* state, or after `deleteStoredGraph` / `replaceStoredGraph` calls that
|
|
24
|
+
* intentionally invalidate the cache.
|
|
18
25
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
export function resetGraphBoostCache() {
|
|
27
|
+
cachedParsedGraph = undefined;
|
|
28
|
+
}
|
|
29
|
+
function resolveGraphBoostWeights(config) {
|
|
30
|
+
const configured = config?.search?.graphBoost;
|
|
31
|
+
return {
|
|
32
|
+
directBoostPerEntity: configured?.directBoostPerEntity ?? GRAPH_DIRECT_BOOST_PER_ENTITY,
|
|
33
|
+
directBoostCap: configured?.directBoostCap ?? GRAPH_DIRECT_BOOST_CAP,
|
|
34
|
+
hopBoostPerEntity: configured?.hopBoostPerEntity ?? GRAPH_HOP_BOOST_PER_ENTITY,
|
|
35
|
+
hopBoostCap: configured?.hopBoostCap ?? GRAPH_HOP_BOOST_CAP,
|
|
36
|
+
maxHops: Math.min(Math.max(configured?.maxHops ?? GRAPH_MAX_HOPS, 1), GRAPH_MAX_HOPS_HARD_CAP),
|
|
37
|
+
confidenceMode: configured?.confidenceMode ?? GRAPH_CONFIDENCE_MODE,
|
|
38
|
+
confidenceWeight: configured?.confidenceWeight ?? GRAPH_CONFIDENCE_WEIGHT,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
22
41
|
/**
|
|
23
42
|
* Per-entry weights, exposed as constants so tests can read them and so the
|
|
24
43
|
* single-source-of-truth for "how much does the graph contribute" is here
|
|
@@ -29,20 +48,47 @@ export const GRAPH_DIRECT_BOOST_PER_ENTITY = 0.25;
|
|
|
29
48
|
export const GRAPH_DIRECT_BOOST_CAP = 0.75;
|
|
30
49
|
export const GRAPH_HOP_BOOST_PER_ENTITY = 0.1;
|
|
31
50
|
export const GRAPH_HOP_BOOST_CAP = 0.3;
|
|
51
|
+
export const GRAPH_MAX_HOPS = 1;
|
|
52
|
+
export const GRAPH_CONFIDENCE_MODE = "blend";
|
|
53
|
+
export const GRAPH_CONFIDENCE_WEIGHT = 0.2;
|
|
54
|
+
const GRAPH_MAX_HOPS_HARD_CAP = 3;
|
|
55
|
+
function normalizeConfidence(raw) {
|
|
56
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
57
|
+
return undefined;
|
|
58
|
+
return Math.max(0, Math.min(1, raw));
|
|
59
|
+
}
|
|
60
|
+
function combineConfidence(...parts) {
|
|
61
|
+
let out;
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
const value = normalizeConfidence(part);
|
|
64
|
+
if (value === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
out = out === undefined ? value : out * value;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function toConfidenceMultiplier(rawConfidence, weights) {
|
|
71
|
+
if (weights.confidenceMode === "off")
|
|
72
|
+
return 1;
|
|
73
|
+
const confidence = normalizeConfidence(rawConfidence) ?? 1;
|
|
74
|
+
if (weights.confidenceMode === "multiply")
|
|
75
|
+
return confidence;
|
|
76
|
+
const blendWeight = Math.max(0, Math.min(1, weights.confidenceWeight));
|
|
77
|
+
return 1 - blendWeight + blendWeight * confidence;
|
|
78
|
+
}
|
|
32
79
|
/**
|
|
33
80
|
* Load the graph file for a stash root and pre-compute everything that's
|
|
34
81
|
* shared across all entries scored for one query. Returns `null` when:
|
|
35
|
-
* -
|
|
36
|
-
* - The file fails to parse.
|
|
37
|
-
* - The schema version doesn't match (treated like "missing" so an old
|
|
38
|
-
* index keeps working until the next `akm index --full`).
|
|
82
|
+
* - No graph snapshot exists in SQLite.
|
|
39
83
|
* - The query produces no token-level entity matches (no boost is
|
|
40
84
|
* possible, so we skip the per-entry overhead entirely).
|
|
41
85
|
*/
|
|
42
|
-
export function loadGraphBoostContext(stashRoot, query) {
|
|
43
|
-
const
|
|
44
|
-
|
|
86
|
+
export function loadGraphBoostContext(stashRoot, query, config, db) {
|
|
87
|
+
const stashRoots = Array.isArray(stashRoot) ? stashRoot : [stashRoot];
|
|
88
|
+
const parsed = readParsedGraphContext(stashRoots, db);
|
|
89
|
+
if (!parsed)
|
|
45
90
|
return null;
|
|
91
|
+
const weights = resolveGraphBoostWeights(config);
|
|
46
92
|
const queryTokens = query
|
|
47
93
|
.toLowerCase()
|
|
48
94
|
.split(/[\s\-_/]+/)
|
|
@@ -53,20 +99,21 @@ export function loadGraphBoostContext(stashRoot, query) {
|
|
|
53
99
|
// is small (capped per-asset at extract time) and lets the per-entry
|
|
54
100
|
// path do a single set membership test.
|
|
55
101
|
const allEntities = new Set();
|
|
56
|
-
const
|
|
57
|
-
for (const node of graph.files) {
|
|
58
|
-
nodesByPath.set(node.path, node);
|
|
102
|
+
for (const node of parsed.graph.files) {
|
|
59
103
|
for (const entity of node.entities)
|
|
60
104
|
allEntities.add(entity);
|
|
61
105
|
}
|
|
62
106
|
// An entity matches the query when any of its sub-tokens equals or
|
|
63
|
-
// contains a query token.
|
|
64
|
-
//
|
|
107
|
+
// contains a query token. Matching is case-insensitive; the graph keeps
|
|
108
|
+
// canonical display strings and we normalize only for comparisons here.
|
|
65
109
|
const matchedEntities = new Set();
|
|
66
110
|
for (const entity of allEntities) {
|
|
67
|
-
const
|
|
111
|
+
const normalizedEntity = normalizeGraphName(entity);
|
|
112
|
+
const entityTokens = normalizedEntity.split(/[\s\-_/]+/).filter(Boolean);
|
|
68
113
|
for (const qt of queryTokens) {
|
|
69
|
-
if (
|
|
114
|
+
if (normalizedEntity === qt ||
|
|
115
|
+
normalizedEntity.includes(qt) ||
|
|
116
|
+
entityTokens.some((et) => et === qt || et.includes(qt))) {
|
|
70
117
|
matchedEntities.add(entity);
|
|
71
118
|
break;
|
|
72
119
|
}
|
|
@@ -74,20 +121,47 @@ export function loadGraphBoostContext(stashRoot, query) {
|
|
|
74
121
|
}
|
|
75
122
|
if (matchedEntities.size === 0)
|
|
76
123
|
return null;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
const connectedEntities = new Set();
|
|
125
|
+
const connectedConfidence = new Map();
|
|
126
|
+
const visited = new Set();
|
|
127
|
+
let frontier = new Map();
|
|
128
|
+
for (const entity of matchedEntities) {
|
|
129
|
+
const seed = parsed.entityConfidence.get(entity) ?? 1;
|
|
130
|
+
frontier.set(entity, seed);
|
|
131
|
+
visited.add(entity);
|
|
132
|
+
}
|
|
133
|
+
for (let hop = 1; hop <= weights.maxHops; hop += 1) {
|
|
134
|
+
const next = new Map();
|
|
135
|
+
for (const [entity, pathConfidence] of frontier.entries()) {
|
|
136
|
+
const neighbors = parsed.adjacency.get(entity);
|
|
137
|
+
if (!neighbors)
|
|
138
|
+
continue;
|
|
139
|
+
for (const [neighbor, edgeConfidence] of neighbors.entries()) {
|
|
140
|
+
const neighborPathConfidence = Math.max(0, Math.min(1, pathConfidence * edgeConfidence));
|
|
141
|
+
const currentBest = connectedConfidence.get(neighbor) ?? 0;
|
|
142
|
+
if (neighborPathConfidence > currentBest)
|
|
143
|
+
connectedConfidence.set(neighbor, neighborPathConfidence);
|
|
144
|
+
if (visited.has(neighbor))
|
|
145
|
+
continue;
|
|
146
|
+
visited.add(neighbor);
|
|
147
|
+
next.set(neighbor, Math.max(next.get(neighbor) ?? 0, neighborPathConfidence));
|
|
148
|
+
connectedEntities.add(neighbor);
|
|
87
149
|
}
|
|
88
150
|
}
|
|
151
|
+
if (next.size === 0)
|
|
152
|
+
break;
|
|
153
|
+
frontier = next;
|
|
89
154
|
}
|
|
90
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
graph: parsed.graph,
|
|
157
|
+
nodesByPath: parsed.nodesByPath,
|
|
158
|
+
matchedEntities,
|
|
159
|
+
connectedEntities,
|
|
160
|
+
connectedConfidence,
|
|
161
|
+
entityConfidence: parsed.entityConfidence,
|
|
162
|
+
adjacency: parsed.adjacency,
|
|
163
|
+
weights,
|
|
164
|
+
};
|
|
91
165
|
}
|
|
92
166
|
/**
|
|
93
167
|
* Compute the graph-boost contribution for a single scored entry.
|
|
@@ -100,80 +174,281 @@ export function computeGraphBoost(context, filePath) {
|
|
|
100
174
|
const node = context.nodesByPath.get(filePath);
|
|
101
175
|
if (!node)
|
|
102
176
|
return 0;
|
|
103
|
-
let
|
|
104
|
-
let
|
|
177
|
+
let directBoostRaw = 0;
|
|
178
|
+
let hopBoostRaw = 0;
|
|
105
179
|
for (const entity of node.entities) {
|
|
106
|
-
if (context.matchedEntities.has(entity))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
180
|
+
if (context.matchedEntities.has(entity)) {
|
|
181
|
+
const directConfidence = combineConfidence(node.confidence, context.entityConfidence.get(entity));
|
|
182
|
+
directBoostRaw +=
|
|
183
|
+
context.weights.directBoostPerEntity * toConfidenceMultiplier(directConfidence, context.weights);
|
|
184
|
+
}
|
|
185
|
+
else if (context.connectedEntities.has(entity)) {
|
|
186
|
+
const hopConfidence = combineConfidence(node.confidence, context.entityConfidence.get(entity), context.connectedConfidence.get(entity));
|
|
187
|
+
hopBoostRaw += context.weights.hopBoostPerEntity * toConfidenceMultiplier(hopConfidence, context.weights);
|
|
188
|
+
}
|
|
110
189
|
}
|
|
111
|
-
const directBoost = Math.min(
|
|
112
|
-
const hopBoost = Math.min(
|
|
190
|
+
const directBoost = Math.min(context.weights.directBoostCap, directBoostRaw);
|
|
191
|
+
const hopBoost = Math.min(context.weights.hopBoostCap, hopBoostRaw);
|
|
113
192
|
return directBoost + hopBoost;
|
|
114
193
|
}
|
|
194
|
+
export function collectGraphRelatedHit(context, filePath) {
|
|
195
|
+
const node = context.nodesByPath.get(filePath);
|
|
196
|
+
if (!node)
|
|
197
|
+
return null;
|
|
198
|
+
const entities = [];
|
|
199
|
+
for (const entity of node.entities) {
|
|
200
|
+
if (context.matchedEntities.has(entity)) {
|
|
201
|
+
entities.push({
|
|
202
|
+
name: entity,
|
|
203
|
+
kind: "matched",
|
|
204
|
+
...(context.entityConfidence.get(entity) !== undefined
|
|
205
|
+
? { confidence: context.entityConfidence.get(entity) }
|
|
206
|
+
: {}),
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (context.connectedEntities.has(entity)) {
|
|
211
|
+
entities.push({
|
|
212
|
+
name: entity,
|
|
213
|
+
kind: "connected",
|
|
214
|
+
...(context.connectedConfidence.get(entity) !== undefined
|
|
215
|
+
? { confidence: context.connectedConfidence.get(entity) }
|
|
216
|
+
: {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (entities.length === 0)
|
|
221
|
+
return null;
|
|
222
|
+
const relatedNames = new Set(entities.map((entity) => entity.name));
|
|
223
|
+
const relations = node.relations
|
|
224
|
+
.filter((relation) => relatedNames.has(relation.from) || relatedNames.has(relation.to))
|
|
225
|
+
.map((relation) => ({
|
|
226
|
+
from: relation.from,
|
|
227
|
+
to: relation.to,
|
|
228
|
+
...(relation.type ? { type: relation.type } : {}),
|
|
229
|
+
...(normalizeConfidence(relation.confidence) !== undefined
|
|
230
|
+
? { confidence: normalizeConfidence(relation.confidence) }
|
|
231
|
+
: {}),
|
|
232
|
+
}));
|
|
233
|
+
return {
|
|
234
|
+
path: filePath,
|
|
235
|
+
type: node.type,
|
|
236
|
+
entities: entities.sort((a, b) => a.name.localeCompare(b.name)),
|
|
237
|
+
relations,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
115
240
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
241
|
+
* Find graph files that share entities with the given file.
|
|
242
|
+
*
|
|
243
|
+
* Implementation: SQL self-join on graph_file_entities, scoped by stash_root,
|
|
244
|
+
* grouped by entry_id, ordered by shared-entity count desc. Touches ~50-200
|
|
245
|
+
* rows instead of loading the entire snapshot into memory. Cold-call latency
|
|
246
|
+
* drops from ~30-60ms (full snapshot parse) to ~2-5ms on typical stashes.
|
|
247
|
+
*
|
|
248
|
+
* The returned `ref` field carries the canonical asset ref (`type:name`)
|
|
249
|
+
* resolved from entries.entry_key when the entry is indexed. Callers should
|
|
250
|
+
* fall back to formatting `path` when `ref` is undefined (orphan graph row).
|
|
119
251
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
252
|
+
export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
|
|
253
|
+
if (!db) {
|
|
254
|
+
// Fallback: opening a transient DB here is not currently a use case (all
|
|
255
|
+
// callers pass a handle), so degrade to empty rather than reopening.
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
// Resolve target's entry_id from the stash_root + file_path. The graph rows
|
|
259
|
+
// are keyed on entry_id; without it we can't run the join.
|
|
260
|
+
let targetEntryId;
|
|
123
261
|
try {
|
|
124
|
-
|
|
262
|
+
const row = db
|
|
263
|
+
.prepare("SELECT entry_id FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
|
|
264
|
+
.get(stashRoot, filePath);
|
|
265
|
+
targetEntryId = row?.entry_id;
|
|
125
266
|
}
|
|
126
267
|
catch {
|
|
127
|
-
|
|
128
|
-
// graph extraction yet, or the pass hasn't run.
|
|
129
|
-
return null;
|
|
268
|
+
return [];
|
|
130
269
|
}
|
|
131
|
-
|
|
270
|
+
if (targetEntryId == null)
|
|
271
|
+
return [];
|
|
272
|
+
const effectiveLimit = Math.max(1, limit);
|
|
273
|
+
// Shared-entity count per candidate entry_id.
|
|
274
|
+
let candidateRows;
|
|
132
275
|
try {
|
|
133
|
-
|
|
276
|
+
candidateRows = db
|
|
277
|
+
.prepare(`SELECT gf.entry_id AS entry_id,
|
|
278
|
+
gf.file_path AS file_path,
|
|
279
|
+
gf.file_type AS file_type,
|
|
280
|
+
COUNT(*) AS shared
|
|
281
|
+
FROM graph_file_entities target
|
|
282
|
+
JOIN graph_file_entities e
|
|
283
|
+
ON e.stash_root = target.stash_root
|
|
284
|
+
AND e.entity_norm = target.entity_norm
|
|
285
|
+
AND e.entry_id != target.entry_id
|
|
286
|
+
JOIN graph_files gf
|
|
287
|
+
ON gf.entry_id = e.entry_id
|
|
288
|
+
WHERE target.entry_id = ?
|
|
289
|
+
AND target.stash_root = ?
|
|
290
|
+
GROUP BY gf.entry_id
|
|
291
|
+
ORDER BY shared DESC, gf.file_path ASC
|
|
292
|
+
LIMIT ?`)
|
|
293
|
+
.all(targetEntryId, stashRoot, effectiveLimit);
|
|
134
294
|
}
|
|
135
|
-
catch
|
|
136
|
-
|
|
137
|
-
return null;
|
|
295
|
+
catch {
|
|
296
|
+
return [];
|
|
138
297
|
}
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
298
|
+
if (candidateRows.length === 0)
|
|
299
|
+
return [];
|
|
300
|
+
const candidateIds = candidateRows.map((r) => r.entry_id);
|
|
301
|
+
const placeholders = candidateIds.map(() => "?").join(",");
|
|
302
|
+
// Pull the shared entity names (joined by normalized casing) for display.
|
|
303
|
+
const sharedRows = db
|
|
304
|
+
.prepare(`SELECT e.entry_id AS entry_id, e.entity AS entity
|
|
305
|
+
FROM graph_file_entities e
|
|
306
|
+
JOIN graph_file_entities target
|
|
307
|
+
ON target.stash_root = e.stash_root
|
|
308
|
+
AND target.entity_norm = e.entity_norm
|
|
309
|
+
WHERE e.entry_id IN (${placeholders})
|
|
310
|
+
AND target.entry_id = ?
|
|
311
|
+
AND target.stash_root = ?`)
|
|
312
|
+
.all(...candidateIds, targetEntryId, stashRoot);
|
|
313
|
+
const sharedByEntry = new Map();
|
|
314
|
+
for (const row of sharedRows) {
|
|
315
|
+
let bucket = sharedByEntry.get(row.entry_id);
|
|
316
|
+
if (!bucket) {
|
|
317
|
+
bucket = new Set();
|
|
318
|
+
sharedByEntry.set(row.entry_id, bucket);
|
|
319
|
+
}
|
|
320
|
+
bucket.add(row.entity);
|
|
321
|
+
}
|
|
322
|
+
// Relation count for each candidate (relations where either endpoint
|
|
323
|
+
// matches one of the shared entities).
|
|
324
|
+
const relationCountByEntry = new Map();
|
|
325
|
+
const relationRows = db
|
|
326
|
+
.prepare(`SELECT entry_id, from_entity, to_entity
|
|
327
|
+
FROM graph_file_relations
|
|
328
|
+
WHERE entry_id IN (${placeholders})`)
|
|
329
|
+
.all(...candidateIds);
|
|
330
|
+
for (const row of relationRows) {
|
|
331
|
+
const shared = sharedByEntry.get(row.entry_id);
|
|
332
|
+
if (!shared)
|
|
333
|
+
continue;
|
|
334
|
+
if (shared.has(row.from_entity) || shared.has(row.to_entity)) {
|
|
335
|
+
relationCountByEntry.set(row.entry_id, (relationCountByEntry.get(row.entry_id) ?? 0) + 1);
|
|
336
|
+
}
|
|
141
337
|
}
|
|
142
|
-
|
|
338
|
+
// Optional: ref lookup via entries.entry_key. entry_key is stored as
|
|
339
|
+
// `${stash_dir}:${type}:${name}` — strip the stash-dir prefix to get the
|
|
340
|
+
// user-facing `type:name`.
|
|
341
|
+
const refByEntryId = new Map();
|
|
342
|
+
try {
|
|
343
|
+
const entryRows = db
|
|
344
|
+
.prepare(`SELECT id, entry_key, stash_dir FROM entries WHERE id IN (${placeholders})`)
|
|
345
|
+
.all(...candidateIds);
|
|
346
|
+
for (const row of entryRows) {
|
|
347
|
+
const ref = stripStashPrefix(row.entry_key, row.stash_dir);
|
|
348
|
+
if (ref)
|
|
349
|
+
refByEntryId.set(row.id, ref);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
/* ignore — refs are best-effort */
|
|
354
|
+
}
|
|
355
|
+
return candidateRows.map((row) => {
|
|
356
|
+
const sharedSet = sharedByEntry.get(row.entry_id) ?? new Set();
|
|
357
|
+
const sharedEntities = [...sharedSet].sort((a, b) => a.localeCompare(b));
|
|
358
|
+
const ref = refByEntryId.get(row.entry_id);
|
|
359
|
+
return {
|
|
360
|
+
...(ref ? { ref } : {}),
|
|
361
|
+
path: row.file_path,
|
|
362
|
+
type: row.file_type,
|
|
363
|
+
sharedEntities,
|
|
364
|
+
relationCount: relationCountByEntry.get(row.entry_id) ?? 0,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
143
367
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Convert an entries.entry_key to a user-facing asset ref. Entry keys are
|
|
370
|
+
* stored as `${stash_dir}:${type}:${name}`; strip the stash-dir prefix.
|
|
371
|
+
*/
|
|
372
|
+
function stripStashPrefix(entryKey, stashDir) {
|
|
373
|
+
const prefix = `${stashDir}:`;
|
|
374
|
+
if (entryKey.startsWith(prefix))
|
|
375
|
+
return entryKey.slice(prefix.length);
|
|
376
|
+
// Fall back to last two colon-separated segments.
|
|
377
|
+
const lastColon = entryKey.lastIndexOf(":");
|
|
378
|
+
if (lastColon < 0)
|
|
379
|
+
return entryKey;
|
|
380
|
+
const prevColon = entryKey.lastIndexOf(":", lastColon - 1);
|
|
381
|
+
if (prevColon < 0)
|
|
382
|
+
return entryKey;
|
|
383
|
+
return entryKey.slice(prevColon + 1);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Load and normalize graph data from SQLite once, then reuse it across all
|
|
387
|
+
* per-entry boost lookups in the current query.
|
|
388
|
+
*/
|
|
389
|
+
function readParsedGraphContext(stashRoots, db) {
|
|
390
|
+
const sortedRoots = [...new Set(stashRoots)].sort((a, b) => a.localeCompare(b));
|
|
391
|
+
if (sortedRoots.length === 0)
|
|
392
|
+
return null;
|
|
393
|
+
const metas = sortedRoots
|
|
394
|
+
.map((stashRoot) => loadStoredGraphMeta(stashRoot, db))
|
|
395
|
+
.filter((meta) => meta !== null);
|
|
396
|
+
if (metas.length === 0)
|
|
397
|
+
return null;
|
|
398
|
+
const cacheKey = metas.map((meta) => `${meta.stashPath}\u0000${meta.generatedAt}`).join("\u0001");
|
|
399
|
+
if (cachedParsedGraph && cachedParsedGraph.cacheKey === cacheKey)
|
|
400
|
+
return cachedParsedGraph.context;
|
|
401
|
+
const snapshots = metas
|
|
402
|
+
.map((meta) => loadStoredGraphSnapshot(meta.stashPath, db))
|
|
403
|
+
.filter((snapshot) => snapshot !== null);
|
|
404
|
+
if (snapshots.length === 0)
|
|
405
|
+
return null;
|
|
406
|
+
const graph = {
|
|
407
|
+
schemaVersion: Math.max(...snapshots.map((snapshot) => snapshot.schemaVersion)),
|
|
408
|
+
generatedAt: snapshots
|
|
409
|
+
.map((snapshot) => snapshot.generatedAt)
|
|
410
|
+
.sort()
|
|
411
|
+
.at(-1) ?? new Date(0).toISOString(),
|
|
412
|
+
stashRoot: snapshots[0]?.stashPath ?? "",
|
|
413
|
+
files: snapshots.flatMap((snapshot) => snapshot.files),
|
|
414
|
+
entities: [...new Set(snapshots.flatMap((snapshot) => snapshot.entities))],
|
|
415
|
+
relations: snapshots.flatMap((snapshot) => snapshot.relations),
|
|
416
|
+
};
|
|
417
|
+
const nodesByPath = new Map();
|
|
418
|
+
const entityConfidence = new Map();
|
|
419
|
+
const adjacency = new Map();
|
|
420
|
+
function setBestEntityConfidence(entity, confidence) {
|
|
421
|
+
const normalized = normalizeConfidence(confidence);
|
|
422
|
+
if (normalized === undefined)
|
|
423
|
+
return;
|
|
424
|
+
const current = entityConfidence.get(entity);
|
|
425
|
+
if (current === undefined || normalized > current)
|
|
426
|
+
entityConfidence.set(entity, normalized);
|
|
427
|
+
}
|
|
428
|
+
function setBestEdgeConfidence(from, to, confidence) {
|
|
429
|
+
const normalized = normalizeConfidence(confidence);
|
|
430
|
+
if (!adjacency.has(from))
|
|
431
|
+
adjacency.set(from, new Map());
|
|
432
|
+
const neighbors = adjacency.get(from);
|
|
433
|
+
if (!neighbors)
|
|
434
|
+
return;
|
|
435
|
+
const current = neighbors.get(to);
|
|
436
|
+
const next = normalized ?? 1;
|
|
437
|
+
if (current === undefined || next > current)
|
|
438
|
+
neighbors.set(to, next);
|
|
439
|
+
}
|
|
440
|
+
for (const node of graph.files) {
|
|
441
|
+
nodesByPath.set(node.path, node);
|
|
442
|
+
for (const entity of node.entities) {
|
|
443
|
+
setBestEntityConfidence(entity, node.confidence);
|
|
444
|
+
}
|
|
445
|
+
for (const rel of node.relations) {
|
|
446
|
+
const edgeConfidence = combineConfidence(node.confidence, rel.confidence);
|
|
447
|
+
setBestEdgeConfidence(rel.from, rel.to, edgeConfidence);
|
|
448
|
+
setBestEdgeConfidence(rel.to, rel.from, edgeConfidence);
|
|
176
449
|
}
|
|
177
450
|
}
|
|
178
|
-
|
|
451
|
+
const context = { graph, nodesByPath, entityConfidence, adjacency };
|
|
452
|
+
cachedParsedGraph = { cacheKey, context };
|
|
453
|
+
return context;
|
|
179
454
|
}
|