ucn 5.2.1 → 5.3.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 +49 -3
- package/.claude/skills/ucn/references/commands.md +3 -1
- package/README.md +158 -533
- package/cli/index.js +74 -10
- package/core/account.js +36 -8
- package/core/cache.js +167 -21
- package/core/callers.js +1229 -162
- package/core/execute.js +24 -6
- package/core/graph.js +167 -35
- package/core/index-ir.js +17 -12
- package/core/ir.js +56 -8
- package/core/output/graph.js +60 -11
- package/core/output/lines.js +259 -0
- package/core/output/public.js +15 -0
- package/core/output/reporting.js +9 -2
- package/core/output-budget.js +7 -4
- package/core/project.js +103 -5
- package/core/registry.js +7 -6
- package/core/reporting.js +159 -14
- package/languages/c-family.js +19 -17
- package/languages/go.js +170 -42
- package/languages/javascript.js +470 -15
- package/languages/python.js +678 -38
- package/languages/rust.js +1 -0
- package/mcp/server.js +3 -1
- package/package.json +2 -2
- package/assets/demo.svg +0 -31
package/core/execute.js
CHANGED
|
@@ -464,6 +464,14 @@ const PUBLIC_COMMAND_SET = new Set(CANONICAL_COMMANDS);
|
|
|
464
464
|
* glob, file, and programmatic callers receive the same answer.
|
|
465
465
|
*/
|
|
466
466
|
function validatePublicParams(command, p) {
|
|
467
|
+
if (p.raw && command !== 'source') return '--raw is supported only by source.';
|
|
468
|
+
if (p.lines && !['find', 'usages', 'search', 'show', 'impact'].includes(command)) {
|
|
469
|
+
return '--lines is supported by find, usages, search, show, and impact.';
|
|
470
|
+
}
|
|
471
|
+
if (p.lines && command === 'show' && p.sections) {
|
|
472
|
+
const parsed = parseSections(p.sections, ['callers'], new Set(['callers', 'callees']), 'show --lines');
|
|
473
|
+
if (parsed.error) return parsed.error;
|
|
474
|
+
}
|
|
467
475
|
const hasName = typeof p.name === 'string' && p.name.trim() !== '';
|
|
468
476
|
const gitScope = p.staged || (typeof p.base === 'string' && p.base.trim() !== '');
|
|
469
477
|
if ((command === 'impact' || command === 'check') && hasName && gitScope) {
|
|
@@ -569,7 +577,7 @@ const HANDLERS = {
|
|
|
569
577
|
} : query;
|
|
570
578
|
const parsed = parseSections(
|
|
571
579
|
p.sections,
|
|
572
|
-
['summary', 'callers', 'callees'],
|
|
580
|
+
p.lines ? ['callers'] : ['summary', 'callers', 'callees'],
|
|
573
581
|
SHOW_SECTIONS,
|
|
574
582
|
'show',
|
|
575
583
|
);
|
|
@@ -693,6 +701,8 @@ const HANDLERS = {
|
|
|
693
701
|
const notes = [
|
|
694
702
|
...(resolved.warnings || []).map(warning => warning.message),
|
|
695
703
|
response.note,
|
|
704
|
+
...(p.raw ? (response.result.entries || []).filter(entry => entry.truncated)
|
|
705
|
+
.map(entry => `Source truncated: ${entry.match.relativePath}:${entry.match.startLine}; showing ${entry.shownLines || entry.maxLines} of ${entry.totalLines} lines (--max-lines).`) : []),
|
|
696
706
|
].filter(Boolean);
|
|
697
707
|
return notes.length > 0
|
|
698
708
|
? { ...response, note: combineNotes(notes) }
|
|
@@ -1174,6 +1184,9 @@ const HANDLERS = {
|
|
|
1174
1184
|
file: p.file,
|
|
1175
1185
|
className: p.className,
|
|
1176
1186
|
exact: p.exact || false,
|
|
1187
|
+
// A shell definition listing renders no activity counts. Avoid a
|
|
1188
|
+
// full pinned caller query for every definition in a wildcard.
|
|
1189
|
+
skipCounts: !!p.lines,
|
|
1177
1190
|
exclude,
|
|
1178
1191
|
in: p.in,
|
|
1179
1192
|
});
|
|
@@ -1430,7 +1443,7 @@ const HANDLERS = {
|
|
|
1430
1443
|
exclude,
|
|
1431
1444
|
in: p.in,
|
|
1432
1445
|
file: p.file,
|
|
1433
|
-
top: topVal || 50,
|
|
1446
|
+
top: topVal || (p.lines ? undefined : 50),
|
|
1434
1447
|
});
|
|
1435
1448
|
if (result.meta.error) return { ok: false, error: result.meta.error };
|
|
1436
1449
|
const unsupported = (!p.regex && (p.term || p.name))
|
|
@@ -1456,7 +1469,7 @@ const HANDLERS = {
|
|
|
1456
1469
|
// default at 500 and retain total/truncated metadata for controlled
|
|
1457
1470
|
// expansion with an explicit top/limit.
|
|
1458
1471
|
const topVal = num(p.top, undefined) || num(p.limit, undefined);
|
|
1459
|
-
const effectiveLimit = topVal || 500;
|
|
1472
|
+
const effectiveLimit = topVal || (p.lines ? undefined : 500);
|
|
1460
1473
|
const result = index.search(p.term, {
|
|
1461
1474
|
codeOnly: p.codeOnly || false,
|
|
1462
1475
|
context: num(p.context, 0),
|
|
@@ -2014,9 +2027,12 @@ const HANDLERS = {
|
|
|
2014
2027
|
|
|
2015
2028
|
if (matches.length > 1 && p.all) {
|
|
2016
2029
|
for (const m of matches) {
|
|
2017
|
-
const
|
|
2030
|
+
const fullCode = readAndExtract(m);
|
|
2018
2031
|
const totalLines = m.endLine - m.startLine + 1;
|
|
2019
|
-
|
|
2032
|
+
const truncated = !!maxLines && totalLines > maxLines;
|
|
2033
|
+
const code = truncated ? fullCode.split('\n').slice(0, maxLines).join('\n') : fullCode;
|
|
2034
|
+
entries.push({ match: m, code, totalLines, summaryMode: false, truncated,
|
|
2035
|
+
...(truncated && { maxLines }) });
|
|
2020
2036
|
}
|
|
2021
2037
|
return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
|
|
2022
2038
|
}
|
|
@@ -2035,7 +2051,7 @@ const HANDLERS = {
|
|
|
2035
2051
|
const totalLines = match.endLine - match.startLine + 1;
|
|
2036
2052
|
|
|
2037
2053
|
// Large class summary mode (>200 lines, no maxLines)
|
|
2038
|
-
if (totalLines > 200 && !maxLines) {
|
|
2054
|
+
if (totalLines > 200 && !maxLines && !p.raw) {
|
|
2039
2055
|
const methods = index.findMethodsForType(match.name, match);
|
|
2040
2056
|
entries.push({ match, code: null, methods, totalLines, summaryMode: true, truncated: false });
|
|
2041
2057
|
return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
|
|
@@ -2457,6 +2473,8 @@ function execute(index, command, params = {}) {
|
|
|
2457
2473
|
// logged params object; a frozen object silently no-oped the
|
|
2458
2474
|
// normalizers and returned wrong not-found answers).
|
|
2459
2475
|
params = { ...params };
|
|
2476
|
+
// Listing completeness belongs to execution, including direct API calls.
|
|
2477
|
+
if (params.lines) params.all = true;
|
|
2460
2478
|
try {
|
|
2461
2479
|
if (PUBLIC_COMMAND_SET.has(command)) {
|
|
2462
2480
|
const validationError = validatePublicParams(command, params);
|
package/core/graph.js
CHANGED
|
@@ -13,6 +13,16 @@ const { extractImports, resolveImport } = require('./imports');
|
|
|
13
13
|
const { langTraits } = require('../languages');
|
|
14
14
|
const { isTestFile } = require('./discovery');
|
|
15
15
|
|
|
16
|
+
function importDeferralFields(imp, fileEntry) {
|
|
17
|
+
// A project-local typing module can expose a true TYPE_CHECKING flag.
|
|
18
|
+
// Parser spelling evidence alone cannot prove that guard runtime-false.
|
|
19
|
+
if (imp.deferredReason === 'type-checking' && fileEntry.moduleResolved?.typing) {
|
|
20
|
+
return { deferred: false };
|
|
21
|
+
}
|
|
22
|
+
return { deferred: !!imp.deferred,
|
|
23
|
+
...(imp.deferredReason && { deferredReason: imp.deferredReason }) };
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
/**
|
|
17
27
|
* Resolve imports in a file
|
|
18
28
|
* @param {object} index - ProjectIndex instance
|
|
@@ -49,7 +59,7 @@ function imports(index, filePath) {
|
|
|
49
59
|
isExternal: false,
|
|
50
60
|
isDynamic: true,
|
|
51
61
|
line,
|
|
52
|
-
|
|
62
|
+
...importDeferralFields(imp, fileEntry),
|
|
53
63
|
};
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -66,7 +76,7 @@ function imports(index, filePath) {
|
|
|
66
76
|
isExternal: false,
|
|
67
77
|
isDynamic: true,
|
|
68
78
|
line,
|
|
69
|
-
|
|
79
|
+
...importDeferralFields(imp, fileEntry),
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -97,7 +107,7 @@ function imports(index, filePath) {
|
|
|
97
107
|
// `type: 'dynamic'` with `isDynamic: false` was a contradiction.
|
|
98
108
|
isDynamic: imp.type === 'dynamic',
|
|
99
109
|
line,
|
|
100
|
-
|
|
110
|
+
...importDeferralFields(imp, fileEntry),
|
|
101
111
|
};
|
|
102
112
|
});
|
|
103
113
|
} catch (e) {
|
|
@@ -735,17 +745,15 @@ function graph(index, filePath, options = {}) {
|
|
|
735
745
|
* @param {object} options - { file, exclude }
|
|
736
746
|
* @returns {object} - { cycles, totalFiles, summary }
|
|
737
747
|
*/
|
|
748
|
+
const DEFAULT_CYCLE_LIMIT = 500;
|
|
749
|
+
const MAX_ENUMERATED_COMPONENT = 2000;
|
|
750
|
+
|
|
738
751
|
function circularDeps(index, options = {}) {
|
|
739
752
|
index._beginOp();
|
|
740
753
|
try {
|
|
741
754
|
const exclude = options.exclude || [];
|
|
742
755
|
const fileFilter = options.file || null;
|
|
743
756
|
|
|
744
|
-
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
745
|
-
const color = new Map();
|
|
746
|
-
const cycles = [];
|
|
747
|
-
const stack = [];
|
|
748
|
-
|
|
749
757
|
const shouldSkip = (file) => {
|
|
750
758
|
if (!index.files.has(file)) return true;
|
|
751
759
|
if (exclude.length > 0) {
|
|
@@ -755,37 +763,137 @@ function circularDeps(index, options = {}) {
|
|
|
755
763
|
return false;
|
|
756
764
|
};
|
|
757
765
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
766
|
+
// Complete, order-independent enumeration (fix #339). The previous
|
|
767
|
+
// 3-color DFS reported only back-edge cycles: a cycle closing through
|
|
768
|
+
// an already-finished node was never emitted, so the answer depended
|
|
769
|
+
// on traversal order (click: 10 cycles in canonical order, 20 with
|
|
770
|
+
// reversed neighbor order, 4 shared). Tarjan strongly connected
|
|
771
|
+
// components + Johnson's elementary-circuit search over a canonically
|
|
772
|
+
// sorted adjacency — the same cycles for every build history — capped
|
|
773
|
+
// and disclosed (`summary.truncated`) so a dense tangle cannot explode.
|
|
774
|
+
const nodes = [...index.files.keys()].filter(f => !shouldSkip(f)).sort(codeUnitCompare);
|
|
775
|
+
const nodeIndex = new Map(nodes.map((f, i) => [f, i]));
|
|
776
|
+
const adj = nodes.map(f => {
|
|
777
|
+
const out = new Set();
|
|
778
|
+
for (const n of index.importGraph.get(f) || []) {
|
|
779
|
+
const j = nodeIndex.get(n);
|
|
780
|
+
if (j != null && n !== f) out.add(j); // self-imports are not cycles
|
|
781
|
+
}
|
|
782
|
+
return [...out].sort((a, b) => a - b);
|
|
783
|
+
});
|
|
784
|
+
const components = [];
|
|
785
|
+
{
|
|
786
|
+
let counter = 0;
|
|
787
|
+
const idxOf = new Array(nodes.length).fill(-1);
|
|
788
|
+
const low = new Array(nodes.length).fill(0);
|
|
789
|
+
const onStack = new Array(nodes.length).fill(false);
|
|
790
|
+
const tarjanStack = [];
|
|
791
|
+
for (let root = 0; root < nodes.length; root++) {
|
|
792
|
+
if (idxOf[root] !== -1) continue;
|
|
793
|
+
idxOf[root] = low[root] = counter++;
|
|
794
|
+
tarjanStack.push(root); onStack[root] = true;
|
|
795
|
+
const work = [[root, 0]];
|
|
796
|
+
while (work.length > 0) {
|
|
797
|
+
const frame = work[work.length - 1];
|
|
798
|
+
const v = frame[0];
|
|
799
|
+
if (frame[1] < adj[v].length) {
|
|
800
|
+
const w = adj[v][frame[1]++];
|
|
801
|
+
if (idxOf[w] === -1) {
|
|
802
|
+
idxOf[w] = low[w] = counter++;
|
|
803
|
+
tarjanStack.push(w); onStack[w] = true;
|
|
804
|
+
work.push([w, 0]);
|
|
805
|
+
} else if (onStack[w]) {
|
|
806
|
+
low[v] = Math.min(low[v], idxOf[w]);
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
work.pop();
|
|
810
|
+
if (work.length > 0) {
|
|
811
|
+
const u = work[work.length - 1][0];
|
|
812
|
+
low[u] = Math.min(low[u], low[v]);
|
|
813
|
+
}
|
|
814
|
+
if (low[v] === idxOf[v]) {
|
|
815
|
+
const comp = [];
|
|
816
|
+
let w;
|
|
817
|
+
do { w = tarjanStack.pop(); onStack[w] = false; comp.push(w); } while (w !== v);
|
|
818
|
+
components.push(comp.sort((a, b) => a - b));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
773
821
|
}
|
|
774
822
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
823
|
+
}
|
|
824
|
+
const cycleLimit = Number.isInteger(options.maxCycles) && options.maxCycles > 0
|
|
825
|
+
? options.maxCycles : DEFAULT_CYCLE_LIMIT;
|
|
826
|
+
const cycles = [];
|
|
827
|
+
let truncated = false;
|
|
828
|
+
let limitReached = false;
|
|
829
|
+
const truncationReasons = new Set();
|
|
830
|
+
const cyclicComponents = components.filter(c => c.length >= 2).sort((a, b) => a[0] - b[0]);
|
|
831
|
+
for (const comp of cyclicComponents) {
|
|
832
|
+
if (limitReached) break;
|
|
833
|
+
if (comp.length > MAX_ENUMERATED_COMPONENT) {
|
|
834
|
+
truncated = true;
|
|
835
|
+
truncationReasons.add('component-size');
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const inComp = new Set(comp);
|
|
839
|
+
for (const s of comp) {
|
|
840
|
+
if (limitReached) break;
|
|
841
|
+
const allowed = (v) => v >= s && inComp.has(v);
|
|
842
|
+
const blocked = new Map();
|
|
843
|
+
const blockedBy = new Map();
|
|
844
|
+
const trail = [];
|
|
845
|
+
const unblock = (u) => {
|
|
846
|
+
blocked.set(u, false);
|
|
847
|
+
const set = blockedBy.get(u);
|
|
848
|
+
if (!set) return;
|
|
849
|
+
blockedBy.set(u, new Set());
|
|
850
|
+
for (const w of set) if (blocked.get(w)) unblock(w);
|
|
851
|
+
};
|
|
852
|
+
const circuit = (v) => {
|
|
853
|
+
let found = false;
|
|
854
|
+
trail.push(v);
|
|
855
|
+
blocked.set(v, true);
|
|
856
|
+
for (const w of adj[v]) {
|
|
857
|
+
if (!allowed(w)) continue;
|
|
858
|
+
if (w === s) {
|
|
859
|
+
// Probe one extra cycle to distinguish a full list
|
|
860
|
+
// of exactly N from a genuinely truncated list.
|
|
861
|
+
if (cycles.length === cycleLimit) {
|
|
862
|
+
truncated = limitReached = true;
|
|
863
|
+
truncationReasons.add('cycle-limit');
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
cycles.push(trail.map(i => nodes[i]));
|
|
867
|
+
found = true;
|
|
868
|
+
} else if (!blocked.get(w)) {
|
|
869
|
+
if (circuit(w)) found = true;
|
|
870
|
+
if (limitReached) break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (found) {
|
|
874
|
+
unblock(v);
|
|
875
|
+
} else {
|
|
876
|
+
for (const w of adj[v]) {
|
|
877
|
+
if (!allowed(w)) continue;
|
|
878
|
+
if (!blockedBy.has(w)) blockedBy.set(w, new Set());
|
|
879
|
+
blockedBy.get(w).add(v);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
trail.pop();
|
|
883
|
+
return found;
|
|
884
|
+
};
|
|
885
|
+
circuit(s);
|
|
783
886
|
}
|
|
784
887
|
}
|
|
888
|
+
const componentSummaries = cyclicComponents.map(comp => ({
|
|
889
|
+
files: comp.map(i => index.files.get(nodes[i])?.relativePath || path.relative(index.root, nodes[i]))
|
|
890
|
+
.sort(codeUnitCompare),
|
|
891
|
+
size: comp.length,
|
|
892
|
+
}));
|
|
785
893
|
|
|
786
894
|
const importEdgeDetails = (fromFile, toFile) => {
|
|
787
895
|
const entry = index.files.get(fromFile);
|
|
788
|
-
if (!entry
|
|
896
|
+
if (!entry) return [];
|
|
789
897
|
const matches = [];
|
|
790
898
|
for (const detail of entry.importDetails || []) {
|
|
791
899
|
const specs = [detail.module];
|
|
@@ -804,7 +912,7 @@ function circularDeps(index, options = {}) {
|
|
|
804
912
|
from: entry.relativePath,
|
|
805
913
|
to: index.files.get(toFile)?.relativePath || path.relative(index.root, toFile),
|
|
806
914
|
line: detail.line ?? null,
|
|
807
|
-
|
|
915
|
+
...importDeferralFields(detail, entry),
|
|
808
916
|
});
|
|
809
917
|
}
|
|
810
918
|
return matches;
|
|
@@ -832,10 +940,14 @@ function circularDeps(index, options = {}) {
|
|
|
832
940
|
const to = rotatedAbs[(edgeIndex + 1) % rotatedAbs.length];
|
|
833
941
|
const details = importEdgeDetails(from, to);
|
|
834
942
|
const deferred = details.length > 0 && details.every(detail => detail.deferred);
|
|
943
|
+
const deferredReasons = deferred
|
|
944
|
+
? [...new Set(details.map(detail => detail.deferredReason).filter(Boolean))].sort()
|
|
945
|
+
: [];
|
|
835
946
|
edges.push({
|
|
836
947
|
from: rotated[edgeIndex],
|
|
837
948
|
to: rotated[(edgeIndex + 1) % rotated.length],
|
|
838
949
|
deferred,
|
|
950
|
+
...(deferredReasons.length > 0 && { deferredReasons }),
|
|
839
951
|
...(details.length > 0 && {
|
|
840
952
|
lines: [...new Set(details.map(detail => detail.line)
|
|
841
953
|
.filter(line => line != null))].sort((a, b) => a - b),
|
|
@@ -847,6 +959,7 @@ function circularDeps(index, options = {}) {
|
|
|
847
959
|
from: edge.from,
|
|
848
960
|
to: edge.to,
|
|
849
961
|
line: edge.lines?.[0] ?? null,
|
|
962
|
+
...(edge.deferredReasons && { reasons: edge.deferredReasons }),
|
|
850
963
|
...(edge.lines?.length > 1 && { lines: edge.lines }),
|
|
851
964
|
}));
|
|
852
965
|
uniqueCycles.push({
|
|
@@ -865,7 +978,7 @@ function circularDeps(index, options = {}) {
|
|
|
865
978
|
result = uniqueCycles.filter(c => c.files.some(f => f.includes(fileFilter)));
|
|
866
979
|
}
|
|
867
980
|
|
|
868
|
-
result.sort((a, b) => a.length - b.length || codeUnitCompare(a.files
|
|
981
|
+
result.sort((a, b) => a.length - b.length || codeUnitCompare(a.files.join('\0'), b.files.join('\0')));
|
|
869
982
|
|
|
870
983
|
// Count files that participate in import graph (have edges)
|
|
871
984
|
let filesWithImports = 0;
|
|
@@ -875,16 +988,35 @@ function circularDeps(index, options = {}) {
|
|
|
875
988
|
|
|
876
989
|
const eagerCycles = result.filter(cycle => cycle.classification !== 'deferred').length;
|
|
877
990
|
const deferredCycles = result.length - eagerCycles;
|
|
991
|
+
const groupOf = new Map();
|
|
992
|
+
componentSummaries.forEach((group, i) => { for (const f of group.files) groupOf.set(f, i); });
|
|
993
|
+
for (const group of componentSummaries) { group.eagerCycles = 0; group.deferredCycles = 0; }
|
|
994
|
+
for (const cycle of result) {
|
|
995
|
+
const group = componentSummaries[groupOf.get(cycle.files[0])];
|
|
996
|
+
if (!group) continue;
|
|
997
|
+
if (cycle.classification === 'deferred') group.deferredCycles++;
|
|
998
|
+
else group.eagerCycles++;
|
|
999
|
+
}
|
|
1000
|
+
const cycleGroups = fileFilter
|
|
1001
|
+
? componentSummaries.filter(c => c.files.some(f => f.includes(fileFilter)))
|
|
1002
|
+
: componentSummaries;
|
|
878
1003
|
return {
|
|
879
1004
|
cycles: result,
|
|
1005
|
+
// Strongly connected groups: every file in a group sits on at
|
|
1006
|
+
// least one cycle with every other member — the refactor unit.
|
|
1007
|
+
components: cycleGroups,
|
|
880
1008
|
totalFiles: index.files.size,
|
|
881
1009
|
filesWithImports,
|
|
882
1010
|
fileFilter: fileFilter || undefined,
|
|
883
1011
|
summary: {
|
|
884
1012
|
totalCycles: result.length,
|
|
885
|
-
filesInCycles: new Set(
|
|
1013
|
+
filesInCycles: new Set(cycleGroups.flatMap(c => c.files)).size,
|
|
886
1014
|
eagerCycles,
|
|
887
1015
|
deferredCycles,
|
|
1016
|
+
componentCount: cycleGroups.length,
|
|
1017
|
+
...(truncated && { truncated: true, cycleLimit,
|
|
1018
|
+
maxComponentSize: MAX_ENUMERATED_COMPONENT,
|
|
1019
|
+
truncationReasons: [...truncationReasons] }),
|
|
888
1020
|
}
|
|
889
1021
|
};
|
|
890
1022
|
} finally {
|
package/core/index-ir.js
CHANGED
|
@@ -21,6 +21,7 @@ function createImportBindings(imports) {
|
|
|
21
21
|
...(rename && { alias: rename.local }),
|
|
22
22
|
...(item.defaultLike && { defaultLike: true }),
|
|
23
23
|
...(item.deferred && { deferred: true }),
|
|
24
|
+
...(item.dynamic && { dynamic: true }),
|
|
24
25
|
};
|
|
25
26
|
}));
|
|
26
27
|
}
|
|
@@ -47,15 +48,17 @@ function createFileEntryFromIR({
|
|
|
47
48
|
mtime,
|
|
48
49
|
size,
|
|
49
50
|
imports: imports.map(item => item.module),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
-
|
|
51
|
+
// Per-import detail records (fix #307, Python-only then; un-gated in
|
|
52
|
+
// fix #338 so JS/TS function-local require()/import() and type-only
|
|
53
|
+
// imports classify cycle edges the same way).
|
|
54
|
+
importDetails: imports.map(item => ({
|
|
55
|
+
module: item.module,
|
|
56
|
+
names: [...(item.names || [])],
|
|
57
|
+
...(item.type && { type: item.type }),
|
|
58
|
+
...(item.line != null && { line: item.line }),
|
|
59
|
+
...(item.deferred && { deferred: true }),
|
|
60
|
+
...(item.deferredReason && { deferredReason: item.deferredReason }),
|
|
61
|
+
})),
|
|
59
62
|
globalImports: imports.filter(item => item.global).map(item => item.module),
|
|
60
63
|
importNames: imports.flatMap(item => item.names || []),
|
|
61
64
|
importBindings: createImportBindings(imports),
|
|
@@ -76,9 +79,9 @@ function createFileEntryFromIR({
|
|
|
76
79
|
|
|
77
80
|
const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
78
81
|
'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
|
|
79
|
-
'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
|
|
82
|
+
'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
|
|
80
83
|
'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
|
|
81
|
-
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
84
|
+
'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
82
85
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
83
86
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
84
87
|
'registryMember', 'registryContainer', 'namespace',
|
|
@@ -86,9 +89,11 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
86
89
|
'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
87
90
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
88
91
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
92
|
+
'returnedCallStart', 'returnedCallEnd',
|
|
93
|
+
'returnedReceiverPath',
|
|
89
94
|
'isSpecialization',
|
|
90
95
|
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
91
|
-
'aliasOwner', 'aliasMember', 'macroParamEffects',
|
|
96
|
+
'aliasOwner', 'aliasMember', 'callableTarget', 'macroParamEffects',
|
|
92
97
|
]);
|
|
93
98
|
|
|
94
99
|
function materializeSymbol(fileEntry, item) {
|
package/core/ir.js
CHANGED
|
@@ -11,6 +11,24 @@
|
|
|
11
11
|
const IR_SCHEMA_VERSION = 1;
|
|
12
12
|
const EVIDENCE_TIERS = Object.freeze(['confirmed', 'unverified', 'excluded']);
|
|
13
13
|
|
|
14
|
+
// Runtime receiver identity for overload-heavy JS/TS member aliases. Generic
|
|
15
|
+
// arguments may differ while the produced value still has one concrete owner
|
|
16
|
+
// (`Factory.create(): Schema<A>` / `Schema<B>`). Transparent/async wrappers
|
|
17
|
+
// are deliberately rejected here: downstream assignment flow unwraps them,
|
|
18
|
+
// so agreement on Promise/Optional alone would not prove the inner receiver.
|
|
19
|
+
function callableAliasReturnHead(returnType) {
|
|
20
|
+
if (!returnType || typeof returnType !== 'string') return null;
|
|
21
|
+
const text = returnType.trim();
|
|
22
|
+
const match = text.match(
|
|
23
|
+
/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*(?:<[\s\S]*>|\[[\s\S]*\])?$/);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
const head = match[1].split('.').pop();
|
|
26
|
+
if (['Promise', 'Awaitable', 'Optional', 'Annotated', 'Final'].includes(head)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return head;
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
15
33
|
let normalizedOwner = owner || symbol.className || null;
|
|
16
34
|
if (!normalizedOwner && symbol.receiver && family === 'callable') {
|
|
@@ -43,9 +61,9 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
43
61
|
};
|
|
44
62
|
const passthrough = [
|
|
45
63
|
'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
|
|
46
|
-
'isAsync', 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent',
|
|
64
|
+
'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent',
|
|
47
65
|
'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
|
|
48
|
-
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
66
|
+
'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
49
67
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
50
68
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
51
69
|
'registryMember', 'registryContainer', 'isConstructor',
|
|
@@ -53,9 +71,11 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
53
71
|
'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
54
72
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
55
73
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
74
|
+
'returnedCallStart', 'returnedCallEnd',
|
|
75
|
+
'returnedReceiverPath',
|
|
56
76
|
'isSpecialization',
|
|
57
77
|
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
58
|
-
'aliasOwner', 'aliasMember', 'macroParamEffects',
|
|
78
|
+
'aliasOwner', 'aliasMember', 'callableTarget', 'macroParamEffects',
|
|
59
79
|
];
|
|
60
80
|
for (const field of passthrough) {
|
|
61
81
|
if (symbol[field] !== undefined && symbol[field] !== null) {
|
|
@@ -106,6 +126,9 @@ function createFileIR({
|
|
|
106
126
|
traitImpl: true,
|
|
107
127
|
traitName: type.traitName,
|
|
108
128
|
}),
|
|
129
|
+
...(type.generics && {
|
|
130
|
+
ownerGenerics: type.generics,
|
|
131
|
+
}),
|
|
109
132
|
};
|
|
110
133
|
append(inherited,
|
|
111
134
|
['field', 'property'].includes(inherited.memberType)
|
|
@@ -120,19 +143,44 @@ function createFileIR({
|
|
|
120
143
|
// An immutable module-scope member alias has the callable signature of
|
|
121
144
|
// the class member it captures: `const make = Widget.create`. Materialize
|
|
122
145
|
// the local value and each explicit export alias as real function symbols
|
|
123
|
-
// only when its member is static and every declared return type
|
|
124
|
-
//
|
|
125
|
-
//
|
|
146
|
+
// only when its member is static and every declared return type has the
|
|
147
|
+
// same concrete runtime head. Generic arguments may differ across legal
|
|
148
|
+
// overloads; a different head remains ambiguous. This is compiler-visible
|
|
149
|
+
// identity; mutable aliases and ambiguous overload returns stay rejected.
|
|
126
150
|
for (const alias of (parsed.callableAliases || [])) {
|
|
127
|
-
|
|
151
|
+
let sources = normalizedSymbols.filter(symbol =>
|
|
128
152
|
symbol.name === alias.member && symbol.owner === alias.owner &&
|
|
129
153
|
(symbol.params !== undefined || symbol.paramsStructured) &&
|
|
130
154
|
(symbol.modifiers?.includes('static') ||
|
|
131
155
|
String(symbol.memberType || symbol.kind).startsWith('static')) &&
|
|
132
156
|
symbol.returnType);
|
|
157
|
+
if (sources.length === 0) {
|
|
158
|
+
// Static callable forwarding (zod-measured):
|
|
159
|
+
// `static create = createSchema; const schema = Type.create`.
|
|
160
|
+
// The parser records only a direct identifier initializer. Pin it
|
|
161
|
+
// to top-level callables in this file, and require every matching
|
|
162
|
+
// field declaration to agree before borrowing its signatures.
|
|
163
|
+
const forwarded = normalizedSymbols.filter(symbol =>
|
|
164
|
+
symbol.name === alias.member && symbol.owner === alias.owner &&
|
|
165
|
+
symbol.family === 'state' && symbol.callableTarget &&
|
|
166
|
+
symbol.modifiers?.includes('static'));
|
|
167
|
+
const targets = new Set(forwarded.map(symbol => symbol.callableTarget));
|
|
168
|
+
if (forwarded.length > 0 && targets.size === 1) {
|
|
169
|
+
const [target] = targets;
|
|
170
|
+
sources = normalizedSymbols.filter(symbol =>
|
|
171
|
+
symbol.name === target && symbol.family === 'callable' &&
|
|
172
|
+
!symbol.owner && !symbol.isNested &&
|
|
173
|
+
(symbol.params !== undefined || symbol.paramsStructured) &&
|
|
174
|
+
symbol.returnType);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
133
177
|
if (sources.length === 0) continue;
|
|
134
178
|
const sourceReturns = new Set(sources.map(source => source.returnType));
|
|
135
|
-
if (sourceReturns.size !== 1)
|
|
179
|
+
if (sourceReturns.size !== 1) {
|
|
180
|
+
const heads = new Set(sources.map(source =>
|
|
181
|
+
callableAliasReturnHead(source.returnType)));
|
|
182
|
+
if (heads.has(null) || heads.size !== 1) continue;
|
|
183
|
+
}
|
|
136
184
|
const source = sources[0];
|
|
137
185
|
const exported = (parsed.exports || []).filter(item =>
|
|
138
186
|
!item.source && item.name === alias.name);
|