@remnic/coding-graph 9.6.23 → 9.6.24
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/{chunk-5I2DBHOQ.js → chunk-7DI5P62Q.js} +2 -2
- package/dist/{chunk-CPYJACC5.js → chunk-ZLCF3XQK.js} +72 -8
- package/dist/chunk-ZLCF3XQK.js.map +1 -0
- package/dist/cypher/query-parser.js +2 -2
- package/dist/graph-store.d.ts +27 -0
- package/dist/graph-store.js +1 -1
- package/dist/index.js +302 -22
- 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 +166 -9
- package/src/heuristic-resolution.test.ts +596 -0
- package/src/heuristic-resolution.ts +395 -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-7DI5P62Q.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`
|
|
@@ -1752,12 +1779,18 @@ export class GraphStore {
|
|
|
1752
1779
|
// re-ingest (chatgpt-codex-connector P2).
|
|
1753
1780
|
const srcId = qualifiedNameToId.get(edge.srcQualifiedName);
|
|
1754
1781
|
if (!srcId) continue;
|
|
1755
|
-
// DST may be cross-file
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1782
|
+
// DST may be cross-file. A path-hinted edge (relative import,
|
|
1783
|
+
// issue #1894 review) resolves ONLY within its declared target
|
|
1784
|
+
// file — never via the global bare-name fallback; unhinted edges
|
|
1785
|
+
// keep the batch-map + full-DB fallback.
|
|
1786
|
+
const dstId = edge.dstPathHint
|
|
1787
|
+
? resolveNodeIdWithPathHint(
|
|
1788
|
+
edge.dstQualifiedName,
|
|
1789
|
+
edge.dstPathHint,
|
|
1790
|
+
this.db,
|
|
1791
|
+
edge.dstImporterLanguage,
|
|
1792
|
+
)
|
|
1793
|
+
: resolveNodeId(edge.dstQualifiedName, qualifiedNameToId, this.db);
|
|
1761
1794
|
if (!dstId) continue;
|
|
1762
1795
|
const key = `${srcId}\u0000${dstId}\u0000${edge.type}`;
|
|
1763
1796
|
assertedKeys.add(key);
|
|
@@ -1788,12 +1821,24 @@ export class GraphStore {
|
|
|
1788
1821
|
);
|
|
1789
1822
|
const priorByKey = new Map<string, { confidence: number; provenance: string }>();
|
|
1790
1823
|
const staleSrcDstTypes: Array<{ src: string; dst: string; type: string }> = [];
|
|
1824
|
+
// Provenance scoping (issue #1891): when the IR declares which
|
|
1825
|
+
// provenances it asserts, stale edges of OTHER provenances are not
|
|
1826
|
+
// this ingest's to delete — a fresh parse contradicts only its own
|
|
1827
|
+
// derivation class (rule 25). Absent = legacy delete-all-stale.
|
|
1828
|
+
// An EMPTY scope array is treated as absent (cursor review round 9):
|
|
1829
|
+
// "[]" would otherwise be truthy, protect every prior edge from
|
|
1830
|
+
// deletion AND from updates, and silently disable cleanup — an
|
|
1831
|
+
// assertion that scopes nothing scopes nothing.
|
|
1832
|
+
const scoped =
|
|
1833
|
+
ir.assertedEdgeProvenances && ir.assertedEdgeProvenances.length > 0
|
|
1834
|
+
? ir.assertedEdgeProvenances
|
|
1835
|
+
: undefined;
|
|
1791
1836
|
for (const p of priorEdges) {
|
|
1792
1837
|
const key = `${p.src}\u0000${p.dst}\u0000${p.type}`;
|
|
1793
1838
|
priorByKey.set(key, { confidence: p.confidence, provenance: p.provenance });
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1839
|
+
if (assertedKeys.has(key)) continue;
|
|
1840
|
+
if (scoped && !(scoped as readonly string[]).includes(p.provenance)) continue;
|
|
1841
|
+
staleSrcDstTypes.push({ src: p.src, dst: p.dst, type: p.type });
|
|
1797
1842
|
}
|
|
1798
1843
|
|
|
1799
1844
|
// Delete only the stale src-owned edges (prior-but-not-asserted).
|
|
@@ -1852,6 +1897,27 @@ export class GraphStore {
|
|
|
1852
1897
|
// stale keys.
|
|
1853
1898
|
continue;
|
|
1854
1899
|
}
|
|
1900
|
+
// Two update-path protections under provenance scoping (issue
|
|
1901
|
+
// #1891 + #1894 review rounds):
|
|
1902
|
+
// 1. a prior row whose provenance is OUTSIDE the asserted scope
|
|
1903
|
+
// is not this assertion's to modify (defense in depth — in
|
|
1904
|
+
// practice cross-provenance key collisions are heuristic/lsp
|
|
1905
|
+
// only, handled by 2);
|
|
1906
|
+
// 2. an lsp row is a strictly stronger derivation of the SAME
|
|
1907
|
+
// source-derived edge: re-asserting the heuristic key keeps
|
|
1908
|
+
// the row alive (it is not stale) but must never downgrade it.
|
|
1909
|
+
// Retirement of lsp rows happens through the stale-delete when
|
|
1910
|
+
// the call disappears from the parse — lsp IS in the reindex
|
|
1911
|
+
// assertion scope precisely so vanished calls retire their
|
|
1912
|
+
// upgraded rows too.
|
|
1913
|
+
if (
|
|
1914
|
+
prior &&
|
|
1915
|
+
scoped &&
|
|
1916
|
+
(!(scoped as readonly string[]).includes(prior.provenance) ||
|
|
1917
|
+
(prior.provenance === "lsp" && edge.provenance === "heuristic"))
|
|
1918
|
+
) {
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1855
1921
|
const r = insertEdge.run(srcId, dstId, edge.type, edge.confidence, edge.provenance);
|
|
1856
1922
|
edgeCount += r.changes;
|
|
1857
1923
|
}
|
|
@@ -3289,6 +3355,97 @@ function resolveNodeId(
|
|
|
3289
3355
|
return rows[0]?.id;
|
|
3290
3356
|
}
|
|
3291
3357
|
|
|
3358
|
+
/**
|
|
3359
|
+
* Resolve a dst node constrained to a path hint (issue #1894 review): the
|
|
3360
|
+
* node's file path must be the hint verbatim, `<hint>.<ext>`,
|
|
3361
|
+
* `<hint>/index.<ext>`, or `<hint>/__init__.<ext>` (Python packages) —
|
|
3362
|
+
* where `<ext>` is a SINGLE dot-free extension segment, so `main.test.ts`
|
|
3363
|
+
* and `main.d.ts` never satisfy a `main` hint (they are not what a module
|
|
3364
|
+
* resolver would load for `./main`). Candidate rows are fetched by
|
|
3365
|
+
* qualified name and the path shape is checked in JS: no LIKE/GLOB, so
|
|
3366
|
+
* hint characters are never pattern metacharacters. Zero or multiple
|
|
3367
|
+
* matches return `undefined` — the edge is dropped rather than guessed
|
|
3368
|
+
* (same conservative policy as {@link resolveNodeId}).
|
|
3369
|
+
*/
|
|
3370
|
+
/**
|
|
3371
|
+
* Language families and the file extensions a module resolver in that
|
|
3372
|
+
* family accepts (issue #1894 round 13). A polyglot repo cannot let a
|
|
3373
|
+
* JS import bind a same-named .py file: constrain by importer language.
|
|
3374
|
+
*/
|
|
3375
|
+
const LANG_FAMILY_EXTENSIONS: Record<string, ReadonlySet<string>> = {
|
|
3376
|
+
js: new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]),
|
|
3377
|
+
python: new Set(["py", "pyi"]),
|
|
3378
|
+
ruby: new Set(["rb"]),
|
|
3379
|
+
go: new Set(["go"]),
|
|
3380
|
+
rust: new Set(["rs"]),
|
|
3381
|
+
php: new Set(["php"]),
|
|
3382
|
+
java: new Set(["java"]),
|
|
3383
|
+
csharp: new Set(["cs"]),
|
|
3384
|
+
cpp: new Set(["cc", "cpp", "cxx", "c", "hh", "hpp", "hxx", "h"]),
|
|
3385
|
+
kotlin: new Set(["kt"]),
|
|
3386
|
+
swift: new Set(["swift"]),
|
|
3387
|
+
bash: new Set(["sh", "bash"]),
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
const IMPORTER_LANG_FAMILY: Record<string, keyof typeof LANG_FAMILY_EXTENSIONS> = {
|
|
3391
|
+
typescript: "js",
|
|
3392
|
+
tsx: "js",
|
|
3393
|
+
javascript: "js",
|
|
3394
|
+
python: "python",
|
|
3395
|
+
ruby: "ruby",
|
|
3396
|
+
go: "go",
|
|
3397
|
+
rust: "rust",
|
|
3398
|
+
php: "php",
|
|
3399
|
+
java: "java",
|
|
3400
|
+
csharp: "csharp",
|
|
3401
|
+
c: "cpp",
|
|
3402
|
+
cpp: "cpp",
|
|
3403
|
+
kotlin: "kotlin",
|
|
3404
|
+
swift: "swift",
|
|
3405
|
+
bash: "bash",
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
function resolveNodeIdWithPathHint(
|
|
3409
|
+
qualifiedName: string,
|
|
3410
|
+
pathHint: string,
|
|
3411
|
+
db: BetterSqlite3Database,
|
|
3412
|
+
importerLanguage?: string,
|
|
3413
|
+
): string | undefined {
|
|
3414
|
+
const family = importerLanguage ? IMPORTER_LANG_FAMILY[importerLanguage] : undefined;
|
|
3415
|
+
const allowedExts = family ? LANG_FAMILY_EXTENSIONS[family] : undefined;
|
|
3416
|
+
const rows = expectRows<{ id: string; path: string }>(
|
|
3417
|
+
db
|
|
3418
|
+
.prepare(
|
|
3419
|
+
`SELECT n.id, f.path FROM nodes n JOIN files f ON n.file_id = f.id
|
|
3420
|
+
WHERE n.qualified_name = ?
|
|
3421
|
+
ORDER BY f.path, n.id`,
|
|
3422
|
+
)
|
|
3423
|
+
.all(qualifiedName),
|
|
3424
|
+
["id", "path"],
|
|
3425
|
+
);
|
|
3426
|
+
const matches = rows.filter((row) => {
|
|
3427
|
+
if (row.path === pathHint) {
|
|
3428
|
+
// Language filter still applies on exact match: a TS import whose
|
|
3429
|
+
// normalized hint happens to equal a bare directory name must not
|
|
3430
|
+
// bind a same-named .py file (codex review round 15).
|
|
3431
|
+
if (allowedExts) {
|
|
3432
|
+
return false; // bare hint with no extension cannot match a family
|
|
3433
|
+
}
|
|
3434
|
+
return true;
|
|
3435
|
+
}
|
|
3436
|
+
if (!row.path.startsWith(pathHint)) return false;
|
|
3437
|
+
const rest = row.path.slice(pathHint.length);
|
|
3438
|
+
const m = /^(?:\.([^./]+)|\/(?:index|__init__)\.([^./]+))$/.exec(rest);
|
|
3439
|
+
if (!m) return false;
|
|
3440
|
+
const ext = m[1] ?? m[2];
|
|
3441
|
+
// When the importer's language family is known, only that family's
|
|
3442
|
+
// extensions may satisfy the hint; otherwise any single extension.
|
|
3443
|
+
return !allowedExts || allowedExts.has(ext);
|
|
3444
|
+
});
|
|
3445
|
+
if (matches.length !== 1) return undefined;
|
|
3446
|
+
return matches[0]?.id;
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3292
3449
|
/**
|
|
3293
3450
|
* Resolve a standalone-edge endpoint by content-derived node id (issue #1677).
|
|
3294
3451
|
*
|