ucn 5.5.1 → 5.5.2

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.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +30 -21
  6. package/cli/index.js +1 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +137 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +4726 -940
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +217 -112
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +151 -19
  63. package/core/rust-modules.js +1 -1
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +124 -37
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +510 -82
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
@@ -7,6 +7,13 @@ const isOffset = value => Number.isFinite(value) && value >= 0;
7
7
 
8
8
  const { codeUnitCompare } = require('./shared');
9
9
 
10
+ // Modules read lazily (load cycles), each resolved once: a require() per
11
+ // call in hot paths was measurable (fix #436).
12
+ let lazyProvenanceOverloadModuleCache = null;
13
+ const lazyProvenanceOverloadModule = () => lazyProvenanceOverloadModuleCache || (lazyProvenanceOverloadModuleCache = require('./provenance-overload'));
14
+ let lazyReceiverTypesModuleCache = null;
15
+ const lazyReceiverTypesModule = () => lazyReceiverTypesModuleCache || (lazyReceiverTypesModuleCache = require('./receiver-types'));
16
+
10
17
  const TYPE_SOURCE_RULES = Object.freeze({
11
18
  annotation: 'receiver-annotation', constructor: 'constructor-typed',
12
19
  flow: 'return-flow', field: 'field-hop', literal: 'literal-receiver',
@@ -85,6 +92,124 @@ function validateConfirmation(provenance, target, invalidCall = false) {
85
92
  ? { verdict: 'establishes-target' }
86
93
  : { verdict: 'establishes-other', declaration };
87
94
  if (!targets.length || targets.some(t => !validIdentity(t))) return incomplete('missing-target-identity');
95
+ if (facts.implicitBaseBinding) {
96
+ const owner = facts.implicitBaseBinding.binding?.owner;
97
+ return validIdentity(owner) && require('./provenance-constructor-base').validateImplicitBase(facts.implicitBaseBinding, owner, facts.site)
98
+ ? verdictFor(owner) : incomplete('unproven-implicit-base');
99
+ }
100
+ if (facts.constructionBinding) {
101
+ const owner = facts.constructionBinding.owner;
102
+ return validIdentity(owner) && require('./provenance-type').validateTypeLookup(facts.constructionBinding, owner, facts.site)
103
+ ? verdictFor(owner) : incomplete('unproven-construction-type');
104
+ }
105
+ if (facts.constructorCall) {
106
+ const constructor = facts.constructorCall;
107
+ const { owner, members, selected, classBinding, call, overload } = constructor;
108
+ const constructed = constructor.constructedOwner || owner;
109
+ // A same-type delegation (`this(...)`) runs in one of the owner's
110
+ // constructors, which is never its target: Java JLS 8.8.7, C#
111
+ // CS0516, C++ [class.base.init] (fix #419).
112
+ const delegating = constructor.delegating;
113
+ // The parts of a C# partial type are one type (fix #430): a member
114
+ // lies in one of them. Every part names the owner's type.
115
+ const parts = constructor.parts === undefined ? [owner] : constructor.parts;
116
+ if (constructor.parts !== undefined && (!Array.isArray(parts) || !validIdentity(owner) ||
117
+ !parts.some(part => sameDeclaration(part, owner)) ||
118
+ parts.some(part => !validIdentity(part) || part.name !== owner.name || part.kind !== owner.kind ||
119
+ (part.namespace || null) !== (owner.namespace || null) ||
120
+ (part.enclosingType || null) !== (owner.enclosingType || null)))) {
121
+ return inconsistent('constructor-owner-mismatch');
122
+ }
123
+ const inPart = def => parts.some(part => def.file === part.file &&
124
+ def.startLine >= part.startLine && def.endLine <= part.endLine);
125
+ if (delegating !== undefined) {
126
+ const site = facts.site;
127
+ // An out-of-class definition (fix #421) holds the site and
128
+ // defines the in-class declaration: same owner scope path and
129
+ // parameter signature.
130
+ const body = constructor.delegatingDefinition || delegating;
131
+ const scopePath = def => [def.namespace, def.enclosingType].filter(Boolean).join('::');
132
+ if (!validIdentity(owner) || !validIdentity(delegating) || call?.delegation !== 'this' ||
133
+ call.syntax !== 'delegation' || delegating.kind !== 'constructor' ||
134
+ delegating.className !== owner.name || !inPart(delegating) ||
135
+ (delegating.namespace || null) !== (owner.namespace || null) ||
136
+ !site || site.file !== body.file || site.line < body.startLine || site.line > body.endLine ||
137
+ (members || []).some(member => sameDeclaration(member, delegating))) {
138
+ return inconsistent('constructor-delegation-mismatch');
139
+ }
140
+ if (constructor.delegatingDefinition && (!validIdentity(body) || body.kind !== 'constructor' ||
141
+ body.className !== owner.name || body.name !== delegating.name ||
142
+ scopePath(body) !== [owner.namespace, owner.enclosingType].filter(Boolean).join('::') ||
143
+ typeof body.signature !== 'string' || body.signature !== constructor.delegatingSignature)) {
144
+ return inconsistent('constructor-delegation-mismatch');
145
+ }
146
+ if (targets.every(t => sameDeclaration(t, delegating))) {
147
+ return { verdict: 'establishes-other', declaration: delegating, diagnostic: 'constructor-delegation-self' };
148
+ }
149
+ }
150
+ const structural = require('../languages').langTraits(facts.language)?.typeSystem === 'structural';
151
+ if (!validIdentity(owner) || !validIdentity(selected) || !Array.isArray(members) || !members.length ||
152
+ !call || !(call.isConstructor || require('../languages').langTraits(facts.language)?.classesCallableWithoutNew ||
153
+ require('../languages').langTraits(facts.language)?.functionalTypeConstruction) ||
154
+ call.isFunctionReference || call.isPotentialCallback) return incomplete('missing-constructor-facts');
155
+ if (!classBinding?.facts?.site || classBinding.facts.site.file !== facts.site?.file ||
156
+ classBinding.facts.site.line !== facts.site?.line ||
157
+ classBinding.facts.site.column !== facts.site?.column ||
158
+ (structural || validateConfirmation(classBinding, constructed).verdict !== 'establishes-target') &&
159
+ !require('./provenance-type').validateTypeLookup(constructor.typeLookup, constructed, facts.site)) {
160
+ return incomplete('unproven-construction-binding');
161
+ }
162
+ if (require('../languages').langTraits(facts.language)?.inheritedOverloadSignatures &&
163
+ overload?.call?.argumentFile !== facts.site?.file) {
164
+ return incomplete('unproven-construction-argument-source');
165
+ }
166
+ if (!structural && !['implicit', 'delegation', 'enum'].includes(call.syntax) &&
167
+ (constructor.nameScope?.query?.name !== overload?.call?.name ||
168
+ constructor.nameScope?.query?.qualifier !== (overload?.call?.constructorQualifier || overload?.call?.receiver || null) ||
169
+ !require('./provenance-type').validateTypeLookup(constructor.nameScope, constructed, facts.site))) {
170
+ return incomplete('unproven-construction-name-scope');
171
+ }
172
+ if (call.syntax === 'implicit') {
173
+ const base = classBinding.facts.implicitBaseBinding;
174
+ if (!base || !require('./provenance-constructor-base').validateImplicitBase(base, constructed, facts.site)) {
175
+ return incomplete('unproven-implicit-base');
176
+ }
177
+ if (base.base.potential) return incomplete('constructor-default-unresolved');
178
+ }
179
+ if (!sameDeclaration(owner, constructed) &&
180
+ !require('./constructor-hierarchy').validateHierarchy(constructor.hierarchy, constructed, owner, members)) {
181
+ return incomplete('unproven-constructor-inheritance');
182
+ }
183
+ const constructorProtocol = require('../languages').langTraits(facts.language)?.constructorProtocol;
184
+ if (members.some(member => (member.kind !== 'constructor' && member.name !== constructorProtocol?.initializer) || member.className !== owner.name ||
185
+ !inPart(member) ||
186
+ (member.namespace || null) !== (owner.namespace || null) ||
187
+ (member.enclosingType || null) !== (owner.enclosingType || null)) ||
188
+ !members.some(member => sameDeclaration(member, selected))) return inconsistent('constructor-owner-mismatch');
189
+ if (overload?.candidates?.some(def => def.type === 'constructor' &&
190
+ (!Array.isArray(def.modifiers) || def.modifiers.includes('static')))) {
191
+ return incomplete('constructor-instance-flags');
192
+ }
193
+ if (!call.isConstructor && require('../languages').langTraits(facts.language)?.functionalTypeConstruction) {
194
+ if (!(constructor.typeLookup?.query?.functional &&
195
+ require('./provenance-type').validateTypeLookup(constructor.typeLookup, owner, facts.site))) {
196
+ return incomplete('constructor-functional-binding');
197
+ }
198
+ }
199
+ const protocol = require('../languages').langTraits(facts.language)?.constructorProtocol;
200
+ if (protocol) {
201
+ if (!require('./constructor-protocol').validateConstructorProtocol(constructor.protocol, constructed, members, facts.language)) {
202
+ return incomplete('constructor-protocol-unresolved');
203
+ }
204
+ }
205
+ if (invalidCall && members.length !== 1) return incomplete('constructor-overload-unselected');
206
+ if (!require('../languages').langTraits(facts.language)?.hasArityOverloads && members.length > 1 &&
207
+ !overload?.candidates?.some(def => def.isSignature === true)) return incomplete('constructor-definition-ambiguous');
208
+ if (!lazyProvenanceOverloadModule().validateOverload(overload, members, selected, invalidCall, false, facts.site)) {
209
+ return incomplete('constructor-overload-unselected');
210
+ }
211
+ return verdictFor(selected);
212
+ }
88
213
  if (facts.receiverOrigin?.externalFactory) {
89
214
  const factory = facts.receiverOrigin.externalFactory;
90
215
  const empty = value => Array.isArray(value) && !value.length;
@@ -237,13 +362,33 @@ function validateConfirmation(provenance, target, invalidCall = false) {
237
362
  }
238
363
  }
239
364
  if (facts.builtinReceiver) {
240
- if (!require('./receiver-types').isProvenanceBuiltinReceiver(facts.receiverType, facts.language) ||
365
+ if (!lazyReceiverTypesModule().isProvenanceBuiltinReceiver(facts.receiverType, facts.language) ||
241
366
  facts.builtinReceiver.type !== facts.receiverType ||
242
367
  facts.builtinReceiver.language !== facts.language) return inconsistent('builtin-receiver-mismatch');
243
368
  if (!facts.receiverOrigin || ['unknown', 'guess'].includes(facts.receiverTypeSource) ||
244
369
  facts.receiverOrigin.source !== facts.receiverTypeSource) return incomplete('builtin-origin-not-established');
245
370
  return { verdict: 'establishes-other', declaration: { builtin: facts.receiverType, language: facts.language } };
246
371
  }
372
+ if (facts.typeReference) {
373
+ const reference = facts.typeReference;
374
+ if (!validIdentity(reference.declaration) || !reference.written ||
375
+ String(reference.written).split(/::|\./).pop() !== reference.declaration.name) {
376
+ return inconsistent('type-reference-mismatch');
377
+ }
378
+ return verdictFor(reference.declaration);
379
+ }
380
+ const castThis = facts.receiverCastThis;
381
+ if (castThis) {
382
+ const receiver = facts.lookup?.receiver;
383
+ if (!validIdentity(castThis.enclosing) || castThis.interfaceType !== facts.receiverType ||
384
+ (receiver && (castThis.enclosing.className !== receiver.name ||
385
+ castThis.enclosing.file !== receiver.file))) return inconsistent('receiver-this-cast-mismatch');
386
+ // Knowing the enclosing class does not prove the implementation an
387
+ // interface cast of this invokes. A subclass may reimplement it.
388
+ // This also applies when a qualified cast prevented member lookup:
389
+ // that missing lookup must not fall through as an unsupported proof.
390
+ return incomplete('receiver-this-cast-dispatch');
391
+ }
247
392
  if (facts.lookup) {
248
393
  const { receiver, steps, selected } = facts.lookup;
249
394
  if (!validIdentity(receiver) || !validIdentity(selected) || !Array.isArray(steps) || !steps.length) {
@@ -253,9 +398,15 @@ function validateConfirmation(provenance, target, invalidCall = false) {
253
398
  if (!sameDeclaration(receiver, facts.receiverResolvedIn)) return inconsistent('receiver-identity-mismatch');
254
399
  const aliases = facts.receiverAliases || [];
255
400
  const typeDeclaration = facts.receiverTypeDeclaration || receiver;
401
+ // The written qualifier spells the declaration's enclosing type, its
402
+ // namespace, or (fix #430) its namespace and enclosing types, in full
403
+ // or from an enclosing namespace of the site.
404
+ const qualifiedPath = [typeDeclaration.namespace, typeDeclaration.enclosingType].filter(Boolean).join('.');
256
405
  if (facts.receiverTypeQualifier && ['java', 'csharp'].includes(facts.language) &&
257
406
  facts.receiverTypeQualifier !== typeDeclaration.enclosingType &&
258
- facts.receiverTypeQualifier !== typeDeclaration.namespace) {
407
+ facts.receiverTypeQualifier !== typeDeclaration.namespace &&
408
+ !(typeDeclaration.enclosingType && (qualifiedPath === facts.receiverTypeQualifier ||
409
+ qualifiedPath.endsWith(`.${facts.receiverTypeQualifier}`)))) {
259
410
  return inconsistent('receiver-qualifier-mismatch');
260
411
  }
261
412
  if (facts.receiverImportChain && validateConfirmation({ facts: {
@@ -275,12 +426,8 @@ function validateConfirmation(provenance, target, invalidCall = false) {
275
426
  !bound.bounds?.includes(typeDeclaration.name) || !sameDeclaration(bound.selected, typeDeclaration))) {
276
427
  return inconsistent('receiver-generic-bound-mismatch');
277
428
  }
278
- const castThis = facts.receiverCastThis;
279
- if (castThis && (!validIdentity(castThis.enclosing) ||
280
- castThis.enclosing.className !== receiver.name || castThis.enclosing.file !== receiver.file ||
281
- castThis.interfaceType !== facts.receiverType)) return inconsistent('receiver-this-cast-mismatch');
282
429
  const overload = facts.lookup.overload;
283
- const group = overload && require('./provenance-overload').overloadMemberGroup(steps);
430
+ const group = overload && lazyProvenanceOverloadModule().overloadMemberGroup(steps);
284
431
  if (overload && !group) return incomplete('missing-overload-declarations');
285
432
  if (overload && !group.some(member => sameDeclaration(declarationIdentity(member), selected))) {
286
433
  return inconsistent('selected-member-outside-overload-group');
@@ -301,15 +448,70 @@ function validateConfirmation(provenance, target, invalidCall = false) {
301
448
  return incomplete('missing-member-lookup-facts');
302
449
  }
303
450
  const named = step.members.filter(member => member.name === selected.name);
451
+ const delegation = step.constructorDelegation;
452
+ if (delegation && (i !== 0 || facts.receiver !== 'super' || selected.kind !== 'constructor' ||
453
+ delegation.enclosing?.kind !== 'constructor' ||
454
+ delegation.enclosing.className !== step.owner.name ||
455
+ delegation.enclosing.file !== step.owner.file ||
456
+ facts.site?.line < delegation.enclosing.startLine || facts.site?.line > delegation.enclosing.endLine ||
457
+ !Array.isArray(delegation.members) || named.length ||
458
+ delegation.members.some(member => member.kind !== 'constructor' || member.className !== step.owner.name))) {
459
+ return inconsistent('constructor-delegation-mismatch');
460
+ }
461
+ // A super lookup (fix #428) passes over the enclosing class's
462
+ // own members: only the first step, for the language's super
463
+ // receiver inside a member of that class.
464
+ const superLookup = step.superLookup;
465
+ if (superLookup) {
466
+ const enclosing = facts.receiverOrigin?.declaration;
467
+ const keyword = require('../languages').langTraits(facts.language)?.superReceiver;
468
+ if (i !== 0 || !keyword || facts.receiver !== keyword || named.length ||
469
+ facts.receiverOrigin?.source !== 'same-class' || facts.receiverOrigin.superLookup !== true ||
470
+ !validIdentity(enclosing) || enclosing.className !== step.owner.name ||
471
+ enclosing.file !== facts.site?.file || facts.site.line < enclosing.startLine ||
472
+ facts.site.line > enclosing.endLine || !Array.isArray(superLookup.skipped) ||
473
+ superLookup.skipped.some(member => member.className !== step.owner.name)) {
474
+ return inconsistent('super-lookup-mismatch');
475
+ }
476
+ }
477
+ // The parts of a C# partial type declare one class's members.
478
+ const parts = Array.isArray(step.partialParts) ? step.partialParts : null;
479
+ if (parts && (facts.language !== 'csharp' || !parts.some(part => sameDeclaration(part, step.owner)) ||
480
+ parts.some(part => !validIdentity(part) || part.name !== step.owner.name || part.kind !== step.owner.kind ||
481
+ (part.namespace || null) !== (step.owner.namespace || null) ||
482
+ (part.enclosingType || null) !== (step.owner.enclosingType || null)))) {
483
+ return inconsistent('partial-part-mismatch');
484
+ }
304
485
  if (named.some(member => member.className !== step.owner.name ||
305
486
  (member.namespace || null) !== (step.owner.namespace || null) ||
306
487
  (member.enclosingType || null) !== (step.owner.enclosingType || null) ||
307
- (facts.language !== 'go' && member.file !== step.owner.file))) {
488
+ (facts.language !== 'go' && member.file !== step.owner.file &&
489
+ !parts?.some(part => part.file === member.file) &&
490
+ // Rust impl blocks may sit in any file of the crate.
491
+ !require('../languages').langTraits(facts.language)?.itemScopeResolution))) {
308
492
  return inconsistent('member-owner-mismatch');
309
493
  }
310
494
  if (i + 1 < steps.length) {
311
495
  if (named.length && !overload) return inconsistent('overriding-member-before-target');
312
- if (!step.parents.some(parent => sameDeclaration(parent, steps[i + 1].owner))) {
496
+ const next = steps[i + 1];
497
+ if (next.enclosingOf) {
498
+ // A lexical hop (fix #428): a bare call, a hierarchy that
499
+ // ends here with no member of the name since the type
500
+ // it started from, and the declaration enclosing it.
501
+ const inner = next.enclosingOf;
502
+ let start = 0;
503
+ for (let j = i; j > 0; j--) if (steps[j].enclosingOf) { start = j; break; }
504
+ const segments = String(inner?.enclosingType || '').split('.');
505
+ const outerName = segments.pop();
506
+ if (facts.receiver || facts.receiverOrigin?.source !== 'same-class' || !['java', 'csharp'].includes(facts.language) ||
507
+ step.parents.length !== 0 || !validIdentity(inner) || !sameDeclaration(inner, steps[start].owner) ||
508
+ steps.slice(start, i + 1).some(prior => prior.members.some(member => member.name === selected.name)) ||
509
+ next.owner.name !== outerName || next.owner.file !== inner.file ||
510
+ (next.owner.enclosingType || null) !== (segments.length ? segments.join('.') : null) ||
511
+ next.owner.startLine > inner.startLine || (next.owner.endLine ?? next.owner.startLine) < inner.endLine) {
512
+ return inconsistent('unproven-enclosing-hop');
513
+ }
514
+ } else if (!step.parents.some(parent => sameDeclaration(parent, next.owner))) {
313
515
  return inconsistent('unproven-inheritance-hop');
314
516
  }
315
517
  } else if (configurationItem) {
@@ -328,13 +530,13 @@ function validateConfirmation(provenance, target, invalidCall = false) {
328
530
  } else if (!overload && (named.length !== 1 || !sameDeclaration(named[0], selected))) {
329
531
  const propertyRead = facts.valueReference && step.propertyRead &&
330
532
  sameDeclaration(propertyReadMember(named), selected);
331
- if (!propertyRead && !require('./provenance-overload').validateOverload(step.overload, named, selected)) {
533
+ if (!propertyRead && !lazyProvenanceOverloadModule().validateOverload(step.overload, named, selected, false, false, facts.site)) {
332
534
  return incomplete('ambiguous-or-missing-member');
333
535
  }
334
536
  }
335
537
  }
336
- if (overload && !require('./provenance-overload').validateOverload(
337
- overload, group.map(declarationIdentity), selected, invalidCall, ambiguousOverload)) {
538
+ if (overload && !lazyProvenanceOverloadModule().validateOverload(
539
+ overload, group.map(declarationIdentity), selected, invalidCall, ambiguousOverload, facts.site)) {
338
540
  return incomplete(overload.outcome === 'no-fit' ? 'no-applicable-overload' : 'ambiguous-or-missing-member');
339
541
  }
340
542
  if (facts.receiverTypeSource === 'guess' || facts.receiverTypeSource === 'unknown') {
@@ -348,7 +550,9 @@ function validateConfirmation(provenance, target, invalidCall = false) {
348
550
  }
349
551
  if (memberSetMembers) {
350
552
  if (targets.some(target => memberSetMembers.some(member => sameDeclaration(target, member)))) {
351
- return incomplete('ambiguous-or-missing-member');
553
+ // A choice among them the replay cannot state (fix #428).
554
+ return facts.lookupGap ? { verdict: 'incomplete', diagnostic: facts.lookupGap, gap: true }
555
+ : incomplete('ambiguous-or-missing-member');
352
556
  }
353
557
  return { verdict: 'establishes-other', declarations: memberSetMembers };
354
558
  }
@@ -411,6 +615,12 @@ function validateConfirmation(provenance, target, invalidCall = false) {
411
615
  last.declaration.name !== last.importedName) return incomplete('missing-import-declaration');
412
616
  return verdictFor(last.declaration);
413
617
  }
618
+ // A lookup this collector cannot state yet (fix #428: extension
619
+ // invocations, impl selection, members a using-declaration brings in,
620
+ // const/ref-qualified pairs, trait-provided methods, Rust name scopes)
621
+ // proves nothing: for exclusion and receiver migration it is incomplete,
622
+ // as before; on an edge another rule confirmed it reads `unsupported`.
623
+ if (facts.lookupGap) return { verdict: 'incomplete', diagnostic: facts.lookupGap, gap: true };
414
624
  // An unsupported rule is an instrumentation result, not a failed proof.
415
625
  // A collector that entered a supported lookup must still fail closed if
416
626
  // its witness is absent (including when a saved witness was tampered with).
@@ -420,6 +630,12 @@ function validateConfirmation(provenance, target, invalidCall = false) {
420
630
 
421
631
  /** An invalid member-family call is a diagnostic, never a confirmed edge. */
422
632
  function validateCallMismatch(provenance, target) {
633
+ const constructor = provenance?.facts?.constructorCall;
634
+ if (constructor) {
635
+ if (constructor.overload?.outcome !== 'no-fit' ||
636
+ !constructor.members?.some(member => sameDeclaration(member, target))) return false;
637
+ return validateConfirmation(provenance, constructor.members, true).verdict === 'establishes-target';
638
+ }
423
639
  const lookup = provenance?.facts?.lookup;
424
640
  if (lookup?.overload?.outcome !== 'no-fit') return false;
425
641
  const members = lookup.steps.flatMap(step => step.members || []);
@@ -433,13 +649,16 @@ function createProvenance(evidence, resolution) {
433
649
  const rules = [];
434
650
  if (evidence.possibleDispatch) rules.push('possible-dispatch');
435
651
  if (evidence.methodAmbiguous) rules.push('method-ambiguous');
436
- if (evidence.resolvedBySameClass) rules.push('same-class');
652
+ // A qualified static call resolved to its class is type-qualified, not
653
+ // a same-class receiver (fix #428).
654
+ if (evidence.resolvedBySameClass && !evidence.qualifiedOwner) rules.push('same-class');
437
655
  if (evidence.extensionMethod) rules.push('extension-method');
438
656
  if (evidence.hasReceiverType || evidence.resolvedByReceiverHint) {
439
657
  rules.push(TYPE_SOURCE_RULES[source] || 'receiver-type');
440
658
  }
441
659
  if (evidence.typeQualifiedReceiver) rules.push('type-qualified');
442
660
  if (evidence.moduleOwnedPath) rules.push('module-owned');
661
+ if (evidence.itemScope) rules.push('item-scope');
443
662
  if (evidence.hasBindingId) rules.push('binding');
444
663
  if (evidence.hasSingleOwnerEvidence) rules.push('single-owner');
445
664
  if (evidence.hasImportEvidence) {
@@ -466,7 +685,7 @@ function createProvenance(evidence, resolution) {
466
685
  const provenance = { rule: rules[0], rules: [...new Set(rules)], facts };
467
686
  if (facts.targets?.length) {
468
687
  const checked = validateConfirmation(provenance, facts.targets);
469
- provenance.validation = checked.verdict;
688
+ provenance.validation = checked.gap ? 'unsupported' : checked.verdict;
470
689
  if (checked.diagnostic) provenance.diagnostic = checked.diagnostic;
471
690
  } else {
472
691
  provenance.validation = 'incomplete';
@@ -5,8 +5,14 @@ const { CANONICAL_COMMANDS } = require('./registry');
5
5
  const NAME_COMMANDS = new Set(['show', 'find', 'usages', 'trace', 'tests', 'plan']);
6
6
 
7
7
  function parseSourceTarget(arg, params) {
8
- if (params.range) return { ...params, file: params.file, range: params.range };
9
8
  const value = String(arg || '').trim();
9
+ // `source <file> --range=N-M`: the positional is the file (fix #433). A
10
+ // positional beside an explicit file stays a name, which the handler
11
+ // refuses with the range.
12
+ if (params.range) {
13
+ if (!params.file && value) return { ...params, file: value, range: params.range };
14
+ return { ...params, ...(value && { name: value }), range: params.range };
15
+ }
10
16
  const twoPart = value.match(/^(.+?)\s+(\d+(?:-\d+)?)$/);
11
17
  if (twoPart) return { ...params, file: params.file || twoPart[1], range: twoPart[2] };
12
18
  // A single-colon file:range target is a line request. Stable symbol
@@ -16,6 +22,15 @@ function parseSourceTarget(arg, params) {
16
22
  return { ...params, name: arg };
17
23
  }
18
24
 
25
+ /** MCP and direct execute() params: with a range and no file, `name` is the
26
+ * file (`source` with name=<file>, range="N-M"; fix #433). */
27
+ function sourceRangeTarget(params) {
28
+ if (params.range == null || (params.file && String(params.file).trim()) ||
29
+ typeof params.name !== 'string' || !params.name.trim()) return params;
30
+ const { name, ...rest } = params;
31
+ return { ...rest, file: name.trim() };
32
+ }
33
+
19
34
  /** Build normalized execute() params from the shared CLI-style positional arg. */
20
35
  function buildPublicParams(command, arg, params = {}) {
21
36
  const clean = { ...params };
@@ -47,4 +62,4 @@ function isPublicCommand(command) {
47
62
  return CANONICAL_COMMANDS.includes(command);
48
63
  }
49
64
 
50
- module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget };
65
+ module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget, sourceRangeTarget };
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ const { langTraits, getParser, getLanguageAdapter, safeParse } = require('../languages');
4
+ const { getUsageResult, storeUsageResult } = require('./cache');
5
+ const { indexedContent, syntaxMode } = require('./source-memo');
6
+ const localNameTrees = new WeakSet();
7
+
8
+ /**
9
+ * Bodies whose declarations cannot bind a name outside their own scope.
10
+ * A block is damaged when an enclosing ERROR node or an enclosing block or
11
+ * callable with an error holds it. One native query lists those nodes in
12
+ * document order; a stack of the open ones stands for each block's
13
+ * ancestors (fix #435: every block walked its parent chain). In document
14
+ * order a node that starts before an open non-empty node ends lies inside it.
15
+ */
16
+ function bodyRanges(tree, kinds) {
17
+ if (!tree) return null;
18
+ const ranges = [];
19
+ if (!tree.rootNode.hasError) {
20
+ // No node of an error-free tree has an error. A block's first and
21
+ // last tokens are its first and last characters (an error-free tree
22
+ // has no missing tokens), read from the parsed text when it is at
23
+ // hand instead of asking for two child nodes (fix #436).
24
+ const text = typeof tree.input === 'string' ? tree.input : null;
25
+ for (const body of tree.rootNode.descendantsOfType('block')) {
26
+ const start = body.startIndex, end = body.endIndex;
27
+ if (text !== null ? end - start >= 2 && text[start] === '{' && text[end - 1] === '}'
28
+ : body.firstChild?.type === '{' && body.lastChild?.type === '}') {
29
+ ranges.push([start + 1, end - 1]);
30
+ }
31
+ }
32
+ return ranges.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
33
+ }
34
+ const open = []; // [endIndex, damages descendants]
35
+ for (const node of tree.rootNode.descendantsOfType(['block', 'ERROR', ...kinds])) {
36
+ const start = node.startIndex, end = node.endIndex;
37
+ while (open.length && open[open.length - 1][0] <= start) open.pop();
38
+ const damaged = open.some(entry => entry[1]);
39
+ const hasError = node.hasError;
40
+ if (node.type === 'block' && !damaged && !hasError && node.firstChild?.type === '{' &&
41
+ node.lastChild?.type === '}') ranges.push([start + 1, end - 1]);
42
+ if (end > start) open.push([end, node.type === 'ERROR' || hasError]);
43
+ }
44
+ // Bindings in a block stay in that block. Retain nested ranges so a
45
+ // query-bearing function can still skip unrelated sibling scopes.
46
+ return ranges.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
47
+ }
48
+
49
+ function viewKey(owns) { return owns == null ? 'primary' : [...owns].sort((a, b) => a - b).join(','); }
50
+
51
+ // Recovery remains visible at the same positions, in the same containers.
52
+ // Never prune an erroneous callable or anything inside an ERROR node.
53
+ function errorShape(tree) {
54
+ const errors = [], pending = [tree.rootNode];
55
+ while (pending.length) {
56
+ const node = pending.pop();
57
+ if (node.type === 'ERROR' || node.isMissing) {
58
+ const parents = [];
59
+ for (let parent = node.parent; parent; parent = parent.parent) {
60
+ parents.push([parent.type, parent.startIndex, parent.endIndex]);
61
+ }
62
+ errors.push([node.type, node.startIndex, node.endIndex, !!node.isMissing, parents]);
63
+ } else if (node.hasError) pending.push(...node.children);
64
+ }
65
+ return JSON.stringify(errors);
66
+ }
67
+
68
+ function readRanges(value, content) {
69
+ if (!Array.isArray(value) || !value.length) return null;
70
+ const views = new Map();
71
+ for (const record of value) {
72
+ if (!Array.isArray(record) || record.length !== 3 || typeof record[0] !== 'string' ||
73
+ views.has(record[0]) || !Array.isArray(record[1]) || typeof record[2] !== 'string') return null;
74
+ let previous = -1;
75
+ const ends = [];
76
+ for (const range of record[1]) {
77
+ if (!Array.isArray(range) || range.length !== 2 ||
78
+ !range.every(Number.isInteger) || range[0] <= previous || range[0] < 1 ||
79
+ range[1] < range[0] || range[1] >= content.length ||
80
+ content[range[0] - 1] !== '{' || content[range[1]] !== '}') return null;
81
+ while (ends.length && range[0] >= ends[ends.length - 1]) ends.pop();
82
+ if (ends.length && range[1] >= ends[ends.length - 1]) return null;
83
+ ends.push(range[1]);
84
+ previous = range[0];
85
+ }
86
+ views.set(record[0], { ranges: record[1], errors: record[2] });
87
+ }
88
+ return views.has('primary') ? views : null;
89
+ }
90
+
91
+ function blankBodies(source, ranges, name) {
92
+ const chunks = [];
93
+ let at = 0;
94
+ let occurrence = source.indexOf(name);
95
+ for (const [start, end] of ranges) {
96
+ if (start < at) continue;
97
+ while (occurrence >= 0 && occurrence < start) occurrence = source.indexOf(name, occurrence + name.length);
98
+ if (occurrence >= start && occurrence < end) continue;
99
+ const body = source.slice(start, end);
100
+ const newline = char => char === '\r' || char === '\n';
101
+ let first = 0, last = body.length - 2;
102
+ while (first < last && (newline(body[first]) || newline(body[first + 1]))) first++;
103
+ while (last > first + 1 && (newline(body[last]) || newline(body[last + 1]))) last--;
104
+ if (last <= first + 1) continue;
105
+ // All opted-in grammars have non-nesting block comments. One opaque
106
+ // token avoids parsing the unrelated body while preserving every
107
+ // UTF-16 offset and newline. Existing terminators stay inside it.
108
+ const comment = body.slice(first + 2, last).replace(/\*\//g, '* ');
109
+ chunks.push(source.slice(at, start), body.slice(0, first), '/*', comment, '*/', body.slice(last + 2));
110
+ at = end;
111
+ }
112
+ return chunks.length ? chunks.join('') + source.slice(at) : source;
113
+ }
114
+
115
+ /**
116
+ * Keep every declaration header and every body containing the queried text.
117
+ * The persisted facts are AST ranges, never type or ownership resolutions.
118
+ * These private trees never enter the index's complete parsed-tree caches.
119
+ */
120
+ function localQueryTree(index, file, entry, language, name, content) {
121
+ const traits = langTraits(language);
122
+ if (!traits.closedQueryBodies?.length || !entry.hash || !index._usageResultCache ||
123
+ index.isProvenanceFactIndex || !indexedContent(index, file, entry, content)) return null;
124
+ const mode = syntaxMode('valueBodies', [language, traits.closedQueryBodies, entry.conditionalViews, entry.recoveryBlanks]);
125
+ const key = '\x01body-ranges';
126
+ const value = getUsageResult(index, file, key, mode);
127
+ const ranges = readRanges(value, content);
128
+ const persistent = index._persistentParsedTree?.(file, language);
129
+ const warm = index._opTreeCache?.has(file) || persistent?.fileHash === entry.hash;
130
+ const parser = getParser(language);
131
+ if (ranges && !warm) {
132
+ let complete = true;
133
+ const transform = (source, owns) => {
134
+ const view = ranges.get(viewKey(owns));
135
+ if (!view) { complete = false; return source; }
136
+ return blankBodies(source, view.ranges, name);
137
+ };
138
+ const tree = traits.queryBodyTree
139
+ ? traits.queryBodyTree(content, parser, entry, transform)
140
+ : safeParse(parser, transform(content, null));
141
+ const views = traits.queryBodyViews?.(tree) || [{ tree }];
142
+ if (complete && views.every(view => view.tree &&
143
+ errorShape(view.tree) === ranges.get(viewKey(view.owns))?.errors)) {
144
+ for (const view of views) localNameTrees.add(view.tree);
145
+ return tree;
146
+ }
147
+ }
148
+ const tree = index._getParsedTree(file, content, language) ||
149
+ getLanguageAdapter(language)?.queryTree?.(content, parser, entry) || safeParse(parser, content);
150
+ if (!ranges && tree) {
151
+ const views = traits.queryBodyViews?.(tree) || [{ tree }];
152
+ const kinds = new Set(traits.closedQueryBodies);
153
+ const records = views.map(view => [viewKey(view.owns), bodyRanges(view.tree, kinds), errorShape(view.tree)]);
154
+ if (records.every(record => record[1] !== null)) storeUsageResult(index, file, key, mode, records);
155
+ }
156
+ return tree;
157
+ }
158
+
159
+ module.exports = { localQueryTree, isLocalNameTree: tree => localNameTrees.has(tree), bodyRangesForTest: bodyRanges };