akm-cli 0.8.0-rc1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- 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 +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2162 -1258
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- 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 +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +233 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -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 +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +662 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +114 -48
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -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 +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- 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 +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- 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 +105 -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 -549
- 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 +1059 -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 +12 -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 -1329
- 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 +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- 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 +17767 -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 +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -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 +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- 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 +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -307
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -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
|
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* This module is the consumer half of the graph-extraction pass. It loads
|
|
5
|
-
* the persisted graph snapshot from SQLite and exposes a single helper,
|
|
6
|
-
* {@link computeGraphBoost}, that the existing FTS5+boosts loop in
|
|
7
|
-
* `src/indexer/db-search.ts` calls per-entry to obtain an additive boost
|
|
8
|
-
* value.
|
|
9
|
-
*
|
|
10
|
-
* CLAUDE.md / v1 spec compliance:
|
|
11
|
-
* - The graph signal feeds the **single** FTS5+boosts pipeline as one
|
|
12
|
-
* additive boost component. There is no parallel scoring track.
|
|
13
|
-
* - There is no second `SearchHit` scorer. `searchDatabase` continues to
|
|
14
|
-
* own ranking; this module just answers "what additive boost does the
|
|
15
|
-
* graph contribute for this (query, entry) pair?".
|
|
16
|
-
* - Missing graph rows → boost is `0`. The pipeline
|
|
17
|
-
* degrades gracefully to its non-graph behaviour, exactly as today.
|
|
18
|
-
*/
|
|
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/.
|
|
19
4
|
import { loadStoredGraphMeta, loadStoredGraphSnapshot } from "./graph-db";
|
|
20
5
|
function normalizeGraphName(value) {
|
|
21
6
|
return value.trim().toLowerCase();
|
|
22
7
|
}
|
|
23
8
|
let cachedParsedGraph;
|
|
9
|
+
/**
|
|
10
|
+
* Clear the module-level parsed-graph cache.
|
|
11
|
+
*
|
|
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.
|
|
19
|
+
*
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
export function resetGraphBoostCache() {
|
|
27
|
+
cachedParsedGraph = undefined;
|
|
28
|
+
}
|
|
24
29
|
function resolveGraphBoostWeights(config) {
|
|
25
30
|
const configured = config?.search?.graphBoost;
|
|
26
31
|
return {
|
|
@@ -232,33 +237,150 @@ export function collectGraphRelatedHit(context, filePath) {
|
|
|
232
237
|
relations,
|
|
233
238
|
};
|
|
234
239
|
}
|
|
240
|
+
/**
|
|
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).
|
|
251
|
+
*/
|
|
235
252
|
export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
238
256
|
return [];
|
|
239
|
-
|
|
240
|
-
|
|
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;
|
|
261
|
+
try {
|
|
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;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
241
268
|
return [];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
269
|
+
}
|
|
270
|
+
if (targetEntryId == null)
|
|
271
|
+
return [];
|
|
272
|
+
const effectiveLimit = Math.max(1, limit);
|
|
273
|
+
// Shared-entity count per candidate entry_id.
|
|
274
|
+
let candidateRows;
|
|
275
|
+
try {
|
|
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);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
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)
|
|
249
333
|
continue;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
}
|
|
337
|
+
}
|
|
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
|
+
}
|
|
257
351
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
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
|
+
});
|
|
367
|
+
}
|
|
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);
|
|
262
384
|
}
|
|
263
385
|
/**
|
|
264
386
|
* Load and normalize graph data from SQLite once, then reuse it across all
|
package/dist/indexer/graph-db.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
import fs from "node:fs";
|
|
5
|
+
import { rethrowIfTestIsolationError } from "../core/errors";
|
|
2
6
|
import { getDbPath } from "../core/paths";
|
|
7
|
+
import { warn } from "../core/warn";
|
|
3
8
|
import { closeDatabase, openExistingDatabase } from "./db";
|
|
4
9
|
function withReadableGraphDb(db, fn) {
|
|
5
10
|
if (db)
|
|
@@ -18,6 +23,42 @@ function withReadableGraphDb(db, fn) {
|
|
|
18
23
|
function uniqueSorted(values) {
|
|
19
24
|
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
20
25
|
}
|
|
26
|
+
function normalizeEntity(value) {
|
|
27
|
+
return value.trim().toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a file_path within a stash to its entries.id. Returns null when the
|
|
31
|
+
* path has no indexed entry (orphan graph row).
|
|
32
|
+
*/
|
|
33
|
+
export function resolveEntryIdForPath(db, stashRoot, filePath) {
|
|
34
|
+
try {
|
|
35
|
+
const row = db
|
|
36
|
+
.prepare("SELECT id FROM entries WHERE stash_dir = ? AND file_path = ? LIMIT 1")
|
|
37
|
+
.get(stashRoot, filePath);
|
|
38
|
+
if (row)
|
|
39
|
+
return row.id;
|
|
40
|
+
// Fall back to file_path-only match (legacy callers may pass a stash root
|
|
41
|
+
// that doesn't exactly match entries.stash_dir, e.g. trailing-slash diffs).
|
|
42
|
+
const fallback = db.prepare("SELECT id FROM entries WHERE file_path = ? LIMIT 1").get(filePath);
|
|
43
|
+
return fallback?.id ?? null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Persist (or update) a graph snapshot for a stash root.
|
|
51
|
+
*
|
|
52
|
+
* Implementation: incremental upsert keyed on entries.id. Unchanged files
|
|
53
|
+
* (matching body_hash) are skipped; changed files have their child rows
|
|
54
|
+
* deleted (CASCADE) and re-inserted; files in DB but absent from the new
|
|
55
|
+
* snapshot are deleted. The old behaviour wiped every row for the stash on
|
|
56
|
+
* each write, which produced ~22k row writes per re-index even when one
|
|
57
|
+
* asset changed.
|
|
58
|
+
*
|
|
59
|
+
* Orphan files (no entries row resolvable) are skipped and counted in a
|
|
60
|
+
* single warn() so the caller sees the magnitude without log spam.
|
|
61
|
+
*/
|
|
21
62
|
export function replaceStoredGraph(db, graph) {
|
|
22
63
|
const upsertMeta = db.prepare(`INSERT INTO graph_meta (
|
|
23
64
|
stash_root,
|
|
@@ -28,58 +69,172 @@ export function replaceStoredGraph(db, graph) {
|
|
|
28
69
|
entity_count,
|
|
29
70
|
relation_count,
|
|
30
71
|
extraction_coverage,
|
|
31
|
-
density
|
|
32
|
-
|
|
72
|
+
density,
|
|
73
|
+
extractor_id,
|
|
74
|
+
extraction_run_id,
|
|
75
|
+
model,
|
|
76
|
+
prompt_version,
|
|
77
|
+
batch_size,
|
|
78
|
+
cache_hits,
|
|
79
|
+
cache_misses,
|
|
80
|
+
truncation_count,
|
|
81
|
+
failure_count
|
|
82
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
33
83
|
ON CONFLICT(stash_root) DO UPDATE SET
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
schema_version = excluded.schema_version,
|
|
85
|
+
generated_at = excluded.generated_at,
|
|
86
|
+
considered_files = excluded.considered_files,
|
|
87
|
+
extracted_files = excluded.extracted_files,
|
|
88
|
+
entity_count = excluded.entity_count,
|
|
89
|
+
relation_count = excluded.relation_count,
|
|
90
|
+
extraction_coverage = excluded.extraction_coverage,
|
|
91
|
+
density = excluded.density,
|
|
92
|
+
extractor_id = excluded.extractor_id,
|
|
93
|
+
extraction_run_id = excluded.extraction_run_id,
|
|
94
|
+
model = excluded.model,
|
|
95
|
+
prompt_version = excluded.prompt_version,
|
|
96
|
+
batch_size = excluded.batch_size,
|
|
97
|
+
cache_hits = excluded.cache_hits,
|
|
98
|
+
cache_misses = excluded.cache_misses,
|
|
99
|
+
truncation_count = excluded.truncation_count,
|
|
100
|
+
failure_count = excluded.failure_count`);
|
|
101
|
+
const selectExisting = db.prepare("SELECT entry_id, file_path, body_hash FROM graph_files WHERE stash_root = ?");
|
|
102
|
+
const deleteFile = db.prepare("DELETE FROM graph_files WHERE entry_id = ?");
|
|
103
|
+
const deleteEntities = db.prepare("DELETE FROM graph_file_entities WHERE entry_id = ?");
|
|
104
|
+
const deleteRelations = db.prepare("DELETE FROM graph_file_relations WHERE entry_id = ?");
|
|
105
|
+
const insertFile = db.prepare(`INSERT INTO graph_files (
|
|
106
|
+
entry_id, stash_root, file_path, file_order, file_type, body_hash, confidence, status, reason, extraction_run_id
|
|
107
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
108
|
+
const updateFileMeta = db.prepare(`UPDATE graph_files
|
|
109
|
+
SET file_order = ?, file_type = ?, confidence = ?, status = ?, reason = ?, extraction_run_id = ?
|
|
110
|
+
WHERE entry_id = ?`);
|
|
111
|
+
const insertEntity = db.prepare(`INSERT INTO graph_file_entities (entry_id, entity_order, stash_root, entity_norm, entity)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
49
113
|
const insertRelation = db.prepare(`INSERT INTO graph_file_relations (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
relation_order,
|
|
53
|
-
from_entity,
|
|
54
|
-
to_entity,
|
|
55
|
-
relation_type,
|
|
56
|
-
confidence
|
|
57
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
114
|
+
entry_id, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence
|
|
115
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
58
116
|
const quality = graph.quality;
|
|
117
|
+
const telemetry = graph.telemetry;
|
|
59
118
|
db.transaction(() => {
|
|
60
|
-
upsertMeta.run(graph.stashRoot, graph.schemaVersion, graph.generatedAt, quality?.consideredFiles ?? graph.files.length, quality?.extractedFiles ?? graph.files.length, quality?.entityCount ?? graph.entities?.length ?? 0, quality?.relationCount ?? graph.relations?.length ?? 0, quality?.extractionCoverage ?? 0, quality?.density ?? 0);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
119
|
+
upsertMeta.run(graph.stashRoot, graph.schemaVersion, graph.generatedAt, quality?.consideredFiles ?? graph.files.length, quality?.extractedFiles ?? graph.files.length, quality?.entityCount ?? graph.entities?.length ?? 0, quality?.relationCount ?? graph.relations?.length ?? 0, quality?.extractionCoverage ?? 0, quality?.density ?? 0, telemetry?.extractorId ?? null, telemetry?.extractionRunId ?? null, telemetry?.model ?? null, telemetry?.promptVersion ?? null, telemetry?.batchSize ?? null, telemetry?.cacheHits ?? 0, telemetry?.cacheMisses ?? 0, telemetry?.truncationCount ?? 0, telemetry?.failureCount ?? 0);
|
|
120
|
+
// Build a snapshot of existing rows for incremental compare.
|
|
121
|
+
const existingRows = selectExisting.all(graph.stashRoot);
|
|
122
|
+
const existingByPath = new Map();
|
|
123
|
+
for (const row of existingRows)
|
|
124
|
+
existingByPath.set(row.file_path, row);
|
|
125
|
+
let orphanCount = 0;
|
|
126
|
+
const presentEntryIds = new Set();
|
|
64
127
|
for (const [fileOrder, node] of graph.files.entries()) {
|
|
65
|
-
|
|
128
|
+
// body_hash is NOT NULL in schema v2; default to a sentinel for inputs
|
|
129
|
+
// (test fixtures, legacy imports) that don't supply one. The sentinel
|
|
130
|
+
// never equals a real hash so subsequent staleness checks always
|
|
131
|
+
// re-extract — correct behaviour for "unknown" bodies.
|
|
132
|
+
const bodyHash = node.bodyHash && node.bodyHash.length > 0 ? node.bodyHash : "";
|
|
133
|
+
const entryId = resolveEntryIdForPath(db, graph.stashRoot, node.path);
|
|
134
|
+
if (entryId == null) {
|
|
135
|
+
orphanCount += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
presentEntryIds.add(entryId);
|
|
139
|
+
const existing = existingByPath.get(node.path);
|
|
140
|
+
if (existing && existing.entry_id === entryId && existing.body_hash === bodyHash) {
|
|
141
|
+
// Body unchanged — only fix up file_order/confidence in case they drifted.
|
|
142
|
+
updateFileMeta.run(fileOrder, node.type, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null, entryId);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (existing) {
|
|
146
|
+
// Stale row (different body_hash, or entry_id moved to a different
|
|
147
|
+
// path under the same file_path). Wipe child rows; CASCADE would do
|
|
148
|
+
// it but explicit DELETE keeps the order deterministic.
|
|
149
|
+
deleteEntities.run(existing.entry_id);
|
|
150
|
+
deleteRelations.run(existing.entry_id);
|
|
151
|
+
deleteFile.run(existing.entry_id);
|
|
152
|
+
}
|
|
153
|
+
insertFile.run(entryId, graph.stashRoot, node.path, fileOrder, node.type, bodyHash, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null);
|
|
66
154
|
for (const [entityOrder, entity] of node.entities.entries()) {
|
|
67
|
-
insertEntity.run(graph.stashRoot,
|
|
155
|
+
insertEntity.run(entryId, entityOrder, graph.stashRoot, normalizeEntity(entity), entity);
|
|
68
156
|
}
|
|
69
157
|
for (const [relationOrder, relation] of node.relations.entries()) {
|
|
70
|
-
insertRelation.run(
|
|
158
|
+
insertRelation.run(entryId, relationOrder, normalizeEntity(relation.from), relation.from, normalizeEntity(relation.to), relation.to, relation.type ?? null, relation.confidence ?? null);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Delete files present in DB but absent from the new snapshot. Child
|
|
162
|
+
// tables CASCADE on entry_id.
|
|
163
|
+
for (const row of existingRows) {
|
|
164
|
+
if (!presentEntryIds.has(row.entry_id)) {
|
|
165
|
+
deleteEntities.run(row.entry_id);
|
|
166
|
+
deleteRelations.run(row.entry_id);
|
|
167
|
+
deleteFile.run(row.entry_id);
|
|
71
168
|
}
|
|
72
169
|
}
|
|
170
|
+
if (orphanCount > 0) {
|
|
171
|
+
warn(`[graph] replaceStoredGraph: skipped ${orphanCount} file(s) with no resolvable entry under ${graph.stashRoot}.`);
|
|
172
|
+
}
|
|
73
173
|
})();
|
|
74
174
|
}
|
|
75
175
|
export function deleteStoredGraph(db, stashPath) {
|
|
76
176
|
db.transaction(() => {
|
|
77
|
-
|
|
78
|
-
db.prepare("DELETE FROM graph_file_entities WHERE stash_root = ?").run(stashPath);
|
|
177
|
+
// Child rows cascade via entry_id; deleting graph_files clears them.
|
|
79
178
|
db.prepare("DELETE FROM graph_files WHERE stash_root = ?").run(stashPath);
|
|
80
179
|
db.prepare("DELETE FROM graph_meta WHERE stash_root = ?").run(stashPath);
|
|
81
180
|
})();
|
|
82
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Scoped loader — only the graph_meta row for a stash. Used by callers that
|
|
184
|
+
* only need summary numbers (e.g. `akm graph summary`).
|
|
185
|
+
*/
|
|
186
|
+
export function loadGraphMetaOnly(stashPath, db) {
|
|
187
|
+
return loadStoredGraphMeta(stashPath, db);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Scoped loader — graph_files rows without entities/relations. Used for
|
|
191
|
+
* orphan detection and entity overview commands.
|
|
192
|
+
*/
|
|
193
|
+
export function loadGraphFilesOnly(stashPath, db) {
|
|
194
|
+
try {
|
|
195
|
+
return withReadableGraphDb(db, (readDb) => {
|
|
196
|
+
try {
|
|
197
|
+
const rows = readDb
|
|
198
|
+
.prepare(`SELECT entry_id, file_path, file_type, body_hash, confidence, status, reason
|
|
199
|
+
FROM graph_files
|
|
200
|
+
WHERE stash_root = ?
|
|
201
|
+
ORDER BY file_order`)
|
|
202
|
+
.all(stashPath);
|
|
203
|
+
return rows.map((row) => ({
|
|
204
|
+
entryId: row.entry_id,
|
|
205
|
+
path: row.file_path,
|
|
206
|
+
type: row.file_type,
|
|
207
|
+
bodyHash: row.body_hash,
|
|
208
|
+
...(typeof row.confidence === "number" ? { confidence: row.confidence } : {}),
|
|
209
|
+
...(row.status ? { status: row.status } : {}),
|
|
210
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
// Never mask the bun-test isolation guard as "no stored graph files".
|
|
220
|
+
rethrowIfTestIsolationError(err);
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Scoped loader — entities for a single entry_id. Used by per-asset lookups.
|
|
226
|
+
*/
|
|
227
|
+
export function loadGraphEntitiesByEntry(db, entryId) {
|
|
228
|
+
try {
|
|
229
|
+
const rows = db
|
|
230
|
+
.prepare("SELECT entity FROM graph_file_entities WHERE entry_id = ? ORDER BY entity_order")
|
|
231
|
+
.all(entryId);
|
|
232
|
+
return rows.map((r) => r.entity);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
83
238
|
export function loadStoredGraphMeta(stashPath, db) {
|
|
84
239
|
try {
|
|
85
240
|
return withReadableGraphDb(db, (readDb) => {
|
|
@@ -94,9 +249,18 @@ export function loadStoredGraphMeta(stashPath, db) {
|
|
|
94
249
|
entity_count,
|
|
95
250
|
relation_count,
|
|
96
251
|
extraction_coverage,
|
|
97
|
-
density
|
|
98
|
-
|
|
99
|
-
|
|
252
|
+
density,
|
|
253
|
+
extractor_id,
|
|
254
|
+
extraction_run_id,
|
|
255
|
+
model,
|
|
256
|
+
prompt_version,
|
|
257
|
+
batch_size,
|
|
258
|
+
cache_hits,
|
|
259
|
+
cache_misses,
|
|
260
|
+
truncation_count,
|
|
261
|
+
failure_count
|
|
262
|
+
FROM graph_meta
|
|
263
|
+
WHERE stash_root = ?`)
|
|
100
264
|
.get(stashPath);
|
|
101
265
|
if (!row)
|
|
102
266
|
return null;
|
|
@@ -113,6 +277,17 @@ export function loadStoredGraphMeta(stashPath, db) {
|
|
|
113
277
|
extractionCoverage: row.extraction_coverage,
|
|
114
278
|
density: row.density,
|
|
115
279
|
},
|
|
280
|
+
telemetry: {
|
|
281
|
+
...(row.extractor_id ? { extractorId: row.extractor_id } : {}),
|
|
282
|
+
...(row.extraction_run_id ? { extractionRunId: row.extraction_run_id } : {}),
|
|
283
|
+
...(row.model ? { model: row.model } : {}),
|
|
284
|
+
...(row.prompt_version ? { promptVersion: row.prompt_version } : {}),
|
|
285
|
+
...(typeof row.batch_size === "number" ? { batchSize: row.batch_size } : {}),
|
|
286
|
+
cacheHits: row.cache_hits,
|
|
287
|
+
cacheMisses: row.cache_misses,
|
|
288
|
+
truncationCount: row.truncation_count,
|
|
289
|
+
failureCount: row.failure_count,
|
|
290
|
+
},
|
|
116
291
|
};
|
|
117
292
|
}
|
|
118
293
|
catch {
|
|
@@ -120,7 +295,9 @@ export function loadStoredGraphMeta(stashPath, db) {
|
|
|
120
295
|
}
|
|
121
296
|
});
|
|
122
297
|
}
|
|
123
|
-
catch {
|
|
298
|
+
catch (err) {
|
|
299
|
+
// Never mask the bun-test isolation guard as "no stored graph meta".
|
|
300
|
+
rethrowIfTestIsolationError(err);
|
|
124
301
|
return null;
|
|
125
302
|
}
|
|
126
303
|
}
|
|
@@ -132,22 +309,29 @@ export function loadStoredGraphSnapshot(stashPath, db) {
|
|
|
132
309
|
return null;
|
|
133
310
|
try {
|
|
134
311
|
const fileRows = readDb
|
|
135
|
-
.prepare(`SELECT file_path, file_type, body_hash, confidence
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
312
|
+
.prepare(`SELECT entry_id, file_path, file_type, body_hash, confidence, status, reason, extraction_run_id
|
|
313
|
+
FROM graph_files
|
|
314
|
+
WHERE stash_root = ?
|
|
315
|
+
ORDER BY file_order`)
|
|
139
316
|
.all(stashPath);
|
|
140
317
|
const entityRows = readDb
|
|
141
|
-
.prepare(`SELECT file_path, entity
|
|
142
|
-
FROM graph_file_entities
|
|
143
|
-
|
|
144
|
-
|
|
318
|
+
.prepare(`SELECT gfe.entry_id AS entry_id, gf.file_path AS file_path, gfe.entity AS entity
|
|
319
|
+
FROM graph_file_entities gfe
|
|
320
|
+
JOIN graph_files gf ON gf.entry_id = gfe.entry_id
|
|
321
|
+
WHERE gf.stash_root = ?
|
|
322
|
+
ORDER BY gf.file_order, gfe.entity_order`)
|
|
145
323
|
.all(stashPath);
|
|
146
324
|
const relationRows = readDb
|
|
147
|
-
.prepare(`SELECT
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
325
|
+
.prepare(`SELECT gfr.entry_id AS entry_id,
|
|
326
|
+
gf.file_path AS file_path,
|
|
327
|
+
gfr.from_entity AS from_entity,
|
|
328
|
+
gfr.to_entity AS to_entity,
|
|
329
|
+
gfr.relation_type AS relation_type,
|
|
330
|
+
gfr.confidence AS confidence
|
|
331
|
+
FROM graph_file_relations gfr
|
|
332
|
+
JOIN graph_files gf ON gf.entry_id = gfr.entry_id
|
|
333
|
+
WHERE gf.stash_root = ?
|
|
334
|
+
ORDER BY gf.file_order, gfr.relation_order`)
|
|
151
335
|
.all(stashPath);
|
|
152
336
|
const entitiesByPath = new Map();
|
|
153
337
|
for (const row of entityRows) {
|
|
@@ -178,6 +362,9 @@ export function loadStoredGraphSnapshot(stashPath, db) {
|
|
|
178
362
|
entities: entitiesByPath.get(row.file_path) ?? [],
|
|
179
363
|
relations: relationsByPath.get(row.file_path) ?? [],
|
|
180
364
|
...(typeof row.confidence === "number" ? { confidence: row.confidence } : {}),
|
|
365
|
+
...(row.status ? { status: row.status } : {}),
|
|
366
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
367
|
+
...(row.extraction_run_id ? { extractionRunId: row.extraction_run_id } : {}),
|
|
181
368
|
}));
|
|
182
369
|
return {
|
|
183
370
|
stashPath: meta.stashPath,
|
|
@@ -185,6 +372,7 @@ export function loadStoredGraphSnapshot(stashPath, db) {
|
|
|
185
372
|
schemaVersion: meta.schemaVersion,
|
|
186
373
|
generatedAt: meta.generatedAt,
|
|
187
374
|
...(meta.quality ? { quality: meta.quality } : {}),
|
|
375
|
+
...(meta.telemetry ? { telemetry: meta.telemetry } : {}),
|
|
188
376
|
files,
|
|
189
377
|
entities: uniqueSorted(files.flatMap((file) => file.entities)),
|
|
190
378
|
relations: files.flatMap((file) => file.relations),
|
|
@@ -195,7 +383,9 @@ export function loadStoredGraphSnapshot(stashPath, db) {
|
|
|
195
383
|
}
|
|
196
384
|
});
|
|
197
385
|
}
|
|
198
|
-
catch {
|
|
386
|
+
catch (err) {
|
|
387
|
+
// Never mask the bun-test isolation guard as "no stored graph snapshot".
|
|
388
|
+
rethrowIfTestIsolationError(err);
|
|
199
389
|
return null;
|
|
200
390
|
}
|
|
201
391
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Extracted from src/llm/graph-extract.ts so it can be imported by
|
|
5
|
-
* src/indexer/graph-extraction.ts without being replaced by test mocks
|
|
6
|
-
* that stub the LLM layer.
|
|
7
|
-
*/
|
|
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/.
|
|
8
4
|
function normalizeRelationType(raw) {
|
|
9
5
|
const normalized = raw?.trim().toLowerCase().replace(/\s+/g, " ") ?? "";
|
|
10
6
|
if (!normalized)
|