@remnic/coding-graph 9.6.23 → 9.6.25
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/README.md +16 -19
- package/dist/{chunk-CPYJACC5.js → chunk-ABDBWCXU.js} +146 -8
- package/dist/chunk-ABDBWCXU.js.map +1 -0
- package/dist/{chunk-5I2DBHOQ.js → chunk-I4R6GAAA.js} +2 -2
- package/dist/cypher/query-parser.js +2 -2
- package/dist/graph-store.d.ts +47 -0
- package/dist/graph-store.js +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +318 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/edge-provenance-scope.test.ts +457 -0
- package/src/engine/emit.ts +55 -8
- package/src/engine/extractors.ts +45 -11
- package/src/graph-store.ts +265 -9
- package/src/heuristic-resolution.test.ts +596 -0
- package/src/heuristic-resolution.ts +395 -0
- package/src/lsp/resolution.ts +52 -11
- package/src/lsp-reconciliation.test.ts +145 -0
- package/src/reindex.test.ts +57 -0
- package/src/reindex.ts +18 -6
- package/dist/chunk-CPYJACC5.js.map +0 -1
- /package/dist/{chunk-5I2DBHOQ.js.map → chunk-I4R6GAAA.js.map} +0 -0
package/src/engine/extractors.ts
CHANGED
|
@@ -56,15 +56,25 @@ const JS_FAMILY_DEFINITIONS = `
|
|
|
56
56
|
const JS_IMPORTS = `
|
|
57
57
|
(import_statement
|
|
58
58
|
source: (string (string_fragment) @import.module)) @__import.stmt
|
|
59
|
+
; Default and namespace imports bind LOCAL aliases whose exported symbol
|
|
60
|
+
; is unknowable to Phase A (issue #1894 round 12): they appear in
|
|
61
|
+
; importedNames (informational) but never in bindings (call-bindable).
|
|
59
62
|
(import_statement
|
|
60
|
-
(import_clause (identifier) @import.
|
|
63
|
+
(import_clause (identifier) @import.unboundName)
|
|
61
64
|
source: (string (string_fragment) @import.module)) @__import.stmt
|
|
62
65
|
(import_statement
|
|
63
|
-
(import_clause (namespace_import (identifier) @import.
|
|
66
|
+
(import_clause (namespace_import (identifier) @import.unboundName))
|
|
64
67
|
source: (string (string_fragment) @import.module)) @__import.stmt
|
|
65
68
|
(import_statement
|
|
66
69
|
(import_clause (named_imports (import_specifier name: (identifier) @import.name)))
|
|
67
70
|
source: (string (string_fragment) @import.module)) @__import.stmt
|
|
71
|
+
; Aliased named import (issue #1894 review): import { foo as bar } — the
|
|
72
|
+
; local binding is the alias; the exported name stays the binding target.
|
|
73
|
+
(import_statement
|
|
74
|
+
(import_clause (named_imports (import_specifier
|
|
75
|
+
name: (identifier) @import.aliasExported
|
|
76
|
+
alias: (identifier) @import.aliasLocal)))
|
|
77
|
+
source: (string (string_fragment) @import.module)) @__import.stmt
|
|
68
78
|
|
|
69
79
|
; CommonJS require("...") — capture the module specifier so dependency
|
|
70
80
|
; edges exist for Node/CommonJS codebases, not just ES-module imports.
|
|
@@ -130,7 +140,10 @@ const JS_EXPORTS = `
|
|
|
130
140
|
|
|
131
141
|
const JS_CALLS = `
|
|
132
142
|
(call_expression function: (identifier) @call.callee)
|
|
133
|
-
|
|
143
|
+
; Member calls are captured separately (issue #1894 review): obj.save()'s
|
|
144
|
+
; \`save\` must never bind a bare visible symbol — method dispatch is
|
|
145
|
+
; Phase B (LSP) territory. The emit layer marks these memberAccess: true.
|
|
146
|
+
(call_expression function: (member_expression property: (property_identifier) @call.member))
|
|
134
147
|
`.trim();
|
|
135
148
|
|
|
136
149
|
const JS_ROUTES = `
|
|
@@ -187,6 +200,17 @@ const PYTHON_EXTRACTOR: LanguageExtractor = {
|
|
|
187
200
|
importsQuery: `
|
|
188
201
|
(import_statement (dotted_name) @import.module) @__import.stmt
|
|
189
202
|
(import_from_statement module_name: (dotted_name) @import.module) @__import.stmt
|
|
203
|
+
; Imported names (issue #1894 review round 7): without these captures,
|
|
204
|
+
; extractImports emits importedNames: [] and the heuristic resolver's
|
|
205
|
+
; import bindings never fire for real Python parses.
|
|
206
|
+
(import_from_statement
|
|
207
|
+
module_name: (dotted_name) @import.module
|
|
208
|
+
name: (dotted_name) @import.name) @__import.stmt
|
|
209
|
+
(import_from_statement
|
|
210
|
+
module_name: (dotted_name) @import.module
|
|
211
|
+
name: (aliased_import
|
|
212
|
+
name: (dotted_name) @import.aliasExported
|
|
213
|
+
alias: (identifier) @import.aliasLocal)) @__import.stmt
|
|
190
214
|
; Python relative imports: from .models import User / from ..parent import X
|
|
191
215
|
; tree-sitter-python wraps the module inside a relative_import node, so the
|
|
192
216
|
; module_name field is NOT set. Capture the relative_import node itself so
|
|
@@ -194,11 +218,19 @@ const PYTHON_EXTRACTOR: LanguageExtractor = {
|
|
|
194
218
|
; relative levels must not collapse to the same module name
|
|
195
219
|
; (chatgpt-codex-connector #1688 P2: 'Preserve dots in Python relative imports').
|
|
196
220
|
(import_from_statement (relative_import) @import.module) @__import.stmt
|
|
221
|
+
(import_from_statement
|
|
222
|
+
(relative_import) @import.module
|
|
223
|
+
name: (dotted_name) @import.name) @__import.stmt
|
|
224
|
+
(import_from_statement
|
|
225
|
+
(relative_import) @import.module
|
|
226
|
+
name: (aliased_import
|
|
227
|
+
name: (dotted_name) @import.aliasExported
|
|
228
|
+
alias: (identifier) @import.aliasLocal)) @__import.stmt
|
|
197
229
|
`.trim(),
|
|
198
230
|
exportsQuery: ``,
|
|
199
231
|
callSitesQuery: `
|
|
200
232
|
(call function: (identifier) @call.callee)
|
|
201
|
-
(call function: (attribute attribute: (identifier) @call.
|
|
233
|
+
(call function: (attribute attribute: (identifier) @call.member))
|
|
202
234
|
`.trim(),
|
|
203
235
|
routesQuery: `
|
|
204
236
|
(decorated_definition
|
|
@@ -235,7 +267,7 @@ const GO_EXTRACTOR: LanguageExtractor = {
|
|
|
235
267
|
exportsQuery: ``,
|
|
236
268
|
callSitesQuery: `
|
|
237
269
|
(call_expression function: (identifier) @call.callee)
|
|
238
|
-
(call_expression function: (selector_expression field: (field_identifier) @call.
|
|
270
|
+
(call_expression function: (selector_expression field: (field_identifier) @call.member))
|
|
239
271
|
`.trim(),
|
|
240
272
|
routesQuery: ``,
|
|
241
273
|
};
|
|
@@ -271,7 +303,7 @@ const RUST_EXTRACTOR: LanguageExtractor = {
|
|
|
271
303
|
exportsQuery: ``,
|
|
272
304
|
callSitesQuery: `
|
|
273
305
|
(call_expression function: (identifier) @call.callee)
|
|
274
|
-
(call_expression function: (field_expression field: (field_identifier) @call.
|
|
306
|
+
(call_expression function: (field_expression field: (field_identifier) @call.member))
|
|
275
307
|
(call_expression function: (scoped_identifier) @call.callee)
|
|
276
308
|
`.trim(),
|
|
277
309
|
routesQuery: ``,
|
|
@@ -291,7 +323,8 @@ const JAVA_EXTRACTOR: LanguageExtractor = {
|
|
|
291
323
|
`.trim(),
|
|
292
324
|
exportsQuery: ``,
|
|
293
325
|
callSitesQuery: `
|
|
294
|
-
(method_invocation name: (identifier) @call.callee)
|
|
326
|
+
(method_invocation !object name: (identifier) @call.callee)
|
|
327
|
+
(method_invocation object: (_) name: (identifier) @call.member)
|
|
295
328
|
`.trim(),
|
|
296
329
|
routesQuery: ``,
|
|
297
330
|
};
|
|
@@ -331,7 +364,7 @@ const CPP_EXTRACTOR: LanguageExtractor = {
|
|
|
331
364
|
exportsQuery: ``,
|
|
332
365
|
callSitesQuery: `
|
|
333
366
|
(call_expression function: (identifier) @call.callee)
|
|
334
|
-
(call_expression function: (field_expression field: (field_identifier) @call.
|
|
367
|
+
(call_expression function: (field_expression field: (field_identifier) @call.member))
|
|
335
368
|
`.trim(),
|
|
336
369
|
routesQuery: ``,
|
|
337
370
|
};
|
|
@@ -351,7 +384,7 @@ const CSHARP_EXTRACTOR: LanguageExtractor = {
|
|
|
351
384
|
exportsQuery: ``,
|
|
352
385
|
callSitesQuery: `
|
|
353
386
|
(invocation_expression function: (identifier) @call.callee)
|
|
354
|
-
(invocation_expression function: (member_access_expression name: (identifier) @call.
|
|
387
|
+
(invocation_expression function: (member_access_expression name: (identifier) @call.member))
|
|
355
388
|
`.trim(),
|
|
356
389
|
routesQuery: ``,
|
|
357
390
|
};
|
|
@@ -371,7 +404,8 @@ const RUBY_EXTRACTOR: LanguageExtractor = {
|
|
|
371
404
|
`.trim(),
|
|
372
405
|
exportsQuery: ``,
|
|
373
406
|
callSitesQuery: `
|
|
374
|
-
(call method: (identifier) @call.callee)
|
|
407
|
+
(call !receiver method: (identifier) @call.callee)
|
|
408
|
+
(call receiver: (_) method: (identifier) @call.member)
|
|
375
409
|
`.trim(),
|
|
376
410
|
routesQuery: ``,
|
|
377
411
|
};
|
|
@@ -390,7 +424,7 @@ const PHP_EXTRACTOR: LanguageExtractor = {
|
|
|
390
424
|
exportsQuery: ``,
|
|
391
425
|
callSitesQuery: `
|
|
392
426
|
(function_call_expression function: (name) @call.callee)
|
|
393
|
-
(member_call_expression (name) @call.
|
|
427
|
+
(member_call_expression (name) @call.member)
|
|
394
428
|
`.trim(),
|
|
395
429
|
routesQuery: ``,
|
|
396
430
|
};
|
package/src/graph-store.ts
CHANGED
|
@@ -134,6 +134,23 @@ export interface EdgeIR {
|
|
|
134
134
|
readonly srcNodeId?: string;
|
|
135
135
|
/** Optional content-derived destination node id — see {@link EdgeIR.srcNodeId}. */
|
|
136
136
|
readonly dstNodeId?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Repo-relative, extension-stripped path the dst must live in (issue
|
|
139
|
+
* #1894 review): derived from a relative import's module specifier. A
|
|
140
|
+
* hinted edge resolves ONLY among nodes whose file path matches the
|
|
141
|
+
* hint (`<hint>`, `<hint>.<ext>`, `<hint>/index.<ext>`, or
|
|
142
|
+
* `<hint>/__init__.<ext>`) — never via
|
|
143
|
+
* the global bare-name fallback — so `import { foo } from "./missing"`
|
|
144
|
+
* can never bind an unrelated same-named symbol elsewhere in the repo.
|
|
145
|
+
*/
|
|
146
|
+
readonly dstPathHint?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Language of the importing file (issue #1894 round 13): constrains the
|
|
149
|
+
* hinted dst's file extension to that language's module-resolution set
|
|
150
|
+
* so a polyglot repo cannot cross-bind a JS import to a same-named .py
|
|
151
|
+
* file.
|
|
152
|
+
*/
|
|
153
|
+
readonly dstImporterLanguage?: string;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
/**
|
|
@@ -159,6 +176,16 @@ export interface StoreFileIR {
|
|
|
159
176
|
readonly symbols: readonly SymbolIR[];
|
|
160
177
|
/** Store-specific edges derived from the IR by the caller. */
|
|
161
178
|
readonly edges?: readonly EdgeIR[];
|
|
179
|
+
/**
|
|
180
|
+
* When present, the stale-edge delete in `upsertFileEdges` is scoped to
|
|
181
|
+
* edges whose provenance is in this list: prior src-owned edges of OTHER
|
|
182
|
+
* provenances survive un-asserted (issue #1891). The reindex pipeline
|
|
183
|
+
* asserts `["heuristic"]` because a fresh parse says nothing about
|
|
184
|
+
* `trace`/`lsp` edges; deleting them on every re-ingest would destroy
|
|
185
|
+
* state the parse never contradicted (rule 25). Absent = legacy
|
|
186
|
+
* behavior: every stale src-owned edge is deleted.
|
|
187
|
+
*/
|
|
188
|
+
readonly assertedEdgeProvenances?: readonly EdgeProvenance[];
|
|
162
189
|
/**
|
|
163
190
|
* Per-file export list (mirrors core FileIR.exports). When present,
|
|
164
191
|
* the write pipeline marks every node in this file whose `name`
|
|
@@ -898,6 +925,105 @@ export class GraphStore {
|
|
|
898
925
|
return this.queue.schedule(() => this.runUpsertEdges(edges));
|
|
899
926
|
}
|
|
900
927
|
|
|
928
|
+
/**
|
|
929
|
+
* Retire stale LSP-provenance edges for a file (issue #1895).
|
|
930
|
+
*
|
|
931
|
+
* The LSP resolution pass re-derives edges from the CURRENT source on each
|
|
932
|
+
* run. After writing the new `lsp` edges for a file, this method deletes
|
|
933
|
+
* prior `lsp`-provenance edges owned by that file's nodes whose
|
|
934
|
+
* `(src, dst, type)` key is NOT in the asserted set. This is the LSP
|
|
935
|
+
* layer's side of the provenance-lifecycle contract: each layer owns its
|
|
936
|
+
* own stale-edge retirement (#1894 established that reindex's heuristic
|
|
937
|
+
* scope never touches `lsp` rows).
|
|
938
|
+
*
|
|
939
|
+
* Heuristic, trace, and semantic edges are never touched.
|
|
940
|
+
*
|
|
941
|
+
* @returns the number of retired edges.
|
|
942
|
+
*/
|
|
943
|
+
reconcileLspEdges(
|
|
944
|
+
filePath: string,
|
|
945
|
+
assertedEdges: ReadonlyArray<{
|
|
946
|
+
srcQualifiedName: string;
|
|
947
|
+
dstQualifiedName: string;
|
|
948
|
+
type: string;
|
|
949
|
+
}>,
|
|
950
|
+
): number {
|
|
951
|
+
if (this.closed) return 0;
|
|
952
|
+
try {
|
|
953
|
+
// Resolve the file and its nodes.
|
|
954
|
+
const fileRow = expectRow<{ id: number }>(
|
|
955
|
+
this.db.prepare("SELECT id FROM files WHERE path = ?").get(filePath),
|
|
956
|
+
["id"],
|
|
957
|
+
);
|
|
958
|
+
if (!fileRow) return 0;
|
|
959
|
+
const nodes = expectRows<{ id: string; qualified_name: string }>(
|
|
960
|
+
this.db
|
|
961
|
+
.prepare("SELECT id, qualified_name FROM nodes WHERE file_id = ?")
|
|
962
|
+
.all(fileRow.id),
|
|
963
|
+
["id", "qualified_name"],
|
|
964
|
+
);
|
|
965
|
+
if (nodes.length === 0) return 0;
|
|
966
|
+
|
|
967
|
+
// Build srcQualifiedName → nodeId for this file's nodes. Only
|
|
968
|
+
// include names that appear exactly once (same conservative
|
|
969
|
+
// ambiguity policy as upsertFileEdges — cursor review on #1914).
|
|
970
|
+
const nameCount = new Map<string, number>();
|
|
971
|
+
for (const n of nodes) nameCount.set(n.qualified_name, (nameCount.get(n.qualified_name) ?? 0) + 1);
|
|
972
|
+
const srcMap = new Map<string, string>();
|
|
973
|
+
for (const n of nodes) {
|
|
974
|
+
if (nameCount.get(n.qualified_name) === 1) srcMap.set(n.qualified_name, n.id);
|
|
975
|
+
}
|
|
976
|
+
const nodeIds = nodes.map((n) => n.id);
|
|
977
|
+
|
|
978
|
+
// Build the asserted key set (resolved to node IDs).
|
|
979
|
+
const assertedKeys = new Set<string>();
|
|
980
|
+
for (const e of assertedEdges) {
|
|
981
|
+
const srcId = srcMap.get(e.srcQualifiedName);
|
|
982
|
+
if (!srcId) continue;
|
|
983
|
+
const dstId = resolveNodeId(e.dstQualifiedName, new Map(), this.db);
|
|
984
|
+
if (!dstId) continue;
|
|
985
|
+
assertedKeys.add(`${srcId}\u0000${dstId}\u0000${e.type}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Find prior lsp edges owned by this file's nodes.
|
|
989
|
+
const placeholders = nodeIds.map(() => "?").join(", ");
|
|
990
|
+
const priorEdges = expectRows<{ src: string; dst: string; type: string }>(
|
|
991
|
+
this.db
|
|
992
|
+
.prepare(
|
|
993
|
+
`SELECT src, dst, type FROM edges
|
|
994
|
+
WHERE src IN (${placeholders}) AND provenance = 'lsp'`,
|
|
995
|
+
)
|
|
996
|
+
.all(...nodeIds),
|
|
997
|
+
["src", "dst", "type"],
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
// Delete those NOT in the asserted set.
|
|
1001
|
+
let deleted = 0;
|
|
1002
|
+
const toDelete: Array<[string, string, string]> = [];
|
|
1003
|
+
for (const e of priorEdges) {
|
|
1004
|
+
const key = `${e.src}\u0000${e.dst}\u0000${e.type}`;
|
|
1005
|
+
if (!assertedKeys.has(key)) toDelete.push([e.src, e.dst, e.type]);
|
|
1006
|
+
}
|
|
1007
|
+
if (toDelete.length > 0) {
|
|
1008
|
+
const SQLITE_VARIABLE_LIMIT = 32_766;
|
|
1009
|
+
const PARAMS_PER_TUPLE = 3;
|
|
1010
|
+
const MAX_TUPLES = Math.floor(SQLITE_VARIABLE_LIMIT / PARAMS_PER_TUPLE);
|
|
1011
|
+
for (let i = 0; i < toDelete.length; i += MAX_TUPLES) {
|
|
1012
|
+
const chunk = toDelete.slice(i, i + MAX_TUPLES);
|
|
1013
|
+
const ph = chunk.map(() => "(?, ?, ?)").join(", ");
|
|
1014
|
+
const r = this.db
|
|
1015
|
+
.prepare(`DELETE FROM edges WHERE (src, dst, type) IN (${ph})`)
|
|
1016
|
+
.run(...chunk.flat());
|
|
1017
|
+
deleted += r.changes;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return deleted;
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
logWriteFailure(error);
|
|
1023
|
+
return 0;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
901
1027
|
/** Wait for pending writes to drain — test seam. */
|
|
902
1028
|
async drain(): Promise<void> {
|
|
903
1029
|
await this.queue.drain();
|
|
@@ -1752,12 +1878,18 @@ export class GraphStore {
|
|
|
1752
1878
|
// re-ingest (chatgpt-codex-connector P2).
|
|
1753
1879
|
const srcId = qualifiedNameToId.get(edge.srcQualifiedName);
|
|
1754
1880
|
if (!srcId) continue;
|
|
1755
|
-
// DST may be cross-file
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1881
|
+
// DST may be cross-file. A path-hinted edge (relative import,
|
|
1882
|
+
// issue #1894 review) resolves ONLY within its declared target
|
|
1883
|
+
// file — never via the global bare-name fallback; unhinted edges
|
|
1884
|
+
// keep the batch-map + full-DB fallback.
|
|
1885
|
+
const dstId = edge.dstPathHint
|
|
1886
|
+
? resolveNodeIdWithPathHint(
|
|
1887
|
+
edge.dstQualifiedName,
|
|
1888
|
+
edge.dstPathHint,
|
|
1889
|
+
this.db,
|
|
1890
|
+
edge.dstImporterLanguage,
|
|
1891
|
+
)
|
|
1892
|
+
: resolveNodeId(edge.dstQualifiedName, qualifiedNameToId, this.db);
|
|
1761
1893
|
if (!dstId) continue;
|
|
1762
1894
|
const key = `${srcId}\u0000${dstId}\u0000${edge.type}`;
|
|
1763
1895
|
assertedKeys.add(key);
|
|
@@ -1788,12 +1920,24 @@ export class GraphStore {
|
|
|
1788
1920
|
);
|
|
1789
1921
|
const priorByKey = new Map<string, { confidence: number; provenance: string }>();
|
|
1790
1922
|
const staleSrcDstTypes: Array<{ src: string; dst: string; type: string }> = [];
|
|
1923
|
+
// Provenance scoping (issue #1891): when the IR declares which
|
|
1924
|
+
// provenances it asserts, stale edges of OTHER provenances are not
|
|
1925
|
+
// this ingest's to delete — a fresh parse contradicts only its own
|
|
1926
|
+
// derivation class (rule 25). Absent = legacy delete-all-stale.
|
|
1927
|
+
// An EMPTY scope array is treated as absent (cursor review round 9):
|
|
1928
|
+
// "[]" would otherwise be truthy, protect every prior edge from
|
|
1929
|
+
// deletion AND from updates, and silently disable cleanup — an
|
|
1930
|
+
// assertion that scopes nothing scopes nothing.
|
|
1931
|
+
const scoped =
|
|
1932
|
+
ir.assertedEdgeProvenances && ir.assertedEdgeProvenances.length > 0
|
|
1933
|
+
? ir.assertedEdgeProvenances
|
|
1934
|
+
: undefined;
|
|
1791
1935
|
for (const p of priorEdges) {
|
|
1792
1936
|
const key = `${p.src}\u0000${p.dst}\u0000${p.type}`;
|
|
1793
1937
|
priorByKey.set(key, { confidence: p.confidence, provenance: p.provenance });
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1938
|
+
if (assertedKeys.has(key)) continue;
|
|
1939
|
+
if (scoped && !(scoped as readonly string[]).includes(p.provenance)) continue;
|
|
1940
|
+
staleSrcDstTypes.push({ src: p.src, dst: p.dst, type: p.type });
|
|
1797
1941
|
}
|
|
1798
1942
|
|
|
1799
1943
|
// Delete only the stale src-owned edges (prior-but-not-asserted).
|
|
@@ -1852,6 +1996,27 @@ export class GraphStore {
|
|
|
1852
1996
|
// stale keys.
|
|
1853
1997
|
continue;
|
|
1854
1998
|
}
|
|
1999
|
+
// Two update-path protections under provenance scoping (issue
|
|
2000
|
+
// #1891 + #1894 review rounds):
|
|
2001
|
+
// 1. a prior row whose provenance is OUTSIDE the asserted scope
|
|
2002
|
+
// is not this assertion's to modify (defense in depth — in
|
|
2003
|
+
// practice cross-provenance key collisions are heuristic/lsp
|
|
2004
|
+
// only, handled by 2);
|
|
2005
|
+
// 2. an lsp row is a strictly stronger derivation of the SAME
|
|
2006
|
+
// source-derived edge: re-asserting the heuristic key keeps
|
|
2007
|
+
// the row alive (it is not stale) but must never downgrade it.
|
|
2008
|
+
// Retirement of lsp rows happens through the stale-delete when
|
|
2009
|
+
// the call disappears from the parse — lsp IS in the reindex
|
|
2010
|
+
// assertion scope precisely so vanished calls retire their
|
|
2011
|
+
// upgraded rows too.
|
|
2012
|
+
if (
|
|
2013
|
+
prior &&
|
|
2014
|
+
scoped &&
|
|
2015
|
+
(!(scoped as readonly string[]).includes(prior.provenance) ||
|
|
2016
|
+
(prior.provenance === "lsp" && edge.provenance === "heuristic"))
|
|
2017
|
+
) {
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
1855
2020
|
const r = insertEdge.run(srcId, dstId, edge.type, edge.confidence, edge.provenance);
|
|
1856
2021
|
edgeCount += r.changes;
|
|
1857
2022
|
}
|
|
@@ -3289,6 +3454,97 @@ function resolveNodeId(
|
|
|
3289
3454
|
return rows[0]?.id;
|
|
3290
3455
|
}
|
|
3291
3456
|
|
|
3457
|
+
/**
|
|
3458
|
+
* Resolve a dst node constrained to a path hint (issue #1894 review): the
|
|
3459
|
+
* node's file path must be the hint verbatim, `<hint>.<ext>`,
|
|
3460
|
+
* `<hint>/index.<ext>`, or `<hint>/__init__.<ext>` (Python packages) —
|
|
3461
|
+
* where `<ext>` is a SINGLE dot-free extension segment, so `main.test.ts`
|
|
3462
|
+
* and `main.d.ts` never satisfy a `main` hint (they are not what a module
|
|
3463
|
+
* resolver would load for `./main`). Candidate rows are fetched by
|
|
3464
|
+
* qualified name and the path shape is checked in JS: no LIKE/GLOB, so
|
|
3465
|
+
* hint characters are never pattern metacharacters. Zero or multiple
|
|
3466
|
+
* matches return `undefined` — the edge is dropped rather than guessed
|
|
3467
|
+
* (same conservative policy as {@link resolveNodeId}).
|
|
3468
|
+
*/
|
|
3469
|
+
/**
|
|
3470
|
+
* Language families and the file extensions a module resolver in that
|
|
3471
|
+
* family accepts (issue #1894 round 13). A polyglot repo cannot let a
|
|
3472
|
+
* JS import bind a same-named .py file: constrain by importer language.
|
|
3473
|
+
*/
|
|
3474
|
+
const LANG_FAMILY_EXTENSIONS: Record<string, ReadonlySet<string>> = {
|
|
3475
|
+
js: new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]),
|
|
3476
|
+
python: new Set(["py", "pyi"]),
|
|
3477
|
+
ruby: new Set(["rb"]),
|
|
3478
|
+
go: new Set(["go"]),
|
|
3479
|
+
rust: new Set(["rs"]),
|
|
3480
|
+
php: new Set(["php"]),
|
|
3481
|
+
java: new Set(["java"]),
|
|
3482
|
+
csharp: new Set(["cs"]),
|
|
3483
|
+
cpp: new Set(["cc", "cpp", "cxx", "c", "hh", "hpp", "hxx", "h"]),
|
|
3484
|
+
kotlin: new Set(["kt"]),
|
|
3485
|
+
swift: new Set(["swift"]),
|
|
3486
|
+
bash: new Set(["sh", "bash"]),
|
|
3487
|
+
};
|
|
3488
|
+
|
|
3489
|
+
const IMPORTER_LANG_FAMILY: Record<string, keyof typeof LANG_FAMILY_EXTENSIONS> = {
|
|
3490
|
+
typescript: "js",
|
|
3491
|
+
tsx: "js",
|
|
3492
|
+
javascript: "js",
|
|
3493
|
+
python: "python",
|
|
3494
|
+
ruby: "ruby",
|
|
3495
|
+
go: "go",
|
|
3496
|
+
rust: "rust",
|
|
3497
|
+
php: "php",
|
|
3498
|
+
java: "java",
|
|
3499
|
+
csharp: "csharp",
|
|
3500
|
+
c: "cpp",
|
|
3501
|
+
cpp: "cpp",
|
|
3502
|
+
kotlin: "kotlin",
|
|
3503
|
+
swift: "swift",
|
|
3504
|
+
bash: "bash",
|
|
3505
|
+
};
|
|
3506
|
+
|
|
3507
|
+
function resolveNodeIdWithPathHint(
|
|
3508
|
+
qualifiedName: string,
|
|
3509
|
+
pathHint: string,
|
|
3510
|
+
db: BetterSqlite3Database,
|
|
3511
|
+
importerLanguage?: string,
|
|
3512
|
+
): string | undefined {
|
|
3513
|
+
const family = importerLanguage ? IMPORTER_LANG_FAMILY[importerLanguage] : undefined;
|
|
3514
|
+
const allowedExts = family ? LANG_FAMILY_EXTENSIONS[family] : undefined;
|
|
3515
|
+
const rows = expectRows<{ id: string; path: string }>(
|
|
3516
|
+
db
|
|
3517
|
+
.prepare(
|
|
3518
|
+
`SELECT n.id, f.path FROM nodes n JOIN files f ON n.file_id = f.id
|
|
3519
|
+
WHERE n.qualified_name = ?
|
|
3520
|
+
ORDER BY f.path, n.id`,
|
|
3521
|
+
)
|
|
3522
|
+
.all(qualifiedName),
|
|
3523
|
+
["id", "path"],
|
|
3524
|
+
);
|
|
3525
|
+
const matches = rows.filter((row) => {
|
|
3526
|
+
if (row.path === pathHint) {
|
|
3527
|
+
// Language filter still applies on exact match: a TS import whose
|
|
3528
|
+
// normalized hint happens to equal a bare directory name must not
|
|
3529
|
+
// bind a same-named .py file (codex review round 15).
|
|
3530
|
+
if (allowedExts) {
|
|
3531
|
+
return false; // bare hint with no extension cannot match a family
|
|
3532
|
+
}
|
|
3533
|
+
return true;
|
|
3534
|
+
}
|
|
3535
|
+
if (!row.path.startsWith(pathHint)) return false;
|
|
3536
|
+
const rest = row.path.slice(pathHint.length);
|
|
3537
|
+
const m = /^(?:\.([^./]+)|\/(?:index|__init__)\.([^./]+))$/.exec(rest);
|
|
3538
|
+
if (!m) return false;
|
|
3539
|
+
const ext = m[1] ?? m[2];
|
|
3540
|
+
// When the importer's language family is known, only that family's
|
|
3541
|
+
// extensions may satisfy the hint; otherwise any single extension.
|
|
3542
|
+
return !allowedExts || allowedExts.has(ext);
|
|
3543
|
+
});
|
|
3544
|
+
if (matches.length !== 1) return undefined;
|
|
3545
|
+
return matches[0]?.id;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3292
3548
|
/**
|
|
3293
3549
|
* Resolve a standalone-edge endpoint by content-derived node id (issue #1677).
|
|
3294
3550
|
*
|