@remnic/core 9.6.36 → 9.6.38
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/dist/access-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +8 -8
- package/dist/access-boundary.d.ts +6 -6
- package/dist/access-boundary.js +9 -9
- package/dist/access-cli.js +32 -32
- package/dist/access-http.d.ts +6 -6
- package/dist/access-http.js +13 -13
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +8 -8
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +8 -8
- package/dist/access-mcp.d.ts +6 -6
- package/dist/access-mcp.js +12 -12
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +8 -8
- package/dist/access-operations-batch.js +10 -10
- package/dist/access-operations.d.ts +6 -6
- package/dist/access-operations.js +11 -11
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +8 -8
- package/dist/{access-service-BofDzaPT.d.ts → access-service-Fw9AYEqG.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +8 -8
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -1
- package/dist/active-recall.js.map +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-9WanSGtt.d.ts → catalog-CnS_MP-8.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-B4LPRAWL.js → chunk-2V6L3L56.js} +6 -6
- package/dist/{chunk-AXALUFAH.js → chunk-3BZDBG7U.js} +2 -2
- package/dist/{chunk-KL4UAXUY.js → chunk-3WULMX34.js} +73 -2
- package/dist/{chunk-KL4UAXUY.js.map → chunk-3WULMX34.js.map} +1 -1
- package/dist/{chunk-JODHPNLN.js → chunk-5ELELET5.js} +2 -2
- package/dist/{chunk-DQ42IQSK.js → chunk-5SORASJK.js} +2 -2
- package/dist/{chunk-DAAJUA6Q.js → chunk-6MWGGPJJ.js} +5 -5
- package/dist/{chunk-2K3OSLHE.js → chunk-7TEWOG3R.js} +2 -2
- package/dist/{chunk-23G3T2Q5.js → chunk-7XY6TJLM.js} +2 -2
- package/dist/{chunk-W2AHSCKY.js → chunk-AZK3NEHL.js} +2 -2
- package/dist/{chunk-7ZYBDQ7V.js → chunk-CVEEJKRD.js} +2 -2
- package/dist/{chunk-YQB32INI.js → chunk-EBVUFGJ4.js} +2 -2
- package/dist/{chunk-4Q2XWAHZ.js → chunk-EDD2G35Z.js} +2 -2
- package/dist/{chunk-CAZ3Z5M6.js → chunk-F7AE7L6G.js} +2 -2
- package/dist/{chunk-B5GBWBCZ.js → chunk-GCTJG5MP.js} +2 -2
- package/dist/{chunk-C6TKSI35.js → chunk-HVE3UVAB.js} +3 -3
- package/dist/{chunk-SDAXPXER.js → chunk-IGCQHJSQ.js} +2 -2
- package/dist/{chunk-3AC5UYKI.js → chunk-METJNEIB.js} +1 -1
- package/dist/chunk-METJNEIB.js.map +1 -0
- package/dist/{chunk-MXAGJHTA.js → chunk-NREKOGZ2.js} +3 -3
- package/dist/{chunk-H4FHR4UM.js → chunk-PQG2AOL4.js} +2 -2
- package/dist/{chunk-WSOEYOHN.js → chunk-PWSZNML3.js} +15 -15
- package/dist/{chunk-OXVKR5OU.js → chunk-RU73ZROD.js} +342 -16
- package/dist/chunk-RU73ZROD.js.map +1 -0
- package/dist/{chunk-VJRJTRYF.js → chunk-RVLE2SNP.js} +17 -17
- package/dist/{chunk-KVSVVKHA.js → chunk-U5JP7FCM.js} +2 -2
- package/dist/{chunk-GQURC4B7.js → chunk-VTRF5QBX.js} +2 -2
- package/dist/{chunk-FVTLSFWU.js → chunk-WHZ7MK72.js} +2 -2
- package/dist/{chunk-PUQZIS6A.js → chunk-WX4XTAOI.js} +4 -4
- package/dist/{chunk-VHJARF5R.js → chunk-XH2Y6UIW.js} +2 -2
- package/dist/{chunk-GQGDRLDM.js → chunk-YAN6NTJN.js} +3 -3
- package/dist/{chunk-DEKIXXIG.js → chunk-ZIKJD6NL.js} +2 -2
- package/dist/{chunk-KJIJNLXT.js → chunk-ZMXXHUEZ.js} +3 -3
- package/dist/{chunk-O6YI4CVI.js → chunk-ZSDRGPMV.js} +2 -2
- package/dist/{cli-3_x5sJTL.d.ts → cli-PGF41MN6.d.ts} +5 -5
- package/dist/cli.d.ts +8 -8
- package/dist/cli.js +26 -26
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +35 -35
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +9 -8
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-CAH5-PKN.d.ts → orchestrator-xePY-WPe.d.ts} +4 -4
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +32 -32
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-CiD86zIk.d.ts → pipeline-Zgwz-Kne.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-Bef7DDuQ.d.ts → qmd-B6Xwfn5_.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +3 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BTShF2Fo.d.ts → semantic-consolidation-ClfD_CWg.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/{storage-DxPDAiU9.d.ts → storage-ZrHZ0dSM.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-DDUDgd7T.d.ts → types-BbtQZBq9.d.ts} +33 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- package/src/coding/codegraph-runtime.ts +539 -1
- package/src/coding/codegraph-surfaces.test.ts +75 -0
- package/src/coding/codegraph-surfaces.ts +34 -0
- package/src/coding/coding-knowledge-config.ts +100 -2
- package/src/types.ts +30 -0
- package/dist/chunk-3AC5UYKI.js.map +0 -1
- package/dist/chunk-OXVKR5OU.js.map +0 -1
- /package/dist/{chunk-B4LPRAWL.js.map → chunk-2V6L3L56.js.map} +0 -0
- /package/dist/{chunk-AXALUFAH.js.map → chunk-3BZDBG7U.js.map} +0 -0
- /package/dist/{chunk-JODHPNLN.js.map → chunk-5ELELET5.js.map} +0 -0
- /package/dist/{chunk-DQ42IQSK.js.map → chunk-5SORASJK.js.map} +0 -0
- /package/dist/{chunk-DAAJUA6Q.js.map → chunk-6MWGGPJJ.js.map} +0 -0
- /package/dist/{chunk-2K3OSLHE.js.map → chunk-7TEWOG3R.js.map} +0 -0
- /package/dist/{chunk-23G3T2Q5.js.map → chunk-7XY6TJLM.js.map} +0 -0
- /package/dist/{chunk-W2AHSCKY.js.map → chunk-AZK3NEHL.js.map} +0 -0
- /package/dist/{chunk-7ZYBDQ7V.js.map → chunk-CVEEJKRD.js.map} +0 -0
- /package/dist/{chunk-YQB32INI.js.map → chunk-EBVUFGJ4.js.map} +0 -0
- /package/dist/{chunk-4Q2XWAHZ.js.map → chunk-EDD2G35Z.js.map} +0 -0
- /package/dist/{chunk-CAZ3Z5M6.js.map → chunk-F7AE7L6G.js.map} +0 -0
- /package/dist/{chunk-B5GBWBCZ.js.map → chunk-GCTJG5MP.js.map} +0 -0
- /package/dist/{chunk-C6TKSI35.js.map → chunk-HVE3UVAB.js.map} +0 -0
- /package/dist/{chunk-SDAXPXER.js.map → chunk-IGCQHJSQ.js.map} +0 -0
- /package/dist/{chunk-MXAGJHTA.js.map → chunk-NREKOGZ2.js.map} +0 -0
- /package/dist/{chunk-H4FHR4UM.js.map → chunk-PQG2AOL4.js.map} +0 -0
- /package/dist/{chunk-WSOEYOHN.js.map → chunk-PWSZNML3.js.map} +0 -0
- /package/dist/{chunk-VJRJTRYF.js.map → chunk-RVLE2SNP.js.map} +0 -0
- /package/dist/{chunk-KVSVVKHA.js.map → chunk-U5JP7FCM.js.map} +0 -0
- /package/dist/{chunk-GQURC4B7.js.map → chunk-VTRF5QBX.js.map} +0 -0
- /package/dist/{chunk-FVTLSFWU.js.map → chunk-WHZ7MK72.js.map} +0 -0
- /package/dist/{chunk-PUQZIS6A.js.map → chunk-WX4XTAOI.js.map} +0 -0
- /package/dist/{chunk-VHJARF5R.js.map → chunk-XH2Y6UIW.js.map} +0 -0
- /package/dist/{chunk-GQGDRLDM.js.map → chunk-YAN6NTJN.js.map} +0 -0
- /package/dist/{chunk-DEKIXXIG.js.map → chunk-ZIKJD6NL.js.map} +0 -0
- /package/dist/{chunk-KJIJNLXT.js.map → chunk-ZMXXHUEZ.js.map} +0 -0
- /package/dist/{chunk-O6YI4CVI.js.map → chunk-ZSDRGPMV.js.map} +0 -0
|
@@ -23,11 +23,15 @@
|
|
|
23
23
|
* by the surface handler via a context seam.
|
|
24
24
|
*/
|
|
25
25
|
import path from "node:path";
|
|
26
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
27
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
26
29
|
import { createHash } from "node:crypto";
|
|
27
30
|
import { expandTildePath } from "../utils/path.js";
|
|
28
31
|
|
|
29
32
|
import type { PluginConfig, CodingKnowledgeConfig, CodingContext } from "../types.js";
|
|
30
33
|
import { isCodingGraphInstalled } from "./optional-coding-graph.js";
|
|
34
|
+
import { displayErrorDetail } from "../runtime/better-sqlite.js";
|
|
31
35
|
// Type-only reference to the surface context shape (ts-import-type rule).
|
|
32
36
|
// Erased at compile time, so the runtime → surfaces → runtime import cycle is
|
|
33
37
|
// type-only and has no runtime effect.
|
|
@@ -68,6 +72,16 @@ interface CodegraphModule {
|
|
|
68
72
|
): unknown;
|
|
69
73
|
defaultCodingGitInvoker?: unknown;
|
|
70
74
|
createCodingGraphEngine?(options?: unknown): { parseFile(input: unknown): Promise<unknown> };
|
|
75
|
+
// LSP resolution (issue #1917): present when @remnic/coding-graph ships
|
|
76
|
+
executeLspResolution?(requests: readonly unknown[], options: unknown): Promise<unknown>;
|
|
77
|
+
planLspUpgrades?(sites: readonly unknown[], budget: { maxRequests: number }): { requests: readonly unknown[]; budgetExhausted: number };
|
|
78
|
+
LspClient?: {
|
|
79
|
+
connect(options: {
|
|
80
|
+
launchSpec: { command: string; args: readonly string[] };
|
|
81
|
+
rootUri: string | null;
|
|
82
|
+
timeoutMs: number;
|
|
83
|
+
}): Promise<{ ok: true; client: unknown } | { ok: false; degradation: unknown }>;
|
|
84
|
+
};
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
/**
|
|
@@ -95,6 +109,21 @@ export interface CodegraphStore {
|
|
|
95
109
|
upsertEdges?(
|
|
96
110
|
edges: readonly CodegraphEdgeInput[],
|
|
97
111
|
): Promise<{ ok: true; persisted: number; skipped: number } | { ok: false; code: string }>;
|
|
112
|
+
/** Find the node whose span contains the byte offset (issue #1917). */
|
|
113
|
+
findNodeBySpan?(filePath: string, byteOffset: number): string | null;
|
|
114
|
+
/** Retire stale lsp-provenance edges for a file (issue #1895). */
|
|
115
|
+
reconcileLspEdges?(
|
|
116
|
+
filePath: string,
|
|
117
|
+
assertedEdges: ReadonlyArray<{ srcQualifiedName: string; dstQualifiedName: string; type: string }>,
|
|
118
|
+
): number;
|
|
119
|
+
/** Callee names already CALLS-linked from a caller with a given provenance (issue #1917). */
|
|
120
|
+
resolvedCalleeNames?(srcQualifiedName: string, provenance: string, filePath?: string): string[];
|
|
121
|
+
/** Current CALLS edges of a provenance owned by a caller symbol in a file (issue #1917). */
|
|
122
|
+
callEdgesForCaller?(
|
|
123
|
+
srcQualifiedName: string,
|
|
124
|
+
filePath: string,
|
|
125
|
+
provenance: string,
|
|
126
|
+
): Array<{ srcQualifiedName: string; dstQualifiedName: string; dstName: string; type: string }>;
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
/**
|
|
@@ -470,13 +499,14 @@ export async function closeAllCodegraphStores(): Promise<void> {
|
|
|
470
499
|
|
|
471
500
|
export function makeCodegraphRuntimeDelegates(): Pick<
|
|
472
501
|
CodegraphSurfaceContext,
|
|
473
|
-
"runReindex" | "reportIndexStatus" | "detectChanges" | "ingestTraces"
|
|
502
|
+
"runReindex" | "runLspResolution" | "reportIndexStatus" | "detectChanges" | "ingestTraces"
|
|
474
503
|
> {
|
|
475
504
|
return {
|
|
476
505
|
runReindex: (store, repoRoot, mode) => runCodegraphReindex({ store, repoRoot, mode }),
|
|
477
506
|
reportIndexStatus: (store, repoRoot) => reportCodegraphIndexStatus({ store, repoRoot }),
|
|
478
507
|
detectChanges: (store, repoRoot, head) => detectCodegraphChanges({ store, repoRoot, head }),
|
|
479
508
|
ingestTraces: (store, traces) => ingestCodegraphTraces({ store, traces }),
|
|
509
|
+
runLspResolution: (store, repoRoot, lspConfig) => runCodegraphLspResolution({ store, repoRoot, lspConfig }),
|
|
480
510
|
};
|
|
481
511
|
}
|
|
482
512
|
|
|
@@ -566,6 +596,514 @@ export async function runCodegraphReindex(params: {
|
|
|
566
596
|
}
|
|
567
597
|
}
|
|
568
598
|
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* True when a tracked path resolves — via a symlinked file or any symlinked
|
|
602
|
+
* parent — outside repoRoot. Mirrors the reindex path's guard (reindex.ts
|
|
603
|
+
* `symlinkEscapesRoot`, rule 3): without it, a tracked `*.ts` symlink to a
|
|
604
|
+
* private file outside the checkout would be parsed and sent to a language
|
|
605
|
+
* server. A realpath error returns false so the normal read path surfaces
|
|
606
|
+
* the natural failure.
|
|
607
|
+
*/
|
|
608
|
+
async function lspPathEscapesRoot(repoRoot: string, absPath: string): Promise<boolean> {
|
|
609
|
+
try {
|
|
610
|
+
const [realRoot, realAbs] = await Promise.all([realpath(repoRoot), realpath(absPath)]);
|
|
611
|
+
const rel = path.relative(realRoot, realAbs);
|
|
612
|
+
return rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel);
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Sync variant for the executor's resolveContent seam. */
|
|
619
|
+
function lspPathEscapesRootSync(repoRoot: string, absPath: string): boolean {
|
|
620
|
+
try {
|
|
621
|
+
const realRoot = realpathSync(repoRoot);
|
|
622
|
+
const realAbs = realpathSync(absPath);
|
|
623
|
+
const rel = path.relative(realRoot, realAbs);
|
|
624
|
+
return rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel);
|
|
625
|
+
} catch {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Default LSP server launch specs per language. The config's
|
|
632
|
+
* `codingKnowledge.lsp.servers` overrides these; a language with neither
|
|
633
|
+
* an override nor a default is skipped (degradation, not an error).
|
|
634
|
+
*/
|
|
635
|
+
const DEFAULT_LSP_SERVERS: Record<string, { command: string; args: string[] }> = {
|
|
636
|
+
typescript: { command: "typescript-language-server", args: ["--stdio"] },
|
|
637
|
+
tsx: { command: "typescript-language-server", args: ["--stdio"] },
|
|
638
|
+
javascript: { command: "typescript-language-server", args: ["--stdio"] },
|
|
639
|
+
python: { command: "pyright-langserver", args: ["--stdio"] },
|
|
640
|
+
go: { command: "gopls", args: [] },
|
|
641
|
+
rust: { command: "rust-analyzer", args: [] },
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/** One gathered candidate call site (pre-planner shape). */
|
|
645
|
+
interface GatheredLspSite {
|
|
646
|
+
filePath: string;
|
|
647
|
+
language: string;
|
|
648
|
+
content: string;
|
|
649
|
+
calleeByteOffset: number;
|
|
650
|
+
calleeName: string;
|
|
651
|
+
srcQualifiedName: string;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Run LSP type resolution after a heuristic reindex (issue #1917).
|
|
656
|
+
* Re-parses tracked files, gathers call sites Phase A left unresolved
|
|
657
|
+
* (member accesses plus bare calls with no existing CALLS edge), groups
|
|
658
|
+
* them by language, and per language connects a real language server via
|
|
659
|
+
* `LspClient.connect` and invokes `executeLspResolution`.
|
|
660
|
+
*
|
|
661
|
+
* Degrades cleanly at every seam: missing package/functions, no language
|
|
662
|
+
* server for a language, or a failed handshake all leave the heuristic
|
|
663
|
+
* edges standing (soft-fail, mirrors #1894's documented posture).
|
|
664
|
+
*/
|
|
665
|
+
export async function runCodegraphLspResolution(params: {
|
|
666
|
+
readonly store: CodegraphStore;
|
|
667
|
+
readonly repoRoot: string;
|
|
668
|
+
readonly lspConfig: NonNullable<CodingKnowledgeConfig["lsp"]>;
|
|
669
|
+
}): Promise<
|
|
670
|
+
| {
|
|
671
|
+
ok: true;
|
|
672
|
+
upgraded: number;
|
|
673
|
+
unresolved: number;
|
|
674
|
+
budgetExhausted: number;
|
|
675
|
+
degradations?: Array<{ language: string; code: string; message: string }>;
|
|
676
|
+
}
|
|
677
|
+
| { ok: false; code: string; message: string }
|
|
678
|
+
> {
|
|
679
|
+
const mod = await loadCodegraphModule();
|
|
680
|
+
if (mod === null) {
|
|
681
|
+
return { ok: false, code: "package_missing", message: "@remnic/coding-graph is not installed." };
|
|
682
|
+
}
|
|
683
|
+
if (
|
|
684
|
+
typeof mod.executeLspResolution !== "function" ||
|
|
685
|
+
typeof mod.planLspUpgrades !== "function" ||
|
|
686
|
+
typeof mod.LspClient?.connect !== "function"
|
|
687
|
+
) {
|
|
688
|
+
return { ok: false, code: "runtime_unavailable", message: "LSP resolution is not available in the installed @remnic/coding-graph." };
|
|
689
|
+
}
|
|
690
|
+
const engine = mod.createCodingGraphEngine?.() ?? null;
|
|
691
|
+
if (engine === null) {
|
|
692
|
+
return { ok: false, code: "engine_unavailable", message: "createCodingGraphEngine is not available." };
|
|
693
|
+
}
|
|
694
|
+
// Resolve to an absolute root up front: the executor builds file://
|
|
695
|
+
// URIs by joining workspaceRoot, and a relative root (".") would yield
|
|
696
|
+
// malformed URIs like file://src/foo.ts (review thread). The reindex
|
|
697
|
+
// path resolves relative cwd values the same way.
|
|
698
|
+
const repoRoot = path.resolve(params.repoRoot);
|
|
699
|
+
const gitFactory = mod.defaultCodingGitInvoker as
|
|
700
|
+
| (() => { listTrackedFiles(cwd: string): { ok: true; paths: readonly string[] } | { ok: false; code: string } })
|
|
701
|
+
| undefined;
|
|
702
|
+
if (typeof gitFactory !== "function") {
|
|
703
|
+
return { ok: true, upgraded: 0, unresolved: 0, budgetExhausted: 0 };
|
|
704
|
+
}
|
|
705
|
+
const tracked = gitFactory().listTrackedFiles(repoRoot);
|
|
706
|
+
if (!("paths" in tracked)) {
|
|
707
|
+
return { ok: true, upgraded: 0, unresolved: 0, budgetExhausted: 0 };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Memoized per-caller lookup of already-resolved callee names: a bare
|
|
711
|
+
// call whose (src)-[CALLS]->(dst.name == callee) edge already exists was
|
|
712
|
+
// resolved by Phase A and must not consume LSP budget (review thread:
|
|
713
|
+
// budget starvation). Member accesses are Phase A's deliberate skips —
|
|
714
|
+
// always candidates.
|
|
715
|
+
// Provenance matters: only HEURISTIC edges mark a site as Phase-A
|
|
716
|
+
// resolved. A site whose only edge is a prior "lsp" edge MUST be
|
|
717
|
+
// re-planned — excluding it from the plan while reconciling the file
|
|
718
|
+
// would retire that still-valid lsp edge (review thread). When the
|
|
719
|
+
// installed store predates resolvedCalleeNames, nothing is treated as
|
|
720
|
+
// resolved (conservative: spends budget, never mis-retires).
|
|
721
|
+
// Memo key includes the caller FILE: duplicate top-level qualified
|
|
722
|
+
// names across files must not leak each other's resolutions.
|
|
723
|
+
const resolvedCalleesBySrc = new Map<string, Set<string>>();
|
|
724
|
+
const alreadyResolved = (filePath: string, srcQualifiedName: string, calleeName: string): boolean => {
|
|
725
|
+
const key = `${filePath}\u0000${srcQualifiedName}`;
|
|
726
|
+
let names = resolvedCalleesBySrc.get(key);
|
|
727
|
+
if (names === undefined) {
|
|
728
|
+
names = new Set<string>();
|
|
729
|
+
if (typeof params.store.resolvedCalleeNames === "function") {
|
|
730
|
+
for (const name of params.store.resolvedCalleeNames(srcQualifiedName, "heuristic", filePath)) {
|
|
731
|
+
names.add(name);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
resolvedCalleesBySrc.set(key, names);
|
|
735
|
+
}
|
|
736
|
+
return names.has(calleeName);
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Gather unresolved call sites from the freshly indexed source.
|
|
740
|
+
const sites: GatheredLspSite[] = [];
|
|
741
|
+
const gatheredCountByFile = new Map<string, number>();
|
|
742
|
+
// Sites excluded because Phase A already resolved them (heuristic edge).
|
|
743
|
+
// A file with ANY filtered site must not reconcile: those sites are not
|
|
744
|
+
// in the asserted set, so reconciliation would retire their prior lsp
|
|
745
|
+
// edges even though the sites were never re-queried (review thread).
|
|
746
|
+
const filteredCountByFile = new Map<string, number>();
|
|
747
|
+
// Caller symbols of filtered sites per file — their CURRENT lsp edges
|
|
748
|
+
// are appended to the asserted set at reconcile time so stale cleanup
|
|
749
|
+
// still runs for the rest of the file without dropping them.
|
|
750
|
+
const filteredSrcByFile = new Map<string, Map<string, Set<string>>>();
|
|
751
|
+
// Files that parsed cleanly this run — the safe candidates for the
|
|
752
|
+
// empty-set reconcile below.
|
|
753
|
+
const parsedFiles = new Set<string>();
|
|
754
|
+
for (const rel of tracked.paths) {
|
|
755
|
+
const absPath = path.join(repoRoot, rel);
|
|
756
|
+
// Same symlink containment the reindex path enforces (rule 3).
|
|
757
|
+
if (await lspPathEscapesRoot(repoRoot, absPath)) continue;
|
|
758
|
+
let content: string;
|
|
759
|
+
try {
|
|
760
|
+
content = await readFile(absPath, "utf-8");
|
|
761
|
+
} catch {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
let parsed: unknown;
|
|
765
|
+
try {
|
|
766
|
+
parsed = await engine.parseFile({ path: rel, content });
|
|
767
|
+
} catch {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (!parsed || typeof parsed !== "object" || !("ok" in parsed) || parsed.ok !== true || !("ir" in parsed)) continue;
|
|
771
|
+
const ir = parsed.ir;
|
|
772
|
+
if (!ir || typeof ir !== "object" || !("callSites" in ir) || !Array.isArray(ir.callSites)) continue;
|
|
773
|
+
parsedFiles.add(rel);
|
|
774
|
+
const language = "language" in ir && typeof ir.language === "string" ? ir.language : "";
|
|
775
|
+
const symbols = "symbols" in ir && Array.isArray(ir.symbols) ? ir.symbols : [];
|
|
776
|
+
// UTF-8 view of the source for byte-exact callee-name searches.
|
|
777
|
+
const contentBytes = Buffer.from(content, "utf-8");
|
|
778
|
+
for (const site of ir.callSites) {
|
|
779
|
+
if (!site || typeof site !== "object" || !("span" in site)) continue;
|
|
780
|
+
const span = site.span;
|
|
781
|
+
if (!span || typeof span !== "object" || !("startByte" in span) || typeof span.startByte !== "number") continue;
|
|
782
|
+
const calleeName =
|
|
783
|
+
"calleeNameCandidates" in site && Array.isArray(site.calleeNameCandidates) && typeof site.calleeNameCandidates[0] === "string"
|
|
784
|
+
? site.calleeNameCandidates[0]
|
|
785
|
+
: "";
|
|
786
|
+
if (calleeName.length === 0) continue;
|
|
787
|
+
// Innermost enclosing symbol = SMALLEST containing span (review
|
|
788
|
+
// thread: first-match picks the outermost container for nested
|
|
789
|
+
// symbols, attaching edges to the wrong caller).
|
|
790
|
+
let srcQualifiedName = "";
|
|
791
|
+
let bestSpanSize = Number.POSITIVE_INFINITY;
|
|
792
|
+
for (const sym of symbols) {
|
|
793
|
+
if (!sym || typeof sym !== "object" || !("span" in sym) || !("qualifiedName" in sym)) continue;
|
|
794
|
+
const ss = sym.span;
|
|
795
|
+
if (!ss || typeof ss !== "object" || !("startByte" in ss) || !("endByte" in ss)) continue;
|
|
796
|
+
if (typeof ss.startByte !== "number" || typeof ss.endByte !== "number") continue;
|
|
797
|
+
if (ss.startByte <= span.startByte && span.startByte < ss.endByte) {
|
|
798
|
+
const size = ss.endByte - ss.startByte;
|
|
799
|
+
if (size < bestSpanSize) {
|
|
800
|
+
bestSpanSize = size;
|
|
801
|
+
srcQualifiedName = typeof sym.qualifiedName === "string" ? sym.qualifiedName : "";
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (srcQualifiedName.length === 0) continue;
|
|
806
|
+
const memberAccess = "memberAccess" in site && site.memberAccess === true;
|
|
807
|
+
if (!memberAccess && alreadyResolved(rel, srcQualifiedName, calleeName)) {
|
|
808
|
+
filteredCountByFile.set(rel, (filteredCountByFile.get(rel) ?? 0) + 1);
|
|
809
|
+
let srcs = filteredSrcByFile.get(rel);
|
|
810
|
+
if (srcs === undefined) {
|
|
811
|
+
srcs = new Map<string, Set<string>>();
|
|
812
|
+
filteredSrcByFile.set(rel, srcs);
|
|
813
|
+
}
|
|
814
|
+
const callees = srcs.get(srcQualifiedName);
|
|
815
|
+
if (callees === undefined) srcs.set(srcQualifiedName, new Set([calleeName]));
|
|
816
|
+
else callees.add(calleeName);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
// Position the definition query on the callee NAME, not the call
|
|
820
|
+
// expression start (member calls: `obj.save()` must query at `save`).
|
|
821
|
+
// The search runs in Buffer space because span offsets are UTF-8
|
|
822
|
+
// BYTES while string indexOf returns UTF-16 code units — non-ASCII
|
|
823
|
+
// source before the callee would skew a string-index search (review
|
|
824
|
+
// thread). Bounded to this call site's span (parser rule 20).
|
|
825
|
+
const nameIdx = contentBytes.indexOf(Buffer.from(calleeName, "utf-8"), span.startByte);
|
|
826
|
+
const endByte = "endByte" in span && typeof span.endByte === "number" ? span.endByte : span.startByte + calleeName.length;
|
|
827
|
+
const calleeByteOffset = nameIdx >= 0 && nameIdx < endByte ? nameIdx : span.startByte;
|
|
828
|
+
sites.push({ filePath: rel, language, content, calleeByteOffset, calleeName, srcQualifiedName });
|
|
829
|
+
gatheredCountByFile.set(rel, (gatheredCountByFile.get(rel) ?? 0) + 1);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Retire stale lsp edges for files that parsed cleanly and have NO LSP
|
|
833
|
+
// candidates at all this run (e.g. a previously upgraded member call was
|
|
834
|
+
// deleted while its containing function remains). The heuristic reindex
|
|
835
|
+
// deliberately keeps lsp-provenance rows out of its stale-delete scope,
|
|
836
|
+
// so this pass is the only owner of that cleanup (review thread). Files
|
|
837
|
+
// with filtered (heuristic-resolved) sites are left alone — their prior
|
|
838
|
+
// lsp edges were not re-queried.
|
|
839
|
+
// Filtered (Phase-A-resolved) sites keep their CURRENT lsp edges by
|
|
840
|
+
// asserting them explicitly — reconciliation can then retire genuinely
|
|
841
|
+
// stale edges in the same file without dropping preserved ones. When
|
|
842
|
+
// the store predates callEdgesForCaller, preservation is impossible,
|
|
843
|
+
// so callers below fall back to skipping reconcile for such files.
|
|
844
|
+
const canPreserve = typeof params.store.callEdgesForCaller === "function";
|
|
845
|
+
const preservedEdgesForFile = (
|
|
846
|
+
rel: string,
|
|
847
|
+
): Array<{ srcQualifiedName: string; dstQualifiedName: string; type: string }> => {
|
|
848
|
+
const srcs = filteredSrcByFile.get(rel);
|
|
849
|
+
if (srcs === undefined || !canPreserve) return [];
|
|
850
|
+
const preserved: Array<{ srcQualifiedName: string; dstQualifiedName: string; type: string }> = [];
|
|
851
|
+
for (const [src] of srcs) {
|
|
852
|
+
// Preserve ONLY lsp edges that DUPLICATE a current heuristic
|
|
853
|
+
// resolution (same src, same dst) — the covering edge a filtered
|
|
854
|
+
// bare call still asserts today. A dstName-only match would also
|
|
855
|
+
// re-assert a REMOVED member call's edge when a same-named bare
|
|
856
|
+
// call stays filtered (review thread); requiring the exact
|
|
857
|
+
// heuristic dst retires those while keeping the live coverage.
|
|
858
|
+
// Cost: an lsp edge MORE precise than its heuristic twin (rare
|
|
859
|
+
// re-export chains) is retired too — conservative, the heuristic
|
|
860
|
+
// edge still represents the call.
|
|
861
|
+
const heuristicDsts = new Set(
|
|
862
|
+
params.store.callEdgesForCaller!(src, rel, "heuristic").map((e) => e.dstQualifiedName),
|
|
863
|
+
);
|
|
864
|
+
for (const edge of params.store.callEdgesForCaller!(src, rel, "lsp")) {
|
|
865
|
+
if (!heuristicDsts.has(edge.dstQualifiedName)) continue;
|
|
866
|
+
preserved.push({
|
|
867
|
+
srcQualifiedName: edge.srcQualifiedName,
|
|
868
|
+
dstQualifiedName: edge.dstQualifiedName,
|
|
869
|
+
type: edge.type,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return preserved;
|
|
874
|
+
};
|
|
875
|
+
if (typeof params.store.reconcileLspEdges === "function") {
|
|
876
|
+
for (const rel of parsedFiles) {
|
|
877
|
+
if ((gatheredCountByFile.get(rel) ?? 0) !== 0) continue;
|
|
878
|
+
const filteredCount = filteredCountByFile.get(rel) ?? 0;
|
|
879
|
+
if (filteredCount > 0 && !canPreserve) continue;
|
|
880
|
+
params.store.reconcileLspEdges(rel, preservedEdgesForFile(rel));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (sites.length === 0) {
|
|
884
|
+
return { ok: true, upgraded: 0, unresolved: 0, budgetExhausted: 0 };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Group by language; each group gets its own server connection. The
|
|
888
|
+
// budget is shared across groups (maxRequestsPerRun is per RUN).
|
|
889
|
+
const byLanguage = new Map<string, GatheredLspSite[]>();
|
|
890
|
+
// Caller symbol -> FILES, for attributing apply-time skips to the file
|
|
891
|
+
// batches they may belong to (the apply callback sees upgrades, not
|
|
892
|
+
// files). A SET because duplicate caller qualified names can exist in
|
|
893
|
+
// multiple tracked files — a skip conservatively suppresses reconcile
|
|
894
|
+
// in every file that could own the skipped edge (review thread: a
|
|
895
|
+
// single-file map was overwritten by the later file).
|
|
896
|
+
const srcToFiles = new Map<string, Set<string>>();
|
|
897
|
+
for (const site of sites) {
|
|
898
|
+
const group = byLanguage.get(site.language);
|
|
899
|
+
if (group === undefined) byLanguage.set(site.language, [site]);
|
|
900
|
+
else group.push(site);
|
|
901
|
+
const files = srcToFiles.get(site.srcQualifiedName);
|
|
902
|
+
if (files === undefined) srcToFiles.set(site.srcQualifiedName, new Set([site.filePath]));
|
|
903
|
+
else files.add(site.filePath);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const configServers = params.lspConfig.servers ?? {};
|
|
907
|
+
const timeoutMs = params.lspConfig.timeoutMs ?? 3000;
|
|
908
|
+
let remainingBudget = Math.max(1, Math.floor(params.lspConfig.maxRequestsPerRun ?? 500));
|
|
909
|
+
const rootUri = pathToFileURL(repoRoot).href;
|
|
910
|
+
// Cross-file definition targets need their bytes for position→offset
|
|
911
|
+
// conversion (review thread: without resolveContent, cross-file
|
|
912
|
+
// upgrades — the point of Phase B — stay unresolved).
|
|
913
|
+
const resolveContent = (filePath: string): string | null => {
|
|
914
|
+
const absPath = path.join(repoRoot, filePath);
|
|
915
|
+
if (lspPathEscapesRootSync(repoRoot, absPath)) return null;
|
|
916
|
+
try {
|
|
917
|
+
return readFileSync(absPath, "utf-8");
|
|
918
|
+
} catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
let upgraded = 0;
|
|
924
|
+
let unresolved = 0;
|
|
925
|
+
let budgetExhausted = 0;
|
|
926
|
+
const degradations: Array<{ language: string; code: string; message: string }> = [];
|
|
927
|
+
for (const [language, group] of byLanguage) {
|
|
928
|
+
if (remainingBudget <= 0) {
|
|
929
|
+
budgetExhausted += group.length;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
const rawSpec = configServers[language] ?? DEFAULT_LSP_SERVERS[language];
|
|
933
|
+
if (rawSpec === undefined) {
|
|
934
|
+
// A language with candidates but no launch spec is a DIAGNOSABLE
|
|
935
|
+
// gap, not ordinary unresolved calls (review thread): surface it so
|
|
936
|
+
// the operator knows to configure codingKnowledge.lsp.servers.
|
|
937
|
+
degradations.push({
|
|
938
|
+
language,
|
|
939
|
+
code: "no_launch_spec",
|
|
940
|
+
message: `No LSP server configured for ${language} (${group.length} candidate call site(s)); set codingKnowledge.lsp.servers.${language}.`,
|
|
941
|
+
});
|
|
942
|
+
unresolved += group.length;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const launchSpec = { command: rawSpec.command, args: rawSpec.args ?? [] };
|
|
946
|
+
const plan = mod.planLspUpgrades(group, { maxRequests: remainingBudget });
|
|
947
|
+
budgetExhausted += plan.budgetExhausted;
|
|
948
|
+
remainingBudget -= plan.requests.length;
|
|
949
|
+
if (plan.requests.length === 0) continue;
|
|
950
|
+
// Reconciliation is only safe for files whose gathered sites ALL made
|
|
951
|
+
// it into the plan — a budget-truncated file would otherwise retire
|
|
952
|
+
// valid lsp edges its omitted call sites still assert (review thread).
|
|
953
|
+
const plannedCountByFile = new Map<string, number>();
|
|
954
|
+
for (const req of plan.requests) {
|
|
955
|
+
if (req && typeof req === "object" && "filePath" in req && typeof req.filePath === "string") {
|
|
956
|
+
plannedCountByFile.set(req.filePath, (plannedCountByFile.get(req.filePath) ?? 0) + 1);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const connected = await mod.LspClient.connect({ launchSpec, rootUri, timeoutMs });
|
|
960
|
+
if (!connected.ok) {
|
|
961
|
+
// A missing/misconfigured server is a DIAGNOSABLE condition, not a
|
|
962
|
+
// silent count (review thread): record the degradation so
|
|
963
|
+
// codegraph_index can distinguish "server absent" from "LSP found
|
|
964
|
+
// no definitions".
|
|
965
|
+
const deg = connected.degradation;
|
|
966
|
+
const code =
|
|
967
|
+
deg && typeof deg === "object" && "code" in deg && typeof deg.code === "string" ? deg.code : "connect_failed";
|
|
968
|
+
const message =
|
|
969
|
+
deg && typeof deg === "object" && "detail" in deg && typeof deg.detail === "string"
|
|
970
|
+
? deg.detail
|
|
971
|
+
: `LSP server for ${language} failed to connect.`;
|
|
972
|
+
degradations.push({ language, code, message });
|
|
973
|
+
unresolved += plan.requests.length;
|
|
974
|
+
// The planned requests never reached a server — hand their budget
|
|
975
|
+
// back so a missing server for one language cannot starve the
|
|
976
|
+
// remaining language groups (review thread).
|
|
977
|
+
remainingBudget += plan.requests.length;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const client = connected.client;
|
|
981
|
+
// Skip tracking keyed by FILE (via the upgrades' caller symbols):
|
|
982
|
+
// a skip must (a) not count as an upgrade and (b) suppress
|
|
983
|
+
// reconciliation for exactly the file batch that observed it — a
|
|
984
|
+
// boolean flag went stale whenever the executor skipped a batch's
|
|
985
|
+
// reconcile (non-exhaustive) or never called apply (zero upgrades)
|
|
986
|
+
// (review threads).
|
|
987
|
+
let skippedEdgesTotal = 0;
|
|
988
|
+
const skippedFiles = new Set<string>();
|
|
989
|
+
try {
|
|
990
|
+
const result = await mod.executeLspResolution(plan.requests, {
|
|
991
|
+
client,
|
|
992
|
+
workspaceRoot: repoRoot,
|
|
993
|
+
resolveContent,
|
|
994
|
+
nodeLocator: (filePath: string, byteOffset: number) =>
|
|
995
|
+
typeof params.store.findNodeBySpan === "function" ? params.store.findNodeBySpan(filePath, byteOffset) : null,
|
|
996
|
+
applyUpgrades: async (upgrades: readonly CodegraphEdgeInput[]) => {
|
|
997
|
+
if (typeof params.store.upsertEdges !== "function") {
|
|
998
|
+
throw new Error("store.upsertEdges is not available");
|
|
999
|
+
}
|
|
1000
|
+
const result = await params.store.upsertEdges(upgrades);
|
|
1001
|
+
// Throw ONLY when nothing persisted (ok:false) — the executor's
|
|
1002
|
+
// transactional contract holds because no state changed. A
|
|
1003
|
+
// skipped>0 result has ALREADY committed the resolvable edges in
|
|
1004
|
+
// one transaction, so throwing after the fact would desync the
|
|
1005
|
+
// executor's view from the DB (skipped reconciliation +
|
|
1006
|
+
// undercounted upgrades while edges persist — review thread).
|
|
1007
|
+
// Skips are surfaced as a degradation instead of a silent drop.
|
|
1008
|
+
if (!result.ok) {
|
|
1009
|
+
// The executor catches this throw and counts the batch as
|
|
1010
|
+
// unresolved — record the failure as a degradation FIRST so
|
|
1011
|
+
// a failed write (store_closed, SQLite error) is diagnosable
|
|
1012
|
+
// from the index summary, not indistinguishable from
|
|
1013
|
+
// "no definitions found" (review thread).
|
|
1014
|
+
degradations.push({
|
|
1015
|
+
language,
|
|
1016
|
+
code: "edge_write_failed",
|
|
1017
|
+
message: `upsertEdges failed for ${language}: ${result.code}.`,
|
|
1018
|
+
});
|
|
1019
|
+
throw new Error(`upsertEdges failed: ${result.code}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (result.skipped > 0) {
|
|
1022
|
+
skippedEdgesTotal += result.skipped;
|
|
1023
|
+
for (const upgrade of upgrades) {
|
|
1024
|
+
for (const file of srcToFiles.get(upgrade.srcQualifiedName) ?? []) {
|
|
1025
|
+
skippedFiles.add(file);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
degradations.push({
|
|
1029
|
+
language,
|
|
1030
|
+
code: "edges_skipped",
|
|
1031
|
+
message: `upsertEdges persisted ${result.persisted} and skipped ${result.skipped} edge(s) for ${language} (unresolvable endpoints).`,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
reconcileLspEdges: (
|
|
1036
|
+
filePath: string,
|
|
1037
|
+
assertedEdges: ReadonlyArray<{ srcQualifiedName: string; dstQualifiedName: string; type: string }>,
|
|
1038
|
+
) => {
|
|
1039
|
+
if (typeof params.store.reconcileLspEdges !== "function") return;
|
|
1040
|
+
if (plannedCountByFile.get(filePath) !== gatheredCountByFile.get(filePath)) return;
|
|
1041
|
+
// This file's batch had skipped writes — its asserted set is
|
|
1042
|
+
// incomplete, so reconciling would retire edges that were never
|
|
1043
|
+
// re-asserted (see skip tracking above).
|
|
1044
|
+
if (skippedFiles.has(filePath)) return;
|
|
1045
|
+
// Heuristic-filtered sites were NOT re-queried: their current
|
|
1046
|
+
// lsp edges are appended to the asserted set so they survive
|
|
1047
|
+
// while stale edges for RE-QUERIED sites still get retired.
|
|
1048
|
+
// Without the preservation seam, skip (conservative).
|
|
1049
|
+
if ((filteredCountByFile.get(filePath) ?? 0) > 0 && !canPreserve) return;
|
|
1050
|
+
params.store.reconcileLspEdges(filePath, [...assertedEdges, ...preservedEdgesForFile(filePath)]);
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
if (result && typeof result === "object" && "upgraded" in result && typeof result.upgraded === "number") {
|
|
1054
|
+
// The executor counts every upgrade it HANDED to applyUpgrades;
|
|
1055
|
+
// subtract the writes the store skipped so the reported number
|
|
1056
|
+
// reflects edges that actually persisted (review thread).
|
|
1057
|
+
upgraded += Math.max(0, result.upgraded - skippedEdgesTotal);
|
|
1058
|
+
unresolved += "unresolved" in result && typeof result.unresolved === "number" ? result.unresolved : 0;
|
|
1059
|
+
budgetExhausted += "budgetExhausted" in result && typeof result.budgetExhausted === "number" ? result.budgetExhausted : 0;
|
|
1060
|
+
// A mid-run server crash / protocol error returns counts PLUS a
|
|
1061
|
+
// degradation tag — dropping it would make the failure look like
|
|
1062
|
+
// ordinary unresolved calls (review thread).
|
|
1063
|
+
if ("degradation" in result && result.degradation && typeof result.degradation === "object") {
|
|
1064
|
+
const rd = result.degradation;
|
|
1065
|
+
degradations.push({
|
|
1066
|
+
language,
|
|
1067
|
+
code: "code" in rd && typeof rd.code === "string" ? rd.code : "lsp_degraded",
|
|
1068
|
+
message: "detail" in rd && typeof rd.detail === "string" ? rd.detail : `LSP pass for ${language} degraded mid-run.`,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
unresolved += plan.requests.length;
|
|
1073
|
+
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
// Sanitized detail only (class name + Node error code) — this
|
|
1076
|
+
// message reaches client surfaces via result.lsp and must not leak
|
|
1077
|
+
// absolute paths or stack fragments (review thread; mirrors
|
|
1078
|
+
// displayErrorDetail's contract). The full error is recoverable
|
|
1079
|
+
// from server logs, not from the tool response.
|
|
1080
|
+
const detail = displayErrorDetail(err);
|
|
1081
|
+
return {
|
|
1082
|
+
ok: false,
|
|
1083
|
+
code: "lsp_resolution_error",
|
|
1084
|
+
message: detail.length > 0 ? `LSP resolution failed: ${detail}.` : "LSP resolution failed.",
|
|
1085
|
+
};
|
|
1086
|
+
} finally {
|
|
1087
|
+
const disposable = client as { dispose?: () => Promise<void> };
|
|
1088
|
+
if (typeof disposable.dispose === "function") {
|
|
1089
|
+
try {
|
|
1090
|
+
await disposable.dispose();
|
|
1091
|
+
} catch {
|
|
1092
|
+
// Disposal failure is not actionable — the child process is
|
|
1093
|
+
// reaped by the OS when the parent exits.
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
ok: true,
|
|
1100
|
+
upgraded,
|
|
1101
|
+
unresolved,
|
|
1102
|
+
budgetExhausted,
|
|
1103
|
+
...(degradations.length > 0 ? { degradations } : {}),
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
569
1107
|
/**
|
|
570
1108
|
* Report index status via @remnic/coding-graph's getIndexStatus. Never throws
|
|
571
1109
|
* — a git failure degrades to `mode: "git_unavailable"` in the status body.
|
|
@@ -878,3 +878,78 @@ test("resolveCodegraphDbPath: accepts an absolute codegraphDbDir", () => {
|
|
|
878
878
|
assert.ok(p.startsWith("/var/lib/remnic/codegraph/"), p);
|
|
879
879
|
assert.ok(p.endsWith(".sqlite"), p);
|
|
880
880
|
});
|
|
881
|
+
|
|
882
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
883
|
+
// LSP Phase B wiring (issue #1917): handleIndex invokes runLspResolution
|
|
884
|
+
// after a successful reindex iff codingKnowledge.lsp.enabled — and LSP
|
|
885
|
+
// failure is non-fatal (heuristic edges stand).
|
|
886
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
887
|
+
|
|
888
|
+
function makeLspCtx(
|
|
889
|
+
lsp: { enabled: boolean } | undefined,
|
|
890
|
+
overrides: Partial<CodegraphSurfaceContext> = {},
|
|
891
|
+
): CodegraphSurfaceContext {
|
|
892
|
+
const pluginConfig = {
|
|
893
|
+
codingKnowledge: { ...GATE_ON_CONFIG, lsp },
|
|
894
|
+
} as unknown as PluginConfig;
|
|
895
|
+
return makeEnabledCtx({ config: pluginConfig, ...overrides });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
test("LSP wiring: handleIndex invokes runLspResolution after reindex when lsp.enabled", async () => {
|
|
899
|
+
const calls: Array<{ repoRoot: string; lspEnabled: boolean }> = [];
|
|
900
|
+
const ctx = makeLspCtx(
|
|
901
|
+
{ enabled: true },
|
|
902
|
+
{
|
|
903
|
+
runReindex: async () => ({ ok: true, mode: "full", filesIngested: 2, head: "h1" }),
|
|
904
|
+
runLspResolution: async (_store, repoRoot, lspConfig) => {
|
|
905
|
+
calls.push({ repoRoot, lspEnabled: lspConfig.enabled });
|
|
906
|
+
return { ok: true, upgraded: 4, unresolved: 1, budgetExhausted: 0 };
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
);
|
|
910
|
+
const response = await handleCodegraphTool({ tool: "index", repoRoot: "/repo", mode: "auto" }, ctx);
|
|
911
|
+
assert.equal(response.ok, true);
|
|
912
|
+
assert.equal(calls.length, 1, "runLspResolution must be invoked exactly once");
|
|
913
|
+
assert.equal(calls[0]?.repoRoot, "/repo", "repoRoot forwards to the LSP pass");
|
|
914
|
+
assert.equal(calls[0]?.lspEnabled, true, "the parsed lsp config forwards");
|
|
915
|
+
if (!response.ok) throw new Error("expected ok");
|
|
916
|
+
const result = response.result as { lsp?: { upgraded: number; unresolved: number } };
|
|
917
|
+
assert.equal(result.lsp?.upgraded, 4, "LSP upgrade summary surfaces in the index result");
|
|
918
|
+
assert.equal(result.lsp?.unresolved, 1);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test("LSP wiring: handleIndex does NOT invoke runLspResolution when lsp is disabled or absent", async () => {
|
|
922
|
+
for (const lsp of [{ enabled: false }, undefined]) {
|
|
923
|
+
let called = false;
|
|
924
|
+
const ctx = makeLspCtx(lsp, {
|
|
925
|
+
runReindex: async () => ({ ok: true, mode: "full", filesIngested: 1, head: "h2" }),
|
|
926
|
+
runLspResolution: async () => {
|
|
927
|
+
called = true;
|
|
928
|
+
return { ok: true, upgraded: 0, unresolved: 0, budgetExhausted: 0 };
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
const response = await handleCodegraphTool({ tool: "index", repoRoot: "/repo", mode: "auto" }, ctx);
|
|
932
|
+
assert.equal(response.ok, true);
|
|
933
|
+
assert.equal(called, false, `lsp=${JSON.stringify(lsp)} must not trigger the LSP pass`);
|
|
934
|
+
if (!response.ok) throw new Error("expected ok");
|
|
935
|
+
const result = response.result as { lsp?: unknown };
|
|
936
|
+
assert.equal(result.lsp, undefined, "no LSP summary when the pass did not run");
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("LSP wiring: a failed LSP pass is non-fatal — index still reports ok with reindex stats", async () => {
|
|
941
|
+
const ctx = makeLspCtx(
|
|
942
|
+
{ enabled: true },
|
|
943
|
+
{
|
|
944
|
+
runReindex: async () => ({ ok: true, mode: "incremental", filesIngested: 5, head: "h3" }),
|
|
945
|
+
runLspResolution: async () => ({ ok: false, code: "lsp_resolution_error", message: "server crashed" }),
|
|
946
|
+
},
|
|
947
|
+
);
|
|
948
|
+
const response = await handleCodegraphTool({ tool: "index", repoRoot: "/repo", mode: "auto" }, ctx);
|
|
949
|
+
assert.equal(response.ok, true, "LSP failure must not fail the index (heuristic edges stand)");
|
|
950
|
+
if (!response.ok) throw new Error("expected ok");
|
|
951
|
+
const result = response.result as { filesIngested: number; lsp?: { error?: string; message?: string } };
|
|
952
|
+
assert.equal(result.filesIngested, 5, "reindex stats survive the failed LSP pass");
|
|
953
|
+
assert.equal(result.lsp?.error, "lsp_resolution_error", "the degradation code surfaces (never silent)");
|
|
954
|
+
assert.equal(result.lsp?.message, "server crashed", "the degradation message surfaces");
|
|
955
|
+
});
|