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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* core/hot-summary.js - The HOT ranking of `repo`, kept per index state.
|
|
5
|
+
*
|
|
6
|
+
* Ranking the most-called functions runs an exact, pinned caller query on
|
|
7
|
+
* every definition able to enter the list (core/reporting.js getStats). On a
|
|
8
|
+
* 3,400-file project that is about 1,300 queries and ten seconds, and the
|
|
9
|
+
* answer is the same until a file changes: it is a function of the index and
|
|
10
|
+
* of the options that shape the ranking, nothing else. So it is kept:
|
|
11
|
+
*
|
|
12
|
+
* - in memory, on the index, until the index is rebuilt;
|
|
13
|
+
* - on disk beside the index (hot-summary.json), tagged with the index
|
|
14
|
+
* generation it was computed on. A loaded index reads it back only when
|
|
15
|
+
* its own generation is the tagged one. Every save of the index starts a
|
|
16
|
+
* new generation and rewrites the file under it; a rebuild drops the
|
|
17
|
+
* summaries first, so a changed file always ranks afresh.
|
|
18
|
+
*
|
|
19
|
+
* A summary answers only the request it was computed for: the key is every
|
|
20
|
+
* option that reaches the ranking (list length, production-only counting,
|
|
21
|
+
* both budgets, scope). No summary is derived from another, so a kept answer
|
|
22
|
+
* is the answer a fresh ranking gives, byte for byte. Fresh and kept answers
|
|
23
|
+
* leave this module through the same serialization.
|
|
24
|
+
*
|
|
25
|
+
* Two more conditions keep that true. A ranking is kept only when every
|
|
26
|
+
* indexed file still is what the index recorded once it has been computed:
|
|
27
|
+
* call records are refreshed from a file that changes under a running
|
|
28
|
+
* command, and such a ranking describes neither the index nor the edit. And
|
|
29
|
+
* a stored ranking is read only by the engine build that computed it: unlike
|
|
30
|
+
* a parsed fact it is a verdict of the caller engine, so an edited engine
|
|
31
|
+
* file in a development tree ends it.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
const { version: UCN_VERSION } = require('../package.json');
|
|
38
|
+
|
|
39
|
+
const SUMMARY_FILE = 'hot-summary.json';
|
|
40
|
+
const SUMMARY_FORMAT = 1;
|
|
41
|
+
// Requests differ by scope and list length; a session asks a handful.
|
|
42
|
+
const MAX_SUMMARIES = 8;
|
|
43
|
+
// Listed functions kept on disk across all summaries. A list longer than
|
|
44
|
+
// this (`--hot --top=10000`) is ranked again instead of being stored.
|
|
45
|
+
const MAX_STORED_ITEMS = 2000;
|
|
46
|
+
|
|
47
|
+
/** The request a summary answers: every option the ranking reads. */
|
|
48
|
+
function keyOf({ top, productionCallsOnly, maxRefine, workBudget, file, in: inScope, exclude }) {
|
|
49
|
+
return JSON.stringify([
|
|
50
|
+
top,
|
|
51
|
+
!!productionCallsOnly,
|
|
52
|
+
Number.isFinite(maxRefine) ? maxRefine : null,
|
|
53
|
+
Number.isFinite(workBudget) ? workBudget : null,
|
|
54
|
+
file || null,
|
|
55
|
+
inScope || null,
|
|
56
|
+
Array.isArray(exclude) && exclude.length > 0 ? exclude : null,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let engineStampMemo = null;
|
|
61
|
+
/**
|
|
62
|
+
* The engine build: the package version and the size and modification time
|
|
63
|
+
* of every engine source file. Constant for an installed package.
|
|
64
|
+
*/
|
|
65
|
+
function engineStamp() {
|
|
66
|
+
if (engineStampMemo) return engineStampMemo;
|
|
67
|
+
const hash = crypto.createHash('md5').update(UCN_VERSION);
|
|
68
|
+
const root = path.join(__dirname, '..');
|
|
69
|
+
for (const folder of ['core', 'core/output', 'languages']) {
|
|
70
|
+
let names = [];
|
|
71
|
+
try {
|
|
72
|
+
names = fs.readdirSync(path.join(root, folder)).filter(name => name.endsWith('.js')).sort();
|
|
73
|
+
} catch (_) { /* a folder this build does not have */ }
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
let state = '?';
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.statSync(path.join(root, folder, name));
|
|
78
|
+
state = `${stat.size}:${stat.mtimeMs}`;
|
|
79
|
+
} catch (_) { /* unreadable: its own state */ }
|
|
80
|
+
hash.update(`\0${folder}/${name}:${state}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return (engineStampMemo = hash.digest('hex'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether every indexed file still is what the index recorded: same size
|
|
88
|
+
* and modification time, or the same content when only the time moved (the
|
|
89
|
+
* rule the staleness check applies).
|
|
90
|
+
*/
|
|
91
|
+
function sourceMatchesIndex(index) {
|
|
92
|
+
for (const [filePath, entry] of index.files) {
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.statSync(filePath);
|
|
95
|
+
if (entry.size !== undefined && stat.size !== entry.size) return false;
|
|
96
|
+
if (entry.mtime && stat.mtimeMs === entry.mtime) continue;
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
if (crypto.createHash('md5').update(content).digest('hex') !== entry.hash) return false;
|
|
99
|
+
} catch (_) { return false; }
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cacheDirOf(index, cachePath) {
|
|
105
|
+
if (cachePath) return path.dirname(cachePath);
|
|
106
|
+
if (index._cacheDir) return index._cacheDir;
|
|
107
|
+
return require('./cache').getProjectCacheDir(index.root);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A summary as every reader receives it: plain data, its own copy. */
|
|
111
|
+
function copyOf(summary) {
|
|
112
|
+
return JSON.parse(JSON.stringify(summary));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validSummary(summary) {
|
|
116
|
+
return !!summary && typeof summary === 'object' && Number.isInteger(summary.top) &&
|
|
117
|
+
Array.isArray(summary.items) && summary.items.every(item =>
|
|
118
|
+
item && typeof item === 'object' && typeof item.name === 'string' &&
|
|
119
|
+
Number.isInteger(item.callCount));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read the stored summaries of the generation in memory, once per
|
|
124
|
+
* generation. An index that was rebuilt, or never saved, has none.
|
|
125
|
+
*/
|
|
126
|
+
function ensure(index) {
|
|
127
|
+
const generation = index._callsGenerationKey || null;
|
|
128
|
+
if (index._hotSummariesGeneration === generation && index._hotSummaries) return;
|
|
129
|
+
// Summaries computed since the last save stay: the state they describe
|
|
130
|
+
// is the one in memory.
|
|
131
|
+
const kept = index._hotSummaries || new Map();
|
|
132
|
+
index._hotSummaries = kept;
|
|
133
|
+
index._hotSummariesGeneration = generation;
|
|
134
|
+
if (!generation || index.isProvenanceFactIndex) return;
|
|
135
|
+
let stored;
|
|
136
|
+
try {
|
|
137
|
+
stored = JSON.parse(fs.readFileSync(path.join(cacheDirOf(index), SUMMARY_FILE), 'utf8'));
|
|
138
|
+
} catch (_) { return; }
|
|
139
|
+
if (!stored || stored.format !== SUMMARY_FORMAT || stored.engine !== engineStamp() ||
|
|
140
|
+
stored.generation !== generation || !Array.isArray(stored.summaries)) return;
|
|
141
|
+
for (const row of stored.summaries) {
|
|
142
|
+
if (!Array.isArray(row) || row.length !== 2 || typeof row[0] !== 'string' ||
|
|
143
|
+
!validSummary(row[1])) return;
|
|
144
|
+
}
|
|
145
|
+
for (const [key, summary] of stored.summaries) {
|
|
146
|
+
if (!kept.has(key)) kept.set(key, summary);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The kept answer to this request, or undefined. */
|
|
151
|
+
function get(index, key) {
|
|
152
|
+
ensure(index);
|
|
153
|
+
const summary = index._hotSummaries.get(key);
|
|
154
|
+
if (summary === undefined) return undefined;
|
|
155
|
+
// A caller may edit source between the surface's freshness check and
|
|
156
|
+
// this query, or use a live index directly. Native caller queries refresh
|
|
157
|
+
// those records; a kept verdict must not bypass that existing path.
|
|
158
|
+
if (!sourceMatchesIndex(index)) {
|
|
159
|
+
reset(index);
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
// Most recently asked last: the bound drops the oldest.
|
|
163
|
+
index._hotSummaries.delete(key);
|
|
164
|
+
index._hotSummaries.set(key, summary);
|
|
165
|
+
return copyOf(summary);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Keep a freshly ranked answer; returns the copy the caller hands out. */
|
|
169
|
+
function put(index, key, summary) {
|
|
170
|
+
const stored = copyOf(summary);
|
|
171
|
+
// Ranked while a file was being edited: answered, not kept.
|
|
172
|
+
if (!sourceMatchesIndex(index)) return stored;
|
|
173
|
+
ensure(index);
|
|
174
|
+
index._hotSummaries.delete(key);
|
|
175
|
+
index._hotSummaries.set(key, stored);
|
|
176
|
+
while (index._hotSummaries.size > MAX_SUMMARIES) {
|
|
177
|
+
index._hotSummaries.delete(index._hotSummaries.keys().next().value);
|
|
178
|
+
}
|
|
179
|
+
index.hotSummaryDirty = true;
|
|
180
|
+
return copyOf(stored);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Drop every summary: the index is being changed. */
|
|
184
|
+
function reset(index) {
|
|
185
|
+
index._hotSummaries = new Map();
|
|
186
|
+
// The generation in memory no longer describes the index, so its stored
|
|
187
|
+
// summaries are not read back either.
|
|
188
|
+
index._hotSummariesGeneration = index._callsGenerationKey || null;
|
|
189
|
+
// A direct indexFile/removeFileSymbols mutation leaves the calls-shard
|
|
190
|
+
// generation in place. It no longer identifies the state ranked here.
|
|
191
|
+
index._hotSummarySavedGeneration = null;
|
|
192
|
+
index.hotSummaryDirty = false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Another saved state was loaded: its stored summaries are read on demand. */
|
|
196
|
+
function forget(index) {
|
|
197
|
+
index._hotSummaries = null;
|
|
198
|
+
index._hotSummariesGeneration = undefined;
|
|
199
|
+
index._hotSummarySavedGeneration = index._callsGenerationKey || null;
|
|
200
|
+
index.hotSummaryDirty = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A completed index save establishes which state a stored answer may name. */
|
|
204
|
+
function adoptSavedGeneration(index) {
|
|
205
|
+
const generation = index._callsGenerationKey || null;
|
|
206
|
+
index._hotSummarySavedGeneration = generation;
|
|
207
|
+
index._hotSummariesGeneration = generation;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Write the summaries under the generation in memory. An index with no
|
|
212
|
+
* generation has no saved state they could be read back against.
|
|
213
|
+
*/
|
|
214
|
+
function save(index, cachePath) {
|
|
215
|
+
const generation = index._callsGenerationKey || null;
|
|
216
|
+
if (!generation || index._hotSummarySavedGeneration !== generation || index.isProvenanceFactIndex) return null;
|
|
217
|
+
// The file is replaced, so what it holds for this generation comes first.
|
|
218
|
+
ensure(index);
|
|
219
|
+
const target = path.join(cacheDirOf(index, cachePath), SUMMARY_FILE);
|
|
220
|
+
const summaries = [];
|
|
221
|
+
let items = 0;
|
|
222
|
+
// Newest first, within the stored-item bound.
|
|
223
|
+
for (const [key, summary] of [...index._hotSummaries].reverse()) {
|
|
224
|
+
if (items + summary.items.length > MAX_STORED_ITEMS) continue;
|
|
225
|
+
items += summary.items.length;
|
|
226
|
+
summaries.unshift([key, summary]);
|
|
227
|
+
}
|
|
228
|
+
index.hotSummaryDirty = false;
|
|
229
|
+
if (summaries.length === 0) {
|
|
230
|
+
try { fs.rmSync(target, { force: true }); } catch (_) { /* nothing stored */ }
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
234
|
+
fs.writeFileSync(target + '.tmp', JSON.stringify({
|
|
235
|
+
format: SUMMARY_FORMAT, engine: engineStamp(), generation, summaries,
|
|
236
|
+
}));
|
|
237
|
+
fs.renameSync(target + '.tmp', target);
|
|
238
|
+
return target;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { SUMMARY_FILE, keyOf, engineStamp, ensure, get, put, reset, forget, adoptSavedGeneration, save };
|
package/core/index-ir.js
CHANGED
|
@@ -146,6 +146,8 @@ function createFileEntryFromIR({
|
|
|
146
146
|
...(ir.reflectionSites?.length > 0 && {
|
|
147
147
|
reflectionSites: ir.reflectionSites,
|
|
148
148
|
}),
|
|
149
|
+
...(ir.reflectionUses > 0 && { reflectionUses: ir.reflectionUses }),
|
|
150
|
+
...(ir.evalUses > 0 && { evalUses: ir.evalUses }),
|
|
149
151
|
...(Array.isArray(ir.computedDispatchSites) && {
|
|
150
152
|
computedDispatchSites: ir.computedDispatchSites,
|
|
151
153
|
}),
|
|
@@ -183,7 +185,7 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
183
185
|
'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'typeParameterConstraints', 'extends', 'implements', 'indent', 'isNested',
|
|
184
186
|
'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType', 'embedded',
|
|
185
187
|
'aliasOf', 'aliasQualifier', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
186
|
-
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
188
|
+
'annotationsWithArgs', 'attributesWithArgs', 'registrationUnknown', 'runtimeOwnerPath', 'nameLine', 'traitImpl',
|
|
187
189
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'assignedObject', 'selfNamed', 'bodyScopedName',
|
|
188
190
|
'registryMember', 'registryContainer', 'registryContainerType', 'objectLiteralLine', 'namespace', 'isConstructor',
|
|
189
191
|
'isExtensionMethod', 'extensionReceiver', 'explicitInterface', 'primaryParams',
|
|
@@ -201,7 +203,7 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
201
203
|
'annotationType', 'valueShape', 'friendOf', 'templateParams', 'delegateParams',
|
|
202
204
|
'typedefName', 'forwardDeclaration', 'anonymousNamespace', 'specialization',
|
|
203
205
|
'memberConst', 'refQualifier', 'fieldPointers', 'memberPaths', 'instanceAttribute',
|
|
204
|
-
'ambientAugmentation',
|
|
206
|
+
'ambientAugmentation', 'requiredMember', 'protocolClass',
|
|
205
207
|
]);
|
|
206
208
|
|
|
207
209
|
function materializeSymbol(fileEntry, item) {
|
|
@@ -228,7 +230,7 @@ function materializeSymbol(fileEntry, item) {
|
|
|
228
230
|
// Most false feature flags are omitted for compactness, but
|
|
229
231
|
// functionLike=false is the semantic distinction between an
|
|
230
232
|
// object-like macro and a callable macro (UCN5-170).
|
|
231
|
-
if (item[field] === false && field !== 'functionLike') continue;
|
|
233
|
+
if (item[field] === false && field !== 'functionLike' && field !== 'registrationUnknown') continue;
|
|
232
234
|
symbol[field] = item[field];
|
|
233
235
|
}
|
|
234
236
|
return symbol;
|
package/core/ir.js
CHANGED
|
@@ -66,7 +66,7 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
66
66
|
'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'typeParameterConstraints', 'extends', 'implements', 'indent',
|
|
67
67
|
'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType', 'embedded',
|
|
68
68
|
'aliasOf', 'aliasQualifier', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
69
|
-
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
69
|
+
'annotationsWithArgs', 'attributesWithArgs', 'registrationUnknown', 'runtimeOwnerPath', 'nameLine', 'traitImpl',
|
|
70
70
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'assignedObject', 'selfNamed', 'bodyScopedName',
|
|
71
71
|
'registryMember', 'registryContainer', 'registryContainerType', 'objectLiteralLine', 'isConstructor',
|
|
72
72
|
'isExtensionMethod', 'extensionReceiver', 'explicitInterface', 'primaryParams',
|
|
@@ -84,7 +84,7 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
84
84
|
'annotationType', 'valueShape', 'friendOf', 'templateParams', 'delegateParams',
|
|
85
85
|
'typedefName', 'forwardDeclaration', 'anonymousNamespace', 'specialization',
|
|
86
86
|
'memberConst', 'refQualifier', 'fieldPointers', 'memberPaths', 'instanceAttribute',
|
|
87
|
-
'ambientAugmentation',
|
|
87
|
+
'ambientAugmentation', 'requiredMember', 'protocolClass',
|
|
88
88
|
];
|
|
89
89
|
for (const field of passthrough) {
|
|
90
90
|
if (symbol[field] !== undefined && symbol[field] !== null) {
|
|
@@ -261,6 +261,10 @@ function createFileIR({
|
|
|
261
261
|
...(parsed.reflectionSites?.length > 0 && {
|
|
262
262
|
reflectionSites: parsed.reflectionSites.map(item => ({ ...item })),
|
|
263
263
|
}),
|
|
264
|
+
// Uses of the reflection and code-execution vocabulary, counted in
|
|
265
|
+
// the tree (core/reflection.js).
|
|
266
|
+
...(parsed.reflectionUses > 0 && { reflectionUses: parsed.reflectionUses }),
|
|
267
|
+
...(parsed.evalUses > 0 && { evalUses: parsed.evalUses }),
|
|
264
268
|
...(Array.isArray(parsed.computedDispatchSites) && {
|
|
265
269
|
computedDispatchSites: parsed.computedDispatchSites.map(item => ({ ...item })),
|
|
266
270
|
}),
|