ucn 5.0.6 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +12 -5
- package/.claude/skills/ucn/references/commands.md +2 -2
- package/.claude/skills/ucn/references/trust-contract.md +3 -2
- package/README.md +31 -14
- package/core/analysis.js +66 -1
- package/core/bridge.js +2 -1
- package/core/cache.js +64 -8
- package/core/callers.js +1722 -90
- package/core/graph-build.js +6 -0
- package/core/index-ir.js +16 -5
- package/core/ir.js +50 -3
- package/core/output/analysis.js +25 -0
- package/core/output/refactoring.js +1 -1
- package/core/output/shared.js +2 -2
- package/core/project.js +32 -0
- package/core/search.js +9 -0
- package/core/verify.js +1084 -36
- package/languages/c-family.js +231 -9
- package/languages/csharp.js +14 -3
- package/languages/go.js +473 -71
- package/languages/javascript.js +288 -21
- package/languages/python.js +443 -97
- package/languages/rust.js +87 -4
- package/languages/utils.js +11 -0
- package/mcp/server.js +100 -104
- package/mcp/stdio-server.js +296 -0
- package/package.json +10 -8
package/core/graph-build.js
CHANGED
|
@@ -451,6 +451,12 @@ function buildInheritanceGraph(index) {
|
|
|
451
451
|
}
|
|
452
452
|
index.extendsGraph.get(symbol.name).push({
|
|
453
453
|
file: filePath,
|
|
454
|
+
// Per-def anchor (fix #300, attrs-measured): five
|
|
455
|
+
// function-local `class C2Slots(...)` defs in one file
|
|
456
|
+
// carry DIFFERENT parents — file-granular lookup returned
|
|
457
|
+
// the first entry for all of them. startLine lets scope-
|
|
458
|
+
// aware consumers pick the def the call site actually sees.
|
|
459
|
+
startLine: symbol.startLine,
|
|
454
460
|
parents: resolvedParents
|
|
455
461
|
});
|
|
456
462
|
|
package/core/index-ir.js
CHANGED
|
@@ -16,6 +16,7 @@ function createImportBindings(imports) {
|
|
|
16
16
|
return {
|
|
17
17
|
name,
|
|
18
18
|
module: item.module,
|
|
19
|
+
...(item.type && { kind: item.type }),
|
|
19
20
|
...(item.line != null && { line: item.line }),
|
|
20
21
|
...(rename && { alias: rename.local }),
|
|
21
22
|
...(item.defaultLike && { defaultLike: true }),
|
|
@@ -65,17 +66,19 @@ function createFileEntryFromIR({
|
|
|
65
66
|
|
|
66
67
|
const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
67
68
|
'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
|
|
68
|
-
'isGenerator', 'generics', 'extends', 'implements', 'indent', 'isNested',
|
|
69
|
+
'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
|
|
69
70
|
'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
|
|
70
71
|
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
71
72
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
72
|
-
'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
|
|
73
|
+
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
73
74
|
'registryMember', 'registryContainer', 'namespace',
|
|
74
75
|
'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
|
|
75
76
|
'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
76
77
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
77
78
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
78
|
-
'
|
|
79
|
+
'isSpecialization',
|
|
80
|
+
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
81
|
+
'aliasOwner', 'aliasMember', 'macroParamEffects',
|
|
79
82
|
]);
|
|
80
83
|
|
|
81
84
|
function materializeSymbol(fileEntry, item) {
|
|
@@ -91,7 +94,9 @@ function materializeSymbol(fileEntry, item) {
|
|
|
91
94
|
returnType: item.returnType,
|
|
92
95
|
modifiers: item.modifiers,
|
|
93
96
|
docstring: item.docstring,
|
|
94
|
-
bindingId:
|
|
97
|
+
bindingId: item.id
|
|
98
|
+
? `${fileEntry.relativePath}:${item.id}`
|
|
99
|
+
: `${fileEntry.relativePath}:${item.kind}:${item.startLine}`,
|
|
95
100
|
...(item.owner && { className: item.owner }),
|
|
96
101
|
};
|
|
97
102
|
for (const field of OPTIONAL_SYMBOL_FIELDS) {
|
|
@@ -109,7 +114,13 @@ function materializeSymbol(fileEntry, item) {
|
|
|
109
114
|
function addIRSymbol(fileEntry, item, symbolTable = null) {
|
|
110
115
|
const symbol = materializeSymbol(fileEntry, item);
|
|
111
116
|
fileEntry.symbols.push(symbol);
|
|
112
|
-
|
|
117
|
+
// A Rust `impl X`/`impl Trait for X` block introduces NO name into any
|
|
118
|
+
// scope (fix #286b, cursive-measured: the impl symbol stole the bare-name
|
|
119
|
+
// binding of ColorPair from the cross-file struct, excluding a compiler-
|
|
120
|
+
// true composite-literal caller as other-definition). The struct/enum
|
|
121
|
+
// claim covers the impl — same discipline as deadcode's CLASS_AUDIT_KINDS.
|
|
122
|
+
if (!item.memberAssigned && !item.bodyScopedName && !item.exportedAlias &&
|
|
123
|
+
item.kind !== 'impl') {
|
|
113
124
|
fileEntry.bindings.push({
|
|
114
125
|
id: symbol.bindingId,
|
|
115
126
|
name: symbol.name,
|
package/core/ir.js
CHANGED
|
@@ -43,17 +43,19 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
43
43
|
};
|
|
44
44
|
const passthrough = [
|
|
45
45
|
'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
|
|
46
|
-
'isAsync', 'isGenerator', 'generics', 'extends', 'implements', 'indent',
|
|
46
|
+
'isAsync', 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent',
|
|
47
47
|
'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
|
|
48
48
|
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
49
49
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
50
|
-
'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
|
|
50
|
+
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
51
51
|
'registryMember', 'registryContainer', 'isConstructor',
|
|
52
52
|
'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
|
|
53
53
|
'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
54
54
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
55
55
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
56
|
-
'
|
|
56
|
+
'isSpecialization',
|
|
57
|
+
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
58
|
+
'aliasOwner', 'aliasMember', 'macroParamEffects',
|
|
57
59
|
];
|
|
58
60
|
for (const field of passthrough) {
|
|
59
61
|
if (symbol[field] !== undefined && symbol[field] !== null) {
|
|
@@ -114,6 +116,51 @@ function createFileIR({
|
|
|
114
116
|
for (const symbol of stateObjects) append(symbol, 'state', 'state');
|
|
115
117
|
for (const symbol of macros) append(symbol,
|
|
116
118
|
symbol.functionLike ? 'callable' : 'state', 'macro');
|
|
119
|
+
// An immutable module-scope member alias has the callable signature of
|
|
120
|
+
// the class member it captures: `const make = Widget.create`. Materialize
|
|
121
|
+
// the local value and each explicit export alias as real function symbols
|
|
122
|
+
// only when its member is static and every declared return type agrees.
|
|
123
|
+
// This is compiler-visible identity; mutable aliases and ambiguous
|
|
124
|
+
// overload returns were rejected by the parser/agreement gate above.
|
|
125
|
+
for (const alias of (parsed.callableAliases || [])) {
|
|
126
|
+
const sources = normalizedSymbols.filter(symbol =>
|
|
127
|
+
symbol.name === alias.member && symbol.owner === alias.owner &&
|
|
128
|
+
(symbol.params !== undefined || symbol.paramsStructured) &&
|
|
129
|
+
(symbol.modifiers?.includes('static') ||
|
|
130
|
+
String(symbol.memberType || symbol.kind).startsWith('static')) &&
|
|
131
|
+
symbol.returnType);
|
|
132
|
+
if (sources.length === 0) continue;
|
|
133
|
+
const sourceReturns = new Set(sources.map(source => source.returnType));
|
|
134
|
+
if (sourceReturns.size !== 1) continue;
|
|
135
|
+
const source = sources[0];
|
|
136
|
+
const exported = (parsed.exports || []).filter(item =>
|
|
137
|
+
!item.source && item.name === alias.name);
|
|
138
|
+
const exposed = exported.map(item => ({
|
|
139
|
+
name: item.type === 'default' ? 'default' : (item.alias || item.name),
|
|
140
|
+
line: item.line || alias.startLine,
|
|
141
|
+
}));
|
|
142
|
+
const localIsExported = exposed.some(item => item.name === alias.name);
|
|
143
|
+
const makeAlias = (name, startLine, isExported, exportedAlias = false) => ({
|
|
144
|
+
bindingId: `callable-alias:${alias.owner}.${alias.member}:${name}:${startLine}`,
|
|
145
|
+
name,
|
|
146
|
+
startLine,
|
|
147
|
+
endLine: startLine,
|
|
148
|
+
params: source.params,
|
|
149
|
+
...(source.paramsStructured && { paramsStructured: source.paramsStructured }),
|
|
150
|
+
returnType: source.returnType,
|
|
151
|
+
modifiers: isExported ? ['export'] : [],
|
|
152
|
+
callableAlias: true,
|
|
153
|
+
...(exportedAlias && { exportedAlias: true }),
|
|
154
|
+
aliasOwner: alias.owner,
|
|
155
|
+
aliasMember: alias.member,
|
|
156
|
+
});
|
|
157
|
+
append(makeAlias(alias.name, alias.startLine, localIsExported),
|
|
158
|
+
'callable', 'function');
|
|
159
|
+
for (const item of exposed) {
|
|
160
|
+
if (item.name === alias.name) continue;
|
|
161
|
+
append(makeAlias(item.name, item.line, true, true), 'callable', 'function');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
117
164
|
const imports = [...(parsed.imports || [])];
|
|
118
165
|
return {
|
|
119
166
|
schemaVersion: IR_SCHEMA_VERSION,
|
package/core/output/analysis.js
CHANGED
|
@@ -74,6 +74,23 @@ function isTestEntry(entry) {
|
|
|
74
74
|
return isTestPath(entry.relativePath || entry.file || '');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function formatAmbiguityCandidates(lines, ambiguity) {
|
|
78
|
+
if (!ambiguity?.items?.length) return;
|
|
79
|
+
const owners = ambiguity.dispatchOwners
|
|
80
|
+
? `; ${ambiguity.dispatchOwners} dispatch owners` : '';
|
|
81
|
+
lines.push(` competing definitions (${ambiguity.totalDefinitions}${owners}):`);
|
|
82
|
+
for (const candidate of ambiguity.items) {
|
|
83
|
+
const owner = candidate.owner
|
|
84
|
+
? ` on ${candidate.owner}${candidate.memberAssignment ? ' (member assignment)' : ''}` : '';
|
|
85
|
+
const selected = candidate.selected ? ' [selected target]' : '';
|
|
86
|
+
lines.push(` - ${candidate.handle} — ${candidate.type}${owner}${selected}`);
|
|
87
|
+
}
|
|
88
|
+
if (ambiguity.truncated) {
|
|
89
|
+
lines.push(` (+${ambiguity.totalDefinitions - ambiguity.items.length} more — ` +
|
|
90
|
+
`use find ${ambiguity.name})`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
/**
|
|
78
95
|
* Render the conservation contract lines: ACCOUNT and CONTRACT (always),
|
|
79
96
|
* WARNING (unparsed files containing the symbol), FILTERED (display-filter
|
|
@@ -273,6 +290,9 @@ function formatContextJson(context) {
|
|
|
273
290
|
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
274
291
|
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
275
292
|
})),
|
|
293
|
+
...(context.ambiguityCandidates && {
|
|
294
|
+
ambiguityCandidates: context.ambiguityCandidates,
|
|
295
|
+
}),
|
|
276
296
|
...(context.warnings && { warnings: context.warnings })
|
|
277
297
|
}
|
|
278
298
|
});
|
|
@@ -327,6 +347,9 @@ function formatContextJson(context) {
|
|
|
327
347
|
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
328
348
|
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
329
349
|
})),
|
|
350
|
+
...(context.ambiguityCandidates && {
|
|
351
|
+
ambiguityCandidates: context.ambiguityCandidates,
|
|
352
|
+
}),
|
|
330
353
|
callees: callees.map(c => ({
|
|
331
354
|
name: c.name,
|
|
332
355
|
type: c.type,
|
|
@@ -427,6 +450,7 @@ function formatContext(ctx, options = {}) {
|
|
|
427
450
|
const typeUnverified = ctx.unverifiedCallers || [];
|
|
428
451
|
if (typeUnverified.length > 0) {
|
|
429
452
|
lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
453
|
+
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
430
454
|
const cap = 10;
|
|
431
455
|
let shown = 0;
|
|
432
456
|
for (const u of typeUnverified) {
|
|
@@ -653,6 +677,7 @@ function formatContext(ctx, options = {}) {
|
|
|
653
677
|
// Always visible and capped at 10 one-liners unless --all.
|
|
654
678
|
if (actionableUnverified.length > 0) {
|
|
655
679
|
lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
680
|
+
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
656
681
|
const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
|
|
657
682
|
let shown = 0;
|
|
658
683
|
for (const u of actionableUnverified) {
|
|
@@ -70,7 +70,7 @@ function formatPlan(plan, options = {}) {
|
|
|
70
70
|
lines.push(` Files affected: ${plan.filesAffected}`);
|
|
71
71
|
if (plan.changeSummary) {
|
|
72
72
|
const summary = plan.changeSummary;
|
|
73
|
-
lines.push(` Definition ${summary.definitions}, calls
|
|
73
|
+
lines.push(` Definition ${summary.definitions}, calls ${summary.calls}, references ${summary.references || 0}, imports ${summary.imports}, exports ${summary.exports}; manual review required for ${summary.reviewRequired} of these changes`);
|
|
74
74
|
}
|
|
75
75
|
if (plan.unchangedSites > 0) {
|
|
76
76
|
lines.push(` ${plan.unchangedSites} existing call site${plan.unchangedSites === 1 ? '' : 's'} require no edit because the new parameter has a default.`);
|
package/core/output/shared.js
CHANGED
|
@@ -338,7 +338,7 @@ function formatGitLine(git) {
|
|
|
338
338
|
* Display label for an unverified-tier entry's reason. Dispatch-tiered
|
|
339
339
|
* entries (nominal languages) carry attribution metadata: the declared
|
|
340
340
|
* supertype the call dispatches through (dispatchVia) and how many
|
|
341
|
-
* same-name
|
|
341
|
+
* distinct same-name owners the dispatch could land on (dispatchCandidates).
|
|
342
342
|
*/
|
|
343
343
|
function unverifiedReasonLabel(entry) {
|
|
344
344
|
if (!entry || !entry.reason) return '';
|
|
@@ -356,7 +356,7 @@ function unverifiedReasonLabel(entry) {
|
|
|
356
356
|
: `possible-dispatch via ${entry.dispatchVia}`;
|
|
357
357
|
}
|
|
358
358
|
if (entry.reason === 'method-ambiguous' && entry.dispatchCandidates > 1) {
|
|
359
|
-
return `method-ambiguous — ${entry.dispatchCandidates}
|
|
359
|
+
return `method-ambiguous — ${entry.dispatchCandidates} dispatch owners`;
|
|
360
360
|
}
|
|
361
361
|
if (entry.reason === 'overload-ambiguous' && entry.dispatchCandidates > 1) {
|
|
362
362
|
return `overload-ambiguous — 1 of ${entry.dispatchCandidates} applicable overloads`;
|
package/core/project.js
CHANGED
|
@@ -444,6 +444,9 @@ class ProjectIndex {
|
|
|
444
444
|
this._completenessCache = null;
|
|
445
445
|
this._attrTypeCache = null;
|
|
446
446
|
this._computedDispatchBlindspots = null;
|
|
447
|
+
this._cppVisibleFilesCache?.clear();
|
|
448
|
+
this._cppTargetVisibilityCache?.clear();
|
|
449
|
+
this._cppMacroParamOutcomesCache?.clear();
|
|
447
450
|
// Endpoints cache (server routes / client requests / bridges) becomes
|
|
448
451
|
// stale when files change; clear on every rebuild.
|
|
449
452
|
this._endpointsCache = null;
|
|
@@ -589,6 +592,13 @@ class ProjectIndex {
|
|
|
589
592
|
return false;
|
|
590
593
|
}
|
|
591
594
|
|
|
595
|
+
// These query caches depend on project symbols and include closure.
|
|
596
|
+
// A changed file can alter either even before a full rebuild reaches
|
|
597
|
+
// its graph phase, so no prior call-identity verdict may survive.
|
|
598
|
+
this._cppVisibleFilesCache?.clear();
|
|
599
|
+
this._cppTargetVisibilityCache?.clear();
|
|
600
|
+
this._cppMacroParamOutcomesCache?.clear();
|
|
601
|
+
|
|
592
602
|
if (existing) {
|
|
593
603
|
this.removeFileSymbols(filePath);
|
|
594
604
|
}
|
|
@@ -942,6 +952,28 @@ class ProjectIndex {
|
|
|
942
952
|
return entries;
|
|
943
953
|
}
|
|
944
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Def-exact inheritance parents (fix #300): resolve parents for ONE
|
|
957
|
+
* specific class definition by (file, startLine). Same-name classes in
|
|
958
|
+
* one file (function-local test classes) carry different parents; the
|
|
959
|
+
* file-granular lookup above conflates them. Returns null when no entry
|
|
960
|
+
* matches — for a known def that means it extends nothing (entries are
|
|
961
|
+
* only recorded for extends-bearing defs), which callers must treat as
|
|
962
|
+
* "no parents", never fall back to a sibling def's edges.
|
|
963
|
+
* @param {string} className
|
|
964
|
+
* @param {string} contextFile
|
|
965
|
+
* @param {number} defStartLine
|
|
966
|
+
* @returns {string[]|null}
|
|
967
|
+
*/
|
|
968
|
+
_getInheritanceParentsAt(className, contextFile, defStartLine) {
|
|
969
|
+
const entries = this.extendsGraph.get(className);
|
|
970
|
+
if (!entries || entries.length === 0) return null;
|
|
971
|
+
if (typeof entries[0] !== 'object' || entries[0].file === undefined) return null;
|
|
972
|
+
const match = entries.find(e => e.file === contextFile &&
|
|
973
|
+
e.startLine === defStartLine);
|
|
974
|
+
return match ? match.parents : null;
|
|
975
|
+
}
|
|
976
|
+
|
|
945
977
|
/**
|
|
946
978
|
* Resolve which file a class is defined in, preferring contextFile.
|
|
947
979
|
* Used during inheritance BFS to find grandparent chains.
|
package/core/search.js
CHANGED
|
@@ -352,6 +352,15 @@ function usages(index, name, options = {}) {
|
|
|
352
352
|
usageType: u.usageType,
|
|
353
353
|
isDefinition: false,
|
|
354
354
|
...(u.receiver && { receiver: u.receiver }),
|
|
355
|
+
// Refactoring internals need the exact AST token and
|
|
356
|
+
// receiver provenance, but the public usages JSON is a
|
|
357
|
+
// stable line-oriented inventory. Keep this opt-in so
|
|
358
|
+
// plan gains precision without changing that surface.
|
|
359
|
+
...(options.internalEvidence && {
|
|
360
|
+
...(Number.isInteger(u.column) && { column: u.column }),
|
|
361
|
+
...(u.receiverIsModule && { receiverIsModule: true }),
|
|
362
|
+
...(u.receiverLocalBinding && { receiverLocalBinding: true }),
|
|
363
|
+
}),
|
|
355
364
|
...(callerSym && {
|
|
356
365
|
callerName: callerSym.name,
|
|
357
366
|
callerStartLine: callerSym.startLine
|