ucn 5.5.3 → 5.6.1
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 +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/core/verify.js
CHANGED
|
@@ -1215,7 +1215,7 @@ const GIT_REF_FORMAT = /^[a-zA-Z0-9._\-~\/^@{}:]+$/; // eslint-disable-line no-
|
|
|
1215
1215
|
*/
|
|
1216
1216
|
let baseReadSerial = 0;
|
|
1217
1217
|
|
|
1218
|
-
function startBaseRead(file, base) {
|
|
1218
|
+
function startBaseRead(file, base, repoFile = file) {
|
|
1219
1219
|
if (process.platform === 'win32' || !file || !GIT_REF_FORMAT.test(base)) return null;
|
|
1220
1220
|
const out = path.join(os.tmpdir(),
|
|
1221
1221
|
`ucn-base-${process.pid}-${++baseReadSerial}-${Math.random().toString(36).slice(2, 10)}`);
|
|
@@ -1225,8 +1225,8 @@ function startBaseRead(file, base) {
|
|
|
1225
1225
|
// marks a revision that does not hold the file.
|
|
1226
1226
|
child = spawn('/bin/sh', ['-c',
|
|
1227
1227
|
'git show "$1" > "$2.part" 2>/dev/null && mv "$2.part" "$2" || { rm -f "$2.part"; : > "$2.none"; }',
|
|
1228
|
-
'ucn-base', `${base}:./${path.basename(
|
|
1229
|
-
{ cwd: path.dirname(
|
|
1228
|
+
'ucn-base', `${base}:./${path.basename(repoFile)}`, out],
|
|
1229
|
+
{ cwd: path.dirname(repoFile), stdio: 'ignore' });
|
|
1230
1230
|
} catch {
|
|
1231
1231
|
child = null;
|
|
1232
1232
|
}
|
|
@@ -1299,8 +1299,11 @@ function baseDeclarationChanges(index, name, def, base = 'HEAD', baseRead = null
|
|
|
1299
1299
|
if (baseContent === null) continue;
|
|
1300
1300
|
if (baseContent === undefined) {
|
|
1301
1301
|
try {
|
|
1302
|
-
|
|
1303
|
-
|
|
1302
|
+
// The repository's copy of the file (a snapshot exported
|
|
1303
|
+
// from the git index is not a repository).
|
|
1304
|
+
const repoFile = index.repoPathOf ? index.repoPathOf(file) : file;
|
|
1305
|
+
baseContent = execFileSync('git', ['show', `${base}:./${path.basename(repoFile)}`], {
|
|
1306
|
+
cwd: path.dirname(repoFile), encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024,
|
|
1304
1307
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1305
1308
|
});
|
|
1306
1309
|
} catch {
|
|
@@ -1774,10 +1777,19 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1774
1777
|
if (!argsNode) return { args: [], argCount: 0, isMethodCall, ...ctx };
|
|
1775
1778
|
|
|
1776
1779
|
let args = [];
|
|
1780
|
+
const argumentFacts = [];
|
|
1777
1781
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1778
1782
|
const argNode = argsNode.namedChild(i);
|
|
1779
1783
|
if (argNode.type.includes('comment')) continue;
|
|
1780
1784
|
args.push(argNode.text.trim());
|
|
1785
|
+
let value = argNode.type === 'keyword_argument' ? argNode.childForFieldName('value') : argNode;
|
|
1786
|
+
// C# wraps each ordinary value in an argument node. Named or
|
|
1787
|
+
// ref/out arguments remain outside the literal proof domain.
|
|
1788
|
+
if (value?.type === 'argument' && value.namedChildCount === 1 && value.childCount === 1) {
|
|
1789
|
+
value = value.namedChild(0);
|
|
1790
|
+
}
|
|
1791
|
+
argumentFacts.push({ kind: classifyArgNode(value), nodeType: value?.type, text: value?.text,
|
|
1792
|
+
...(argNode.type === 'keyword_argument' && { keyword: argNode.childForFieldName('name')?.text }) });
|
|
1781
1793
|
}
|
|
1782
1794
|
|
|
1783
1795
|
// Python argument structure (fix #281): keyword arguments bind by
|
|
@@ -1847,7 +1859,10 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1847
1859
|
}
|
|
1848
1860
|
}
|
|
1849
1861
|
|
|
1850
|
-
return {
|
|
1862
|
+
return Object.defineProperty({
|
|
1863
|
+
// Where the call is in the source: the same node, whoever reads
|
|
1864
|
+
// the file next (an alias, the second call on a line).
|
|
1865
|
+
span: { start: callNode.startIndex, end: callNode.endIndex },
|
|
1851
1866
|
args,
|
|
1852
1867
|
argCount: args.length,
|
|
1853
1868
|
hasSpread: args.some(a => a.startsWith('...')) || unpackingArgs > 0,
|
|
@@ -1857,7 +1872,7 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1857
1872
|
isMethodCall,
|
|
1858
1873
|
...(indirectKind && { indirectKind }),
|
|
1859
1874
|
...ctx,
|
|
1860
|
-
};
|
|
1875
|
+
}, 'argumentFacts', { value: argumentFacts });
|
|
1861
1876
|
} catch (e) {
|
|
1862
1877
|
return { args: null, argCount: 0 };
|
|
1863
1878
|
}
|
|
@@ -1961,7 +1976,7 @@ function classifyArgNode(node) {
|
|
|
1961
1976
|
}
|
|
1962
1977
|
// Numbers
|
|
1963
1978
|
if (t === 'number' || t === 'integer' || t === 'float' || t === 'number_literal' ||
|
|
1964
|
-
t === 'integer_literal' || t === 'float_literal' || t === 'decimal_integer_literal' ||
|
|
1979
|
+
t === 'integer_literal' || t === 'int_literal' || t === 'float_literal' || t === 'decimal_integer_literal' ||
|
|
1965
1980
|
t === 'hex_integer_literal' || t === 'real_literal') {
|
|
1966
1981
|
return 'number_literal';
|
|
1967
1982
|
}
|
|
@@ -2133,10 +2148,11 @@ function bindKeywordCall(params, analysis) {
|
|
|
2133
2148
|
* @param {object} options - { file }
|
|
2134
2149
|
* @returns {object} Verification results with mismatches
|
|
2135
2150
|
*/
|
|
2136
|
-
function verify(index, name, options = {}) {
|
|
2151
|
+
function verify(index, name, options = {}, direct = null) {
|
|
2137
2152
|
index._beginOp();
|
|
2138
2153
|
try {
|
|
2139
|
-
const { def, warnings } =
|
|
2154
|
+
const { def, warnings } = direct ? { def: direct.definition, warnings: [] }
|
|
2155
|
+
: index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
2140
2156
|
if (!def) {
|
|
2141
2157
|
return { found: false, function: name };
|
|
2142
2158
|
}
|
|
@@ -2173,6 +2189,7 @@ function verify(index, name, options = {}) {
|
|
|
2173
2189
|
const rawParamLists = ctorParamLists ||
|
|
2174
2190
|
(callableIdentityParams?.length ? callableIdentityParams : null) ||
|
|
2175
2191
|
[(arrowTypes?.paramsStructured) || def.paramsStructured || []];
|
|
2192
|
+
if (direct) direct.parameterLists = rawParamLists.map(stripSelf);
|
|
2176
2193
|
const params = stripSelf(rawParamLists[0]);
|
|
2177
2194
|
const arities = rawParamLists.map(l => {
|
|
2178
2195
|
const list = stripSelf(l);
|
|
@@ -2199,11 +2216,13 @@ function verify(index, name, options = {}) {
|
|
|
2199
2216
|
// A base the user names must be a commit: a misspelled ref would read as
|
|
2200
2217
|
// a file absent at base and check nothing against the old declaration
|
|
2201
2218
|
// (the target-less check already resolved its base through the diff).
|
|
2202
|
-
|
|
2203
|
-
|
|
2219
|
+
const repoFile = index.repoPathOf ? index.repoPathOf(def.file) : def.file;
|
|
2220
|
+
if (options.base && options.checkBaseRef) assertGitCommit(repoFile, baseRef);
|
|
2221
|
+
const baseRead = direct ? null : startBaseRead(def.file, baseRef, repoFile);
|
|
2204
2222
|
let sweep;
|
|
2205
2223
|
try {
|
|
2206
|
-
sweep =
|
|
2224
|
+
sweep = direct ? { confirmed: direct.sites, unverified: [], account: null, rebound: [], baseChanges: [] }
|
|
2225
|
+
: contractedCallerSweep(index, name, def, {
|
|
2207
2226
|
baseChanges: () => baseDeclarationChanges(index, name, def, baseRef, baseRead),
|
|
2208
2227
|
});
|
|
2209
2228
|
} finally {
|
|
@@ -2274,12 +2293,22 @@ function verify(index, name, options = {}) {
|
|
|
2274
2293
|
};
|
|
2275
2294
|
}
|
|
2276
2295
|
|
|
2296
|
+
// A mismatch remembers which call it is (not part of what is printed).
|
|
2297
|
+
const located = (record, analysis) => {
|
|
2298
|
+
if (analysis && analysis.span) Object.defineProperty(record, 'span', { value: analysis.span, enumerable: false });
|
|
2299
|
+
// `f.call(self, ..)`: the arguments start one slot later.
|
|
2300
|
+
if (analysis && analysis.indirectKind) {
|
|
2301
|
+
Object.defineProperty(record, 'indirect', { value: analysis.indirectKind, enumerable: false });
|
|
2302
|
+
}
|
|
2303
|
+
return record;
|
|
2304
|
+
};
|
|
2277
2305
|
const verifyLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
2278
2306
|
for (const call of calls) {
|
|
2279
2307
|
const siteKey = `${call.file}:${call.line}:${call.calledAs || name}`;
|
|
2280
2308
|
const occurrence = verifyLineSeen.get(siteKey) || 0;
|
|
2281
2309
|
verifyLineSeen.set(siteKey, occurrence + 1);
|
|
2282
2310
|
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
2311
|
+
if (direct) (direct.callShapes ||= []).push(analysis);
|
|
2283
2312
|
|
|
2284
2313
|
// Carry callerFile/callerStartLine so tagInTestCase can resolve the
|
|
2285
2314
|
// enclosing function in a later pass.
|
|
@@ -2340,9 +2369,10 @@ function verify(index, name, options = {}) {
|
|
|
2340
2369
|
}
|
|
2341
2370
|
|
|
2342
2371
|
// Check if arg count is valid
|
|
2343
|
-
const countOk =
|
|
2344
|
-
? argCount >=
|
|
2345
|
-
:
|
|
2372
|
+
const countOk = direct && !inheritedCtorOnly
|
|
2373
|
+
? arities.some(arity => argCount >= arity.min && (arity.hasRest || argCount <= arity.max))
|
|
2374
|
+
: hasRest ? argCount >= minArgs
|
|
2375
|
+
: (argCount >= minArgs && argCount <= expectedParamCount);
|
|
2346
2376
|
if (call.rebind && call.rebind.kind !== 'no-overload' && (countOk || call.rebind.kind !== 'count')) {
|
|
2347
2377
|
// Bound the base declaration; the new one may still take it, or
|
|
2348
2378
|
// another overload may: the compiler decides (fix #394).
|
|
@@ -2364,7 +2394,7 @@ function verify(index, name, options = {}) {
|
|
|
2364
2394
|
continue;
|
|
2365
2395
|
}
|
|
2366
2396
|
if (!countOk || call.invalidOverload) {
|
|
2367
|
-
mismatches.push({
|
|
2397
|
+
mismatches.push(located({
|
|
2368
2398
|
file: call.relativePath,
|
|
2369
2399
|
line: call.line,
|
|
2370
2400
|
expression: call.content.trim(),
|
|
@@ -2378,7 +2408,7 @@ function verify(index, name, options = {}) {
|
|
|
2378
2408
|
args: analysis.args,
|
|
2379
2409
|
patterns: patternFlagsFrom(analysis),
|
|
2380
2410
|
...carry,
|
|
2381
|
-
});
|
|
2411
|
+
}, analysis));
|
|
2382
2412
|
continue;
|
|
2383
2413
|
}
|
|
2384
2414
|
|
|
@@ -2399,7 +2429,7 @@ function verify(index, name, options = {}) {
|
|
|
2399
2429
|
...carry,
|
|
2400
2430
|
});
|
|
2401
2431
|
} else {
|
|
2402
|
-
mismatches.push({
|
|
2432
|
+
mismatches.push(located({
|
|
2403
2433
|
file: call.relativePath,
|
|
2404
2434
|
line: call.line,
|
|
2405
2435
|
expression: call.content.trim(),
|
|
@@ -2413,7 +2443,7 @@ function verify(index, name, options = {}) {
|
|
|
2413
2443
|
problem: bindingProblems.join('; '),
|
|
2414
2444
|
patterns: patternFlagsFrom(analysis),
|
|
2415
2445
|
...carry,
|
|
2416
|
-
});
|
|
2446
|
+
}, analysis));
|
|
2417
2447
|
}
|
|
2418
2448
|
continue;
|
|
2419
2449
|
}
|
|
@@ -2425,7 +2455,7 @@ function verify(index, name, options = {}) {
|
|
|
2425
2455
|
...carry,
|
|
2426
2456
|
});
|
|
2427
2457
|
}
|
|
2428
|
-
clearTreeCache(index);
|
|
2458
|
+
if (!direct) clearTreeCache(index);
|
|
2429
2459
|
|
|
2430
2460
|
// Feature A: tag each entry with `inTestCase` based on its enclosing function.
|
|
2431
2461
|
// Done after the per-call loop because tagInTestCase prefers a single pass
|
|
@@ -6945,4 +6975,15 @@ function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
|
6945
6975
|
|
|
6946
6976
|
const IDENTITY_TYPE_KINDS_FOR_CREF = new Set(['class', 'struct', 'interface', 'record', 'enum', 'trait', 'type']);
|
|
6947
6977
|
|
|
6948
|
-
|
|
6978
|
+
/** Check already-resolved call sites using exactly the signature rules of verify. */
|
|
6979
|
+
function verifyCallSites(index, definition, sites) {
|
|
6980
|
+
const direct = { definition, sites };
|
|
6981
|
+
const result = verify(index, definition.name, {}, direct);
|
|
6982
|
+
Object.defineProperties(result, {
|
|
6983
|
+
parameterLists: { value: direct.parameterLists },
|
|
6984
|
+
callShapes: { value: direct.callShapes },
|
|
6985
|
+
});
|
|
6986
|
+
return result;
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
module.exports = { verify, verifyCallSites, plan, baseCounterpartOf, sitesReboundByChange, computePlanCallSites, analyzeCallSite, analyzeCallSiteAST, analyzeCallShape, classifyArgNode, findCallNode, clearTreeCache, identifyCallPatterns };
|
package/languages/adapter.js
CHANGED
|
@@ -147,13 +147,18 @@ function createLanguageAdapter(config) {
|
|
|
147
147
|
// safeParse returns the tree the extractors above just built.
|
|
148
148
|
if (config.traits?.reflectionApi && !parsed.reflectionSites) {
|
|
149
149
|
const { safeParse } = require('./index');
|
|
150
|
-
const {
|
|
150
|
+
const { reflectionFactsInTree } = require('../core/reflection');
|
|
151
151
|
// A language reading the file through its own view (C#
|
|
152
152
|
// conditional compilation, fix #391) supplies that tree.
|
|
153
153
|
const analysisTree = typeof languageModule.analysisTree === 'function'
|
|
154
154
|
? languageModule.analysisTree(code, parser) : safeParse(parser, code);
|
|
155
|
-
const
|
|
156
|
-
if (sites.length > 0) parsed.reflectionSites = sites;
|
|
155
|
+
const facts = reflectionFactsInTree(analysisTree, config.name, code, { imports });
|
|
156
|
+
if (facts.sites.length > 0) parsed.reflectionSites = facts.sites;
|
|
157
|
+
// How often the file uses the reflection and code-execution
|
|
158
|
+
// vocabulary: what `repo` and the completeness footer count,
|
|
159
|
+
// read from the index instead of from every file's text.
|
|
160
|
+
if (facts.reflectionUses > 0) parsed.reflectionUses = facts.reflectionUses;
|
|
161
|
+
if (facts.evalUses > 0) parsed.evalUses = facts.evalUses;
|
|
157
162
|
}
|
|
158
163
|
// Computed dispatch sites (`handlers[name]()`) of the literal
|
|
159
164
|
// parse the doctor's blind-spot inventory reads (fix #436): a
|
package/languages/c-family.js
CHANGED
|
@@ -3633,6 +3633,17 @@ function memberQualifiers(declarator) {
|
|
|
3633
3633
|
return out;
|
|
3634
3634
|
}
|
|
3635
3635
|
|
|
3636
|
+
/**
|
|
3637
|
+
* `virtual R f(...) = 0;`: a member function declaration whose initializer
|
|
3638
|
+
* is the literal 0 is pure virtual. A concrete derived class must override
|
|
3639
|
+
* it. (A data member `int x = 0;` has no function declarator and never
|
|
3640
|
+
* reaches the caller.)
|
|
3641
|
+
*/
|
|
3642
|
+
function isPureVirtual(node) {
|
|
3643
|
+
const value = templatedDeclaration(node).childForFieldName('default_value');
|
|
3644
|
+
return !!value && value.type === 'number_literal' && value.text === '0';
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3636
3647
|
function memberFromNode(node, className, access, lines, mode) {
|
|
3637
3648
|
const declarator = functionDeclarator(node);
|
|
3638
3649
|
if (!declarator) return null;
|
|
@@ -3662,6 +3673,7 @@ function memberFromNode(node, className, access, lines, mode) {
|
|
|
3662
3673
|
memberType: isConstructor ? 'constructor' : 'method',
|
|
3663
3674
|
isMethod: true,
|
|
3664
3675
|
isConstructor,
|
|
3676
|
+
...(mode === 'cpp' && !isConstructor && isPureVirtual(node) && { requiredMember: true }),
|
|
3665
3677
|
...(mode === 'cpp' && memberQualifiers(declarator)),
|
|
3666
3678
|
...(enclosingNamespace(node) && {
|
|
3667
3679
|
namespace: enclosingNamespace(node),
|