@crouton-kit/tsym 0.1.0 → 0.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/README.md +1 -1
- package/dist/commands/handlers/find.js +11 -10
- package/dist/commands/handlers/inspect.js +64 -20
- package/dist/commands/handlers/outline.js +25 -14
- package/dist/commands/handlers/show.js +6 -5
- package/dist/commands/handlers/type.js +3 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/tree.js +2 -2
- package/dist/commands/types.d.ts +6 -0
- package/dist/context-exposure.d.ts +9 -0
- package/dist/context-exposure.js +55 -0
- package/dist/core/members.d.ts +9 -0
- package/dist/core/members.js +12 -0
- package/dist/index/build.d.ts +2 -1
- package/dist/index/build.js +3 -3
- package/dist/index/refresh.d.ts +2 -1
- package/dist/index/refresh.js +2 -2
- package/dist/output.js +10 -3
- package/dist/relations/store-relations.d.ts +14 -0
- package/dist/relations/store-relations.js +24 -0
- package/dist/server/client.js +7 -2
- package/dist/server/main.js +129 -53
- package/dist/server/wire.d.ts +4 -1
- package/dist/store/load.js +2 -2
- package/dist/store/meta.d.ts +13 -9
- package/dist/store/meta.js +30 -18
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +1 -1
- package/dist/store/seed.d.ts +13 -0
- package/dist/store/seed.js +185 -0
- package/dist/store/store.d.ts +2 -0
- package/dist/store/store.js +11 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The bundled TypeScript 7 process is always used, so `tsym` works in a checkout w
|
|
|
44
44
|
|
|
45
45
|
## Using it from crtr
|
|
46
46
|
|
|
47
|
-
`tsym` is available inside [crtr](https://www.npmjs.com/package/@north-light/crouter) as `crtr tsym` via the `tsym` plugin in the crouter official marketplace. The plugin passes through to this executable, so it must be on `PATH`.
|
|
47
|
+
`tsym` is available inside [crtr](https://www.npmjs.com/package/@north-light/crouter) as `crtr tsym` via the `tsym` plugin in the crouter official marketplace. The plugin passes through to this executable, so it must be on `PATH`. Prose output of `show`, `type`, or `outline` may be preceded by an `<auto-loaded-context>` block when run inside a crtr node.
|
|
48
48
|
|
|
49
49
|
## License
|
|
50
50
|
|
|
@@ -9,15 +9,13 @@ const handler = async (context, invocation) => {
|
|
|
9
9
|
const prefix = flags['--prefix'] === true;
|
|
10
10
|
const qualified = name.includes('.');
|
|
11
11
|
const rows = new Map();
|
|
12
|
-
for (const declaration of declarations(context)) {
|
|
13
|
-
|
|
14
|
-
if (declaration.kind === 'module' || !(prefix ? candidate.startsWith(name) : candidate === name) || !keep(declaration, flags))
|
|
12
|
+
for (const declaration of declarations(context, name, qualified, prefix)) {
|
|
13
|
+
if (declaration.kind === 'module' || !keep(declaration, flags))
|
|
15
14
|
continue;
|
|
16
15
|
rows.set(declaration.address, declaration);
|
|
17
16
|
}
|
|
18
|
-
for (const alias of aliases(context)) {
|
|
19
|
-
|
|
20
|
-
if (!(prefix ? candidate.startsWith(name) : candidate === name) || !keep(alias, flags))
|
|
17
|
+
for (const alias of aliases(context, name, prefix)) {
|
|
18
|
+
if (!keep(alias, flags))
|
|
21
19
|
continue;
|
|
22
20
|
rows.set(alias.address, alias);
|
|
23
21
|
}
|
|
@@ -31,15 +29,18 @@ const handler = async (context, invocation) => {
|
|
|
31
29
|
receipt: capped ? `${shown.length} of ${sorted.length} find rows shown, limit ${limit}, ${Math.round(performance.now() - started)} ms` : `${shown.length} find row${shown.length === 1 ? '' : 's'}, ${Math.round(performance.now() - started)} ms`,
|
|
32
30
|
};
|
|
33
31
|
};
|
|
34
|
-
function declarations(context) {
|
|
32
|
+
function declarations(context, name, qualified, prefix) {
|
|
35
33
|
if (!context.store)
|
|
36
34
|
throw new DeclarationError('not-in-program', 'The TypeScript index is not available yet.');
|
|
37
|
-
|
|
35
|
+
const property = qualified ? 'qname' : 'name';
|
|
36
|
+
const comparison = prefix ? 'STARTS WITH' : '=';
|
|
37
|
+
return query(context.store, `MATCH (d:Declaration) WHERE d.${property} ${comparison} $name RETURN d.at AS at, d.file AS file, d.kind AS kind, d.exported AS exported, d.name AS name, d.qname AS qname`, { name }).map((row) => ({ address: String(row.at), path: String(row.file), kind: String(row.kind), exported: Boolean(row.exported), name: String(row.name), qualifiedName: String(row.qname) }));
|
|
38
38
|
}
|
|
39
|
-
function aliases(context) {
|
|
39
|
+
function aliases(context, name, prefix) {
|
|
40
40
|
if (!context.store)
|
|
41
41
|
throw new DeclarationError('not-in-program', 'The TypeScript index is not available yet.');
|
|
42
|
-
|
|
42
|
+
const comparison = prefix ? 'STARTS WITH' : '=';
|
|
43
|
+
return query(context.store, `MATCH (m:Declaration)-[e:EXPORTS]->(d:Declaration) WHERE e.name ${comparison} $name RETURN m.file AS fromFile, e.name AS name, e.line AS line, e.col AS col, d.file AS file, d.line AS declarationLine, d.col AS declarationCol`, { name }).flatMap((row) => {
|
|
43
44
|
const fromFile = String(row.fromFile);
|
|
44
45
|
const line = Number(row.line);
|
|
45
46
|
const col = Number(row.col);
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { matchesPathFilters } from '../../core/receipt.js';
|
|
2
2
|
import { normalKind } from '../../read/read.js';
|
|
3
3
|
import { mergeGraphs } from '../../relations/graph.js';
|
|
4
|
-
import { implementations, references, traverse } from '../../relations/store-relations.js';
|
|
4
|
+
import { implementations, memberSummaries, references, traverse } from '../../relations/store-relations.js';
|
|
5
5
|
import { renderGraph } from '../../relations/mermaid.js';
|
|
6
|
-
const relationOrder = ['refs', 'callers', 'calls', 'impl'];
|
|
6
|
+
const relationOrder = ['refs', 'callers', 'calls', 'impl', 'members'];
|
|
7
|
+
const containerKinds = new Set(['class', 'interface', 'enum', 'namespace', 'module', 'type']);
|
|
8
|
+
const callableMemberKinds = new Set(['function', 'method', 'constructor']);
|
|
9
|
+
// A zero is only worth the scope caveat when the kind could have had that relation: a function never has
|
|
10
|
+
// implementations and an interface never makes calls, so those zeros are structural, not a visibility gap.
|
|
11
|
+
const applicableKinds = {
|
|
12
|
+
refs: undefined,
|
|
13
|
+
callers: new Set(['function', 'method', 'constructor', 'getter', 'setter', 'class', 'const', 'let', 'var', 'property', 'module', 'namespace']),
|
|
14
|
+
calls: new Set(['function', 'method', 'constructor', 'getter', 'setter', 'class', 'const', 'let', 'var', 'property', 'module', 'namespace']),
|
|
15
|
+
impl: new Set(['class', 'interface']),
|
|
16
|
+
};
|
|
17
|
+
function applies(relation, kind) { return applicableKinds[relation]?.has(kind) ?? true; }
|
|
18
|
+
const zeroScopeReceipt = '0 counts cover indexed, compiler-resolved relations only; framework, HTTP, and dependency-injection invocation are not visible';
|
|
7
19
|
const handler = async (context, invocation) => {
|
|
8
20
|
const started = performance.now();
|
|
9
21
|
const selected = selectedRelations(invocation.input.flags['--only']);
|
|
@@ -16,35 +28,54 @@ const handler = async (context, invocation) => {
|
|
|
16
28
|
return graphAnswer(context, seeds, selected, depth, filters, started);
|
|
17
29
|
const blocks = [];
|
|
18
30
|
let answers = 0;
|
|
31
|
+
let hasZeroRelation = false;
|
|
19
32
|
const receiptParts = [];
|
|
20
33
|
for (const seed of seeds) {
|
|
21
34
|
const header = await signature(context, seed);
|
|
35
|
+
const summaries = containerKinds.has(seed.kind) ? await memberSummaries(context, seed) : [];
|
|
36
|
+
const isContainer = summaries.length > 0;
|
|
22
37
|
const relations = [];
|
|
38
|
+
const seedReceipt = [];
|
|
23
39
|
for (const relation of selected) {
|
|
24
40
|
if (relation === 'refs') {
|
|
25
41
|
const rows = await references(context, seed, filters);
|
|
26
42
|
answers += rows.length;
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
hasZeroRelation ||= rows.length === 0;
|
|
44
|
+
relations.push(refBlock(rows, each, true));
|
|
45
|
+
seedReceipt.push(`${rows.length} refs`);
|
|
29
46
|
}
|
|
30
47
|
else if (relation === 'impl') {
|
|
31
48
|
const rows = (await implementations(context, seed)).filter((item) => matchesPathFilters(item.location.path, filters));
|
|
32
49
|
answers += rows.length;
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
hasZeroRelation ||= rows.length === 0 && applies('impl', seed.kind);
|
|
51
|
+
relations.push(implBlock(rows, each, applies('impl', seed.kind)));
|
|
52
|
+
seedReceipt.push(`${rows.length} impl`);
|
|
53
|
+
}
|
|
54
|
+
else if (relation === 'members') {
|
|
55
|
+
if (isContainer) {
|
|
56
|
+
answers += summaries.length;
|
|
57
|
+
relations.push(membersBlock(summaries, each));
|
|
58
|
+
seedReceipt.push(`${summaries.length} members`);
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
61
|
else {
|
|
37
62
|
const result = await traverse(context, [seed], relation, depth, filters);
|
|
38
63
|
answers += result.rows.length;
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
hasZeroRelation ||= result.rows.length === 0 && applies(relation, seed.kind);
|
|
65
|
+
relations.push(traversalBlock(relation, result, each, applies(relation, seed.kind)));
|
|
66
|
+
seedReceipt.push(`${result.rows.length} ${relation}${result.excluded ? `, ${result.excluded} excluded` : ''}${result.unresolved ? `, ${result.unresolved} unresolved` : ''}${result.dispatch ? `, ${result.dispatch} dispatch` : ''}`);
|
|
41
67
|
}
|
|
42
68
|
}
|
|
69
|
+
if (isContainer)
|
|
70
|
+
seedReceipt.push(memberGuidance(seed, summaries));
|
|
71
|
+
receiptParts.push(`${seed.name}: ${seedReceipt.join(', ') || 'no indexed members'}`);
|
|
43
72
|
blocks.push({ element: 'inspect', attributes: { symbol: seed.address, name: seed.name, kind: normalKind(seed.kind), depth }, text: header, children: relations });
|
|
44
73
|
}
|
|
45
|
-
const capped = selected.
|
|
46
|
-
const
|
|
47
|
-
|
|
74
|
+
const capped = selected.length > 0 && each > 0;
|
|
75
|
+
const only = selected.length === relationOrder.length ? '' : ` ${selected.map((relation) => `--only ${relation}`).join(' ')}`;
|
|
76
|
+
const rerun = capped ? `; re-run tsym inspect ${invocation.input.args.join(' ')}${only} --each 0 for all rows` : '';
|
|
77
|
+
const scope = hasZeroRelation ? `; ${zeroScopeReceipt}` : '';
|
|
78
|
+
return { data: blocks, count: answers, receipt: `${receiptParts.join('; ')}, ${Math.round(performance.now() - started)} ms${rerun}${scope}` };
|
|
48
79
|
};
|
|
49
80
|
async function graphAnswer(context, seeds, selected, depth, filters, started) {
|
|
50
81
|
const relations = [];
|
|
@@ -56,25 +87,38 @@ async function graphAnswer(context, seeds, selected, depth, filters, started) {
|
|
|
56
87
|
const unresolved = new Set(graph.unresolved.map((site) => site.siteId)).size;
|
|
57
88
|
const dispatch = new Set([...graph.unresolved.filter((site) => site.dispatch).map((site) => site.siteId), ...graph.edges.filter((edge) => edge.kind === 'impl').flatMap((edge) => edge.siteIds)]).size;
|
|
58
89
|
const excluded = relations.reduce((sum, result) => sum + result.excluded, 0);
|
|
59
|
-
|
|
90
|
+
const scope = relations.some((result) => result.rows.length === 0) ? `; ${zeroScopeReceipt}` : '';
|
|
91
|
+
return { data: { element: 'inspect', attributes: { graph: true, count }, text: renderGraph(graph).replace(/\n$/, '') }, count, receipt: `${count} traversal rows${excluded ? `, ${excluded} excluded` : ''}${unresolved ? `, ${unresolved} unresolved` : ''}${dispatch ? `, ${dispatch} dispatch` : ''}, ${Math.round(performance.now() - started)} ms${scope}` };
|
|
60
92
|
}
|
|
61
93
|
async function signature(_context, record) { return `${normalKind(record.kind)} ${record.qualifiedName}`; }
|
|
62
|
-
function relationBlock(element, rows, path, each, format) {
|
|
94
|
+
function relationBlock(element, rows, path, each, format, scoped = false) {
|
|
63
95
|
if (!rows.length)
|
|
64
|
-
return { element, attributes: { count: 0 } };
|
|
96
|
+
return { element, attributes: { count: 0, scope: scoped ? 'compiler-resolved' : undefined } };
|
|
65
97
|
const shown = shownRows(rows, each);
|
|
66
98
|
return { element, attributes: { count: rows.length, files: new Set(rows.map(path)).size, shown: shown.length < rows.length ? shown.length : undefined }, text: shown.map(format).join('\n') };
|
|
67
99
|
}
|
|
68
|
-
function refBlock(rows, each) {
|
|
69
|
-
return relationBlock('refs', rows, (row) => row.path, each, (row) => `${row.path}:${row.line}:${row.col} ${row.kind} ${row.qualifiedName} ${row.role}
|
|
100
|
+
function refBlock(rows, each, scoped) {
|
|
101
|
+
return relationBlock('refs', rows, (row) => row.path, each, (row) => `${row.path}:${row.line}:${row.col} ${row.kind} ${row.qualifiedName} ${row.role}`, scoped);
|
|
102
|
+
}
|
|
103
|
+
function implBlock(rows, each, scoped) {
|
|
104
|
+
return relationBlock('impl', rows, (row) => row.location.path, each, (row) => `${row.address} ${normalKind(row.kind)} ${row.qualifiedName}`, scoped);
|
|
70
105
|
}
|
|
71
|
-
function
|
|
72
|
-
return relationBlock(
|
|
106
|
+
function traversalBlock(relation, result, each, scoped) {
|
|
107
|
+
return relationBlock(relation, result.rows, (row) => row.path, each, formatTraversal, scoped);
|
|
73
108
|
}
|
|
74
|
-
function
|
|
75
|
-
return relationBlock(
|
|
109
|
+
function membersBlock(rows, each) {
|
|
110
|
+
return relationBlock('members', rows, (row) => row.path, each, formatMember);
|
|
76
111
|
}
|
|
77
112
|
function formatTraversal(row) { return `${row.id} ${row.depth} ${normalKind(row.kind)} ${row.qualifiedName} (${row.callSites} ${row.callSites === 1 ? 'site' : 'sites'}${row.edgeKinds.includes('impl') ? ', impl' : ''})`; }
|
|
113
|
+
function formatMember(row) {
|
|
114
|
+
const incoming = callableMemberKinds.has(row.kind) ? `${row.callers} callers` : `${row.refs} refs`;
|
|
115
|
+
return `${row.address} ${row.kind} ${row.qualifiedName} (${incoming}, ${row.calls} calls)`;
|
|
116
|
+
}
|
|
117
|
+
function memberGuidance(seed, members) {
|
|
118
|
+
const target = [...members].sort((left, right) => right.callers - left.callers || left.line - right.line || left.col - right.col)[0];
|
|
119
|
+
const callerScope = seed.kind === 'class' ? 'callers are constructor/type uses' : 'container callers do not include member calls';
|
|
120
|
+
return `${callerScope}; methods have their own — inspect ${target.qualifiedName}`;
|
|
121
|
+
}
|
|
78
122
|
function shownRows(rows, each) { return each === 0 ? rows : rows.slice(0, each); }
|
|
79
123
|
function selectedRelations(value) { const values = Array.isArray(value) ? value : relationOrder; return relationOrder.filter((relation) => values.includes(relation)); }
|
|
80
124
|
function strings(value) { return Array.isArray(value) ? value : value === undefined ? [] : [String(value)]; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { DeclarationError } from '../../core/declarations.js';
|
|
4
|
+
import { membersByContainer } from '../../core/members.js';
|
|
4
5
|
import { matchesPathFilters } from '../../core/receipt.js';
|
|
5
6
|
import { displayPath, scopeFiles } from '../../read/read.js';
|
|
6
7
|
import { query } from '../../store/query.js';
|
|
@@ -12,8 +13,9 @@ const handler = async (context, invocation) => {
|
|
|
12
13
|
const depth = flags['--depth'];
|
|
13
14
|
const scope = await scopeFiles(context, invocation.cwd, invocation.input.args);
|
|
14
15
|
const selected = new Set(scope.files.map((file) => displayPath(context.root, file)).filter((file) => matchesPathFilters(file, { in: flags['--in'], exclude: flags['--exclude'] })));
|
|
15
|
-
const
|
|
16
|
-
const
|
|
16
|
+
const selectedFiles = [...selected];
|
|
17
|
+
const declarations = indexed(context, selectedFiles).filter((declaration) => declaration.kind !== 'module');
|
|
18
|
+
const imports = flags['--imports'] === true && flags['--exported'] !== true && !flags['--kind'] ? indexedImports(context, selectedFiles) : [];
|
|
17
19
|
const directory = invocation.input.args.length === 0 ? context.root : invocation.input.args.map((input) => path.resolve(invocation.cwd, input)).find((candidate) => {
|
|
18
20
|
try {
|
|
19
21
|
return statSync(candidate).isDirectory();
|
|
@@ -23,6 +25,7 @@ const handler = async (context, invocation) => {
|
|
|
23
25
|
}
|
|
24
26
|
});
|
|
25
27
|
const requestedDirectory = directory !== undefined;
|
|
28
|
+
const outlinedFile = directFile(invocation);
|
|
26
29
|
const files = new Map();
|
|
27
30
|
for (const declaration of declarations)
|
|
28
31
|
files.set(declaration.path, [...(files.get(declaration.path) ?? []), declaration]);
|
|
@@ -33,33 +36,41 @@ const handler = async (context, invocation) => {
|
|
|
33
36
|
}
|
|
34
37
|
const rows = requestedDirectory && depth === 1
|
|
35
38
|
? containerRows(context.root, files, importsByFile, flags, directory ?? context.root)
|
|
36
|
-
: [...files.entries()].flatMap(([file, items]) => [...select(items, flags, requestedDirectory ? depth - 1 : depth).map((symbol) => symbolRow(symbol, items, flags)), ...(importsByFile.get(file) ?? []).map(importRow)].sort(byPosition));
|
|
39
|
+
: [...files.entries()].flatMap(([file, items]) => [...select(items, flags, requestedDirectory ? depth - 1 : depth).map((symbol) => symbolRow(context.root, symbol, items, flags)), ...(importsByFile.get(file) ?? []).map(importRow)].sort(byPosition));
|
|
37
40
|
const limit = flags['--limit'];
|
|
38
41
|
const shown = limit === undefined ? rows : rows.slice(0, limit);
|
|
39
42
|
const capped = shown.length < rows.length;
|
|
40
43
|
const symbols = [...files.values()].flatMap((items) => select(items, flags, requestedDirectory ? depth - 1 : depth)).length + imports.length;
|
|
41
44
|
const receipt = `${capped ? `${shown.length} of ${rows.length} outline rows shown, limit ${limit}` : `${shown.length} outline row${shown.length === 1 ? '' : 's'}`}${scope.notInProgram > 0 ? `, ${scope.notInProgram} not in program` : ''}, ${Math.round(performance.now() - started)} ms`;
|
|
42
|
-
return { data: { element: 'outline', attributes: { path: invocation.input.args.join(' ') || '.', depth, symbols, files: files.size, count: rows.length, shown: capped ? shown.length : undefined }, text: shown.map((row) => row.text).join('\n') }, count: rows.length, receipt };
|
|
45
|
+
return { data: { element: 'outline', attributes: { path: invocation.input.args.join(' ') || '.', depth, symbols, files: files.size, count: rows.length, shown: capped ? shown.length : undefined }, text: shown.map((row) => row.text).join('\n') }, count: rows.length, displayedFiles: outlinedFile && shown.length > 0 ? [outlinedFile] : [...new Set(shown.flatMap((row) => row.file ? [row.file] : []))], receipt };
|
|
43
46
|
};
|
|
44
|
-
function
|
|
47
|
+
function directFile(invocation) {
|
|
48
|
+
if (invocation.input.args.length !== 1)
|
|
49
|
+
return undefined;
|
|
50
|
+
const candidate = path.resolve(invocation.cwd, invocation.input.args[0]);
|
|
51
|
+
try {
|
|
52
|
+
return statSync(candidate).isFile() ? candidate : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function indexed(context, files) {
|
|
45
59
|
if (!context.store)
|
|
46
60
|
throw new DeclarationError('not-in-program', 'The TypeScript index is not available yet.');
|
|
47
|
-
return query(context.store, 'MATCH (d:Declaration) RETURN d.key AS key, d.at AS at, d.name AS name, d.qname AS qname, d.kind AS kind, d.exported AS exported, d.file AS file, d.line AS line, d.col AS col, d.lines AS lines, d.container AS container').map(declaration);
|
|
61
|
+
return files.length === 0 ? [] : query(context.store, 'MATCH (d:Declaration) WHERE d.file IN $files RETURN d.key AS key, d.at AS at, d.name AS name, d.qname AS qname, d.kind AS kind, d.exported AS exported, d.file AS file, d.line AS line, d.col AS col, d.lines AS lines, d.container AS container', { files: [...files] }).map(declaration);
|
|
48
62
|
}
|
|
49
63
|
function declaration(row) {
|
|
50
64
|
return { key: String(row.key), address: String(row.at), path: String(row.file), name: String(row.name), qualifiedName: String(row.qname), kind: String(row.kind), exported: Boolean(row.exported), line: Number(row.line), col: Number(row.col), lines: Number(row.lines), container: row.container === null ? null : String(row.container) };
|
|
51
65
|
}
|
|
52
|
-
function indexedImports(context) {
|
|
66
|
+
function indexedImports(context, files) {
|
|
53
67
|
if (!context.store)
|
|
54
68
|
throw new DeclarationError('not-in-program', 'The TypeScript index is not available yet.');
|
|
55
|
-
return query(context.store, 'MATCH (m:Declaration)-[e:IMPORTS]->(d:Declaration) RETURN m.file AS file, e.line AS line, e.names AS names, d.file AS target UNION MATCH (m:Declaration)-[e:IMPORTS]->(external:External) RETURN m.file AS file, e.line AS line, e.names AS names, external.module AS target')
|
|
69
|
+
return files.length === 0 ? [] : query(context.store, 'MATCH (m:Declaration)-[e:IMPORTS]->(d:Declaration) WHERE m.file IN $files RETURN m.file AS file, e.line AS line, e.names AS names, d.file AS target UNION MATCH (m:Declaration)-[e:IMPORTS]->(external:External) WHERE m.file IN $files RETURN m.file AS file, e.line AS line, e.names AS names, external.module AS target', { files: [...files] })
|
|
56
70
|
.map((row) => ({ path: String(row.file), line: Number(row.line), address: `${String(row.file)}:${Number(row.line)}:1`, name: String(row.names), target: String(row.target) }));
|
|
57
71
|
}
|
|
58
72
|
function select(items, flags, depth, members = flags['--members'] === true) {
|
|
59
|
-
const byContainer =
|
|
60
|
-
for (const item of items)
|
|
61
|
-
if (item.container)
|
|
62
|
-
byContainer.set(item.container, [...(byContainer.get(item.container) ?? []), item]);
|
|
73
|
+
const byContainer = membersByContainer(items);
|
|
63
74
|
const roots = items.filter((item) => item.container === `${item.path}#<module>`);
|
|
64
75
|
const out = [];
|
|
65
76
|
const visit = (item, remaining) => {
|
|
@@ -77,10 +88,10 @@ function keep(item, flags) {
|
|
|
77
88
|
const kinds = flags['--kind'];
|
|
78
89
|
return (!kinds || kinds.includes(item.kind)) && (flags['--exported'] !== true || item.exported);
|
|
79
90
|
}
|
|
80
|
-
function symbolRow(symbol, items, flags) {
|
|
91
|
+
function symbolRow(root, symbol, items, flags) {
|
|
81
92
|
const members = items.filter((item) => item.container === symbol.key && keep(item, flags)).length;
|
|
82
93
|
const memberCount = containers.has(symbol.kind) ? `, ${members} members` : '';
|
|
83
|
-
return { text: `${symbol.address} ${symbol.kind}${symbol.exported ? ' export' : ''} ${symbol.qualifiedName} (${symbol.lines}L${memberCount})`, line: symbol.line, col: symbol.col };
|
|
94
|
+
return { text: `${symbol.address} ${symbol.kind}${symbol.exported ? ' export' : ''} ${symbol.qualifiedName} (${symbol.lines}L${memberCount})`, line: symbol.line, col: symbol.col, file: path.join(root, symbol.path) };
|
|
84
95
|
}
|
|
85
96
|
function importRow(item) { return { text: `${item.address} import ${item.name} ${item.target}`, line: item.line, col: 1 }; }
|
|
86
97
|
function containerRows(root, files, imports, flags, directory) {
|
|
@@ -8,17 +8,18 @@ const handler = async (context, invocation) => {
|
|
|
8
8
|
for (const declaration of resolved) {
|
|
9
9
|
const source = context.source.read(declaration.file);
|
|
10
10
|
const span = storedSpan(context, declaration);
|
|
11
|
-
const sites = declaration.location.line === 0 ? [{ source, span }] : await declarationSites(context, declaration, span);
|
|
11
|
+
const sites = declaration.location.line === 0 ? [{ file: declaration.file, source, span }] : await declarationSites(context, declaration, span);
|
|
12
12
|
const text = declaration.location.line === 0 ? source : sites.map((site) => declarationText(site.source, site.span)).join('\n');
|
|
13
13
|
const lines = declaration.location.line === 0 ? sourceLines(source) : sites.reduce((total, site) => total + site.span.lines, 0);
|
|
14
|
-
rendered.push({ element: 'symbol', attributes: { at: declaration.address, name: declaration.name, kind: declaration.kind, lines }, text });
|
|
14
|
+
rendered.push({ node: { element: 'symbol', attributes: { at: declaration.address, name: declaration.name, kind: declaration.kind, lines }, text }, files: sites.map((site) => site.file) });
|
|
15
15
|
}
|
|
16
16
|
const limit = invocation.input.flags['--limit'];
|
|
17
17
|
const shown = limit === undefined ? rendered : rendered.slice(0, limit);
|
|
18
18
|
const capped = shown.length < rendered.length;
|
|
19
19
|
return {
|
|
20
|
-
data: { element: 'show', attributes: { count: rendered.length, shown: capped ? shown.length : undefined }, children: shown },
|
|
20
|
+
data: { element: 'show', attributes: { count: rendered.length, shown: capped ? shown.length : undefined }, children: shown.map((item) => item.node) },
|
|
21
21
|
count: rendered.length,
|
|
22
|
+
displayedFiles: [...new Set(shown.flatMap((item) => item.files))],
|
|
22
23
|
receipt: capped ? `${shown.length} of ${rendered.length} show rows shown, limit ${limit}, ${Math.round(performance.now() - started)} ms` : `${shown.length} show declaration${shown.length === 1 ? '' : 's'}, ${Math.round(performance.now() - started)} ms`,
|
|
23
24
|
};
|
|
24
25
|
};
|
|
@@ -32,7 +33,7 @@ function storedSpan(context, declaration) {
|
|
|
32
33
|
}
|
|
33
34
|
async function declarationSites(context, declaration, stored) {
|
|
34
35
|
if (stored.declCount === 1)
|
|
35
|
-
return [{ source: context.source.read(declaration.file), span: stored }];
|
|
36
|
+
return [{ file: declaration.file, source: context.source.read(declaration.file), span: stored }];
|
|
36
37
|
const membership = await context.projects.membership(declaration.file);
|
|
37
38
|
if (!membership)
|
|
38
39
|
throw new DeclarationError('not-in-program', `${declaration.location.path} exists but is not part of a TypeScript project under ${context.root}`);
|
|
@@ -48,7 +49,7 @@ async function declarationSites(context, declaration, stored) {
|
|
|
48
49
|
const source = node.getSourceFile();
|
|
49
50
|
const start = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
50
51
|
const end = source.getLineAndCharacterOfPosition(node.getEnd());
|
|
51
|
-
return [{ source: context.source.read(source.fileName), span: { endLine: end.line + 1, lines: end.line - start.line + 1, declCount: stored.declCount } }];
|
|
52
|
+
return [{ file: source.fileName, source: context.source.read(source.fileName), span: { endLine: end.line + 1, lines: end.line - start.line + 1, declCount: stored.declCount } }];
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
if (sites.length === 0)
|
|
@@ -7,6 +7,7 @@ const handler = async (context, invocation) => {
|
|
|
7
7
|
const declarations = await context.resolver.resolveAll(invocation.input.args);
|
|
8
8
|
const includeDefinition = invocation.input.flags['--def'] === true;
|
|
9
9
|
const symbols = [];
|
|
10
|
+
const displayedFiles = [];
|
|
10
11
|
let answers = 0;
|
|
11
12
|
for (const declaration of declarations) {
|
|
12
13
|
const project = await context.projects.membership(declaration.file);
|
|
@@ -16,8 +17,9 @@ const handler = async (context, invocation) => {
|
|
|
16
17
|
if (answer !== null)
|
|
17
18
|
answers++;
|
|
18
19
|
symbols.push(renderSymbol(declaration, answer, includeDefinition));
|
|
20
|
+
displayedFiles.push(declaration.file);
|
|
19
21
|
}
|
|
20
|
-
return { data: { element: 'type', children: symbols }, count: answers };
|
|
22
|
+
return { data: { element: 'type', children: symbols }, count: answers, displayedFiles: [...new Set(displayedFiles)] };
|
|
21
23
|
}
|
|
22
24
|
catch (error) {
|
|
23
25
|
if (error instanceof DeclarationError || error instanceof DeclarationResolutionErrors)
|
package/dist/commands/help.js
CHANGED
|
@@ -16,7 +16,7 @@ export function renderHelp(node, serverState = '<server state="stopped"/>') {
|
|
|
16
16
|
lines.push(`<effects>${node.effects}</effects>`);
|
|
17
17
|
if (node === commandTree) {
|
|
18
18
|
lines.push('<globals>\n <flag name="--root" type="path">Workspace root, overriding discovery from the current directory.</flag>\n <flag name="--json" type="bool">Raw JSON on stdout for programmatic consumers.</flag>\n</globals>');
|
|
19
|
-
lines.push('<io>Flags and positional arguments on input. Stdout is one named XML block per call. One receipt goes to stderr on every call. Exit 0 with results, 1 with none, 2 when the invocation was wrong, 3 when the tool could not answer.</io>');
|
|
19
|
+
lines.push('<io>Flags and positional arguments on input. Stdout is one named XML block per call. Prose output of show, type, or outline may be preceded by an <auto-loaded-context> block when run inside a crtr node. One receipt goes to stderr on every call. Exit 0 with results, 1 with none, 2 when the invocation was wrong, 3 when the tool could not answer.</io>');
|
|
20
20
|
}
|
|
21
21
|
return xml('command', { name: node.name, description: node.description }, lines.join('\n'));
|
|
22
22
|
}
|
package/dist/commands/tree.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
const kinds = ['function', 'class', 'interface', 'type', 'enum', 'const', 'let', 'var', 'method', 'property', 'getter', 'setter', 'constructor', 'module'];
|
|
3
|
-
const relations = ['refs', 'callers', 'calls', 'impl'];
|
|
3
|
+
const relations = ['refs', 'callers', 'calls', 'impl', 'members'];
|
|
4
4
|
const inspectUnknownFlags = { '--limit': 'Inspect caps rows per relation with `--each N`.' };
|
|
5
5
|
const filters = [
|
|
6
6
|
{ name: '--in', type: 'glob', repeat: true, description: 'Keep only rows under matching paths.' },
|
|
@@ -103,7 +103,7 @@ export const commandTree = {
|
|
|
103
103
|
leaf('find', 'Locate a declaration by name.', 'Reach for this to disambiguate a name when several declarations share it or you need its container; when the name is unique, pass it straight to show, inspect, or type.', 'find', { name: 'name', repeat: '1', description: 'Declared name. Container.name narrows to a member.' }, [{ name: '--prefix', type: 'bool', description: 'Match names beginning with the input.' }, { name: '--kind', type: 'string', repeat: true, values: kinds, description: 'Keep only these kinds.' }, ...filters, { name: '--limit', type: 'int', minimum: 0, description: 'Cap rows.' }], 'One <find> element. Rows are `path:line:col kind [export] qualifiedName`, ordered by path then position. Alias rows and member rows are included. Count is the true total and shown appears only when capped.'),
|
|
104
104
|
leaf('show', 'The declaration\'s source text.', 'Reach for this to read one declaration in full without reading its file.', 'show', { name: 'symbol', repeat: '1+', description: 'path:line:col, or a name. Address resolution follows the root command contract.' }, [{ name: '--limit', type: 'int', minimum: 0, description: 'Cap how many declarations are printed.' }], 'One <show> element containing one <symbol> per argument, in argument order. Each symbol has at, name, kind, and lines attributes; its body is the whole source lines of the declaration\'s span, and a line shared with another declaration is printed once, in full. Count is the true total and shown appears only when capped.'),
|
|
105
105
|
leaf('type', 'Resolved type, signature, and doc comment.', 'Reach for this when source text does not answer the compiler-resolved type.', 'type', { name: 'symbol', repeat: '1+', description: 'path:line:col, or a name. Address resolution follows the root command contract.' }, [{ name: '--def', type: 'bool', description: 'Also resolve the declaration of the type.' }], 'One <type> element containing one <symbol> per argument. Each symbol has at, name, kind, modifiers, and optional typeAt attributes; its body is the resolved signature, followed by optional <doc> and <tag name="…"> sections.', 'Opens and closes a document in the server. No persistent change.'),
|
|
106
|
-
{ ...leaf('inspect', 'How a symbol relates to the rest of the code.', 'Reach for this for references, callers, calls, implementations, or where a component is rendered; JSX uses a component as a call, so `inspect Component` answers which components render it and which it renders.', 'inspect', { name: 'symbol', repeat: '1+', description: 'path:line:col, or a name. Address resolution follows the root command contract. Each symbol gets its own result block.' }, [{ name: '--only', type: 'string', repeat: true, values: relations, description: `Run only these relations: ${relations.join(', ')}. Default: all
|
|
106
|
+
{ ...leaf('inspect', 'How a symbol relates to the rest of the code.', 'Reach for this for references, callers, calls, implementations, members, or where a component is rendered; JSX uses a component as a call, so `inspect Component` answers which components render it and which it renders.', 'inspect', { name: 'symbol', repeat: '1+', description: 'path:line:col, or a name. Address resolution follows the root command contract. Each symbol gets its own result block.' }, [{ name: '--only', type: 'string', repeat: true, values: relations, description: `Run only these relations: ${relations.join(', ')}. Default: all five.` }, { name: '--each', type: 'int', default: 10, minimum: 0, description: 'Rows per relation. Use 0 for every row. --limit is rejected; use --each instead. Rejected with --graph because graphs are never capped.' }, { name: '--depth', type: 'int', default: 1, minimum: 1, description: 'Levels to follow for callers and calls, including implementation edges. Rejected unless callers or calls is selected.' }, { name: '--graph', type: 'bool', description: 'Requires callers or calls. Render one Mermaid flowchart for all seeds instead of row mode; it prints no refs, impl, or members blocks. Rejected with --each; graphs are never capped.' }, { name: '--in', type: 'glob', repeat: true, description: 'Keep only rows under matching paths. In a graph, projects rows while retaining connecting nodes.' }, { name: '--exclude', type: 'glob', repeat: true, description: 'Drop matching paths. In traversals, also prune nodes reachable only through them. Wins over --in.' }], 'One <inspect> element per symbol, opening with the resolved signature. By default it contains <refs>, <callers>, <calls>, <impl>, and, for a container with indexed members, <members> blocks; --only emits only selected blocks. Empty refs, callers, calls, and impl blocks render count="0" and scope="compiler-resolved". Refs rows are `path:line:col kind qualifiedName role`; callers and calls rows are `path:line:col depth kind qualifiedName (N sites[, impl])`; impl rows are `path:line:col kind qualifiedName`; members rows are `path:line:col kind qualifiedName (N callers|refs, N calls)`, in outline order. Class callers are constructor or type uses; inspect the member summary to follow method calls. The receipt names excluded, unresolved, and dispatch counts when nonzero, and explains once that zero counts cover only indexed compiler-resolved relations, not framework, HTTP, or dependency-injection invocation. Each block count is the true total, files is distinct result paths, and shown appears only when capped by --each. --graph instead returns one Mermaid flowchart; each node label includes a path:line:col address.', 'None.'), constraints: [selectedTraversal], unknownFlagNext: inspectUnknownFlags },
|
|
107
107
|
{ ...leaf('query', 'Set-shaped questions over the whole index, in Cypher.', 'Reach for this when the answer is a set over the whole index rather than one symbol\'s neighbourhood.', 'query', { name: 'cypher', repeat: '1', description: 'The query. `-` reads it from stdin.' }, [{ name: '--param', type: 'string', repeat: true, description: 'NAME=VALUE, binding $NAME. Integers, floats, true and false are parsed; everything else is a string.' }, { name: '--since', type: 'string', description: 'A git ref. Resolves REF..HEAD, ingests any commits the index lacks, and binds the hash list as $commits.' }, { name: '--rows', type: 'int', default: 100, minimum: 0, description: 'Rows printed to stdout. 0 prints every row and creates no implicit file; --out still writes.' }, { name: '--out', type: 'path', description: 'Always write the complete result here, even below --rows or with --rows 0. Printing still honours --rows.' }], 'One <query> element with rows, shown, indexedAt, state and ms attributes. A record returning file/line/col or an at column leads with that address; remaining columns follow in RETURN order, two spaces apart. With no ORDER BY, addressed rows sort by path, line, column and the receipt says unordered for records without addresses. Over --rows, a <truncated> element names the true total and the file holding every row.', '--since may write Commit and CHANGED rows. --out always writes a complete result file; truncation writes one implicitly without --out. Source files are not changed.'), model: queryHelp() },
|
|
108
108
|
leaf('search', 'Structural syntax search over the source.', 'Reach for this when the pattern is syntax shape rather than a named symbol.', 'search', { name: 'pattern', repeat: '1', description: 'An ast-grep pattern that parses as one node. $NAME captures a node; $$$NAME captures a list. Each file is parsed as its own language: ts, tsx, js, or jsx by extension.' }, [...filters, { name: '--limit', type: 'int', minimum: 0, description: 'Cap rows.' }], 'One <search> element. Rows are `path:line:col matched text, first line`, ordered by path then position. Count is the true total and shown appears only when capped.'),
|
|
109
109
|
leaf('check', 'Semantic and syntactic diagnostics.', 'Reach for this after editing to check changed files or the complete program.', 'check', { name: 'path', repeat: '0+', description: 'Files or globs, relative to the current directory. Default: the whole program. Paths are checked synchronously; without paths, the bundled TypeScript 7 checker examines the whole program in about 1–3 seconds on crouter.' }, [{ name: '--errors-only', type: 'bool', description: 'Drop warnings and suggestions.' }, ...filters, { name: '--limit', type: 'int', minimum: 0, description: 'Cap diagnostics.' }], 'One <check> element with scope, errors, warnings, suggestions, and ms attributes, containing diagnostics ordered by path then position. A diagnostic is `<diagnostic at="path:line:col" severity="error|warning|suggestion" code="N">message</diagnostic>`. Count is the true total and shown appears only when capped. A clean check has no diagnostics and exits 1.', 'Opens and closes documents. No persistent change.'),
|
package/dist/commands/types.d.ts
CHANGED
|
@@ -76,5 +76,11 @@ export interface Answer {
|
|
|
76
76
|
data: OutputNode | OutputNode[];
|
|
77
77
|
count: number;
|
|
78
78
|
receipt?: string;
|
|
79
|
+
/** Files whose declarations this result displayed; carried over the resident wire but never rendered. */
|
|
80
|
+
displayedFiles?: string[];
|
|
81
|
+
/** Read-routed memory returned by the client-side crouter call; never included in tsym JSON. */
|
|
82
|
+
context?: string;
|
|
83
|
+
/** Structured context-delivery failure written to stderr without changing the command result. */
|
|
84
|
+
contextError?: string;
|
|
79
85
|
}
|
|
80
86
|
export type Completion = Answer | InvocationError | ToolError;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Completion, Invocation } from './commands/types.js';
|
|
2
|
+
export interface ExposureResult {
|
|
3
|
+
exitCode: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export type ExposureRunner = (files: readonly string[], cwd: string) => Promise<ExposureResult>;
|
|
9
|
+
export declare function attachReadContext(invocation: Invocation, completion: Completion, run?: ExposureRunner, nodeId?: string | undefined): Promise<Completion>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function attachReadContext(invocation, completion, run = runContextExpose, nodeId = process.env.CRTR_NODE_ID) {
|
|
4
|
+
if (!nodeId?.trim() || invocation.json || !isReadLeaf(invocation) || !isAnswer(completion) || completion.count === 0)
|
|
5
|
+
return completion;
|
|
6
|
+
const files = [...new Set((completion.displayedFiles ?? []).map((file) => path.resolve(file)))];
|
|
7
|
+
if (files.length === 0)
|
|
8
|
+
return completion;
|
|
9
|
+
try {
|
|
10
|
+
const result = await run(files, invocation.cwd);
|
|
11
|
+
if (result.exitCode !== 0)
|
|
12
|
+
return withContextError(completion, deliveryError(result, 'context-expose-failed'));
|
|
13
|
+
const response = jsonObject(result.stdout);
|
|
14
|
+
if (!response)
|
|
15
|
+
return withContextError(completion, structuredError('context-expose-invalid-response', result.stderr || 'crtr sys context expose returned invalid JSON.'));
|
|
16
|
+
if (response.content === undefined)
|
|
17
|
+
return completion;
|
|
18
|
+
if (typeof response.content !== 'string')
|
|
19
|
+
return withContextError(completion, structuredError('context-expose-invalid-response', 'crtr sys context expose returned a non-string content field.'));
|
|
20
|
+
return { ...completion, context: response.content };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return withContextError(completion, structuredError('context-expose-failed', error instanceof Error ? error.message : String(error)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isReadLeaf(invocation) { return invocation.declaration.handler === 'show' || invocation.declaration.handler === 'type' || invocation.declaration.handler === 'outline'; }
|
|
27
|
+
function isAnswer(completion) { return !('category' in completion); }
|
|
28
|
+
function withContextError(completion, contextError) { return { ...completion, contextError }; }
|
|
29
|
+
function jsonObject(text) {
|
|
30
|
+
try {
|
|
31
|
+
const value = JSON.parse(text);
|
|
32
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function deliveryError(result, code) {
|
|
39
|
+
const response = jsonObject(result.stdout);
|
|
40
|
+
if (response)
|
|
41
|
+
return JSON.stringify(response);
|
|
42
|
+
return structuredError(code, result.stderr || result.error || 'crtr sys context expose failed.');
|
|
43
|
+
}
|
|
44
|
+
function structuredError(error, message) { return JSON.stringify({ error, message }); }
|
|
45
|
+
async function runContextExpose(files, cwd) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const child = spawn('crtr', ['sys', 'context', 'expose', '--json', ...files.flatMap((file) => ['--file', file])], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
48
|
+
let stdout = '';
|
|
49
|
+
let stderr = '';
|
|
50
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
51
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
52
|
+
child.once('error', (error) => resolve({ exitCode: null, stdout, stderr, error: error.message }));
|
|
53
|
+
child.once('close', (exitCode) => resolve({ exitCode, stdout, stderr }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ContainerMember {
|
|
2
|
+
key: string;
|
|
3
|
+
container: string | null;
|
|
4
|
+
line: number;
|
|
5
|
+
col: number;
|
|
6
|
+
}
|
|
7
|
+
/** Direct members use the source-position order printed by outline. */
|
|
8
|
+
export declare function directMembers<T extends ContainerMember>(items: readonly T[], container: string): T[];
|
|
9
|
+
export declare function membersByContainer<T extends ContainerMember>(items: readonly T[]): ReadonlyMap<string, T[]>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Direct members use the source-position order printed by outline. */
|
|
2
|
+
export function directMembers(items, container) {
|
|
3
|
+
return items.filter((item) => item.container === container).sort(bySourcePosition);
|
|
4
|
+
}
|
|
5
|
+
export function membersByContainer(items) {
|
|
6
|
+
const groups = new Map();
|
|
7
|
+
for (const item of items)
|
|
8
|
+
if (item.container)
|
|
9
|
+
groups.set(item.container, [...(groups.get(item.container) ?? []), item]);
|
|
10
|
+
return new Map([...groups].map(([container, members]) => [container, members.sort(bySourcePosition)]));
|
|
11
|
+
}
|
|
12
|
+
function bySourcePosition(a, b) { return a.line - b.line || a.col - b.col; }
|
package/dist/index/build.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LoadReceipt, type StoreGraph } from '../store/load.js';
|
|
2
|
+
import { type StoreFormatIdentity } from '../store/meta.js';
|
|
2
3
|
import { Store } from '../store/store.js';
|
|
3
4
|
import { Ts7Host } from '../ts7/api.js';
|
|
4
5
|
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
@@ -16,7 +17,7 @@ export interface BuiltIndex {
|
|
|
16
17
|
receipt: BuildReceipt;
|
|
17
18
|
}
|
|
18
19
|
/** Walks the configured project union, adds heritage facts, and atomically replaces the persisted graph. */
|
|
19
|
-
export declare function buildIndex(root: string, program: Ts7Host, projects: ConfiguredProjects, store: Store): BuiltIndex;
|
|
20
|
+
export declare function buildIndex(root: string, program: Ts7Host, projects: ConfiguredProjects, store: Store, identity?: StoreFormatIdentity): BuiltIndex;
|
|
20
21
|
/** Rehydrates an unchanged persisted graph without re-walking the TypeScript program. */
|
|
21
22
|
export declare function reuseIndex(store: Store): BuiltIndex;
|
|
22
23
|
export declare function storeGraph(index: HeritageGraph): StoreGraph;
|
package/dist/index/build.js
CHANGED
|
@@ -4,13 +4,13 @@ import { metadataFor, readMetadata, writeMetadata } from '../store/meta.js';
|
|
|
4
4
|
import { addHeritage } from './heritage.js';
|
|
5
5
|
import { walkIndex } from './walk.js';
|
|
6
6
|
/** Walks the configured project union, adds heritage facts, and atomically replaces the persisted graph. */
|
|
7
|
-
export function buildIndex(root, program, projects, store) {
|
|
7
|
+
export function buildIndex(root, program, projects, store, identity) {
|
|
8
8
|
const started = performance.now();
|
|
9
9
|
const walked = walkIndex(root, program, projects);
|
|
10
10
|
const enriched = addHeritage(root, program, projects, walked);
|
|
11
11
|
const loaded = loadGraph(store, storeGraph(enriched));
|
|
12
12
|
const durationMs = performance.now() - started;
|
|
13
|
-
writeMetadata(store.path, metadataFor(root, projects, headCommit(root), durationMs));
|
|
13
|
+
writeMetadata(store.path, metadataFor(root, projects, headCommit(root), durationMs, identity));
|
|
14
14
|
return {
|
|
15
15
|
graph: enriched.graph,
|
|
16
16
|
heritage: enriched.relationships,
|
|
@@ -28,7 +28,7 @@ export function reuseIndex(store) {
|
|
|
28
28
|
const persisted = readGraph(store);
|
|
29
29
|
const metadata = readMetadata(store.path);
|
|
30
30
|
if (!metadata)
|
|
31
|
-
throw new Error('reuseIndex requires persisted store metadata
|
|
31
|
+
throw new Error('reuseIndex requires persisted store metadata.');
|
|
32
32
|
const durationMs = metadata.indexDurationMs;
|
|
33
33
|
const declarations = new Map(persisted.declarations.map((declaration) => [declaration.key, declaration]));
|
|
34
34
|
const externals = new Map(persisted.externals.map((external) => [external.key, external]));
|
package/dist/index/refresh.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type StoreGraph } from '../store/load.js';
|
|
2
|
+
import { type StoreFormatIdentity } from '../store/meta.js';
|
|
2
3
|
import { Store } from '../store/store.js';
|
|
3
4
|
import { Ts7Host } from '../ts7/api.js';
|
|
4
5
|
import { ConfiguredProjects } from '../ts7/projects.js';
|
|
@@ -22,7 +23,7 @@ export interface RefreshResult {
|
|
|
22
23
|
validation: 'off' | 'matched';
|
|
23
24
|
}
|
|
24
25
|
/** Updates the TS7 snapshot, conservatively re-walks the union, then writes only changed file records. */
|
|
25
|
-
export declare function refreshIndex(root: string, program: Ts7Host, projects: ConfiguredProjects, store: Store, previous: BuiltIndex, options: RefreshOptions): RefreshResult;
|
|
26
|
+
export declare function refreshIndex(root: string, program: Ts7Host, projects: ConfiguredProjects, store: Store, previous: BuiltIndex, options: RefreshOptions, identity?: StoreFormatIdentity): RefreshResult;
|
|
26
27
|
export declare function graphsEqual(left: HeritageGraph, right: HeritageGraph): boolean;
|
|
27
28
|
export declare function fileFingerprint(graph: BuiltIndex['graph'], heritage: readonly HeritageGraph['relationships'][number][], file: string): string;
|
|
28
29
|
export declare function graphFingerprint(graph: StoreGraph): string;
|
package/dist/index/refresh.js
CHANGED
|
@@ -5,7 +5,7 @@ import { addHeritage } from './heritage.js';
|
|
|
5
5
|
import { storeGraph } from './build.js';
|
|
6
6
|
import { walkIndex } from './walk.js';
|
|
7
7
|
/** Updates the TS7 snapshot, conservatively re-walks the union, then writes only changed file records. */
|
|
8
|
-
export function refreshIndex(root, program, projects, store, previous, options) {
|
|
8
|
+
export function refreshIndex(root, program, projects, store, previous, options, identity) {
|
|
9
9
|
const started = performance.now();
|
|
10
10
|
// The update carries a rewritten config's new text, so it must precede the rediscovery that reads membership from it.
|
|
11
11
|
program.update({ fileChanges: options.fileChanges });
|
|
@@ -24,7 +24,7 @@ export function refreshIndex(root, program, projects, store, previous, options)
|
|
|
24
24
|
stored = 'files';
|
|
25
25
|
}
|
|
26
26
|
const durationMs = performance.now() - started;
|
|
27
|
-
writeMetadata(store.path, metadataFor(root, projects, headCommit(root), durationMs));
|
|
27
|
+
writeMetadata(store.path, metadataFor(root, projects, headCommit(root), durationMs, identity));
|
|
28
28
|
let validation = 'off';
|
|
29
29
|
if (options.validate) {
|
|
30
30
|
const verification = indexGraph(root, program, projects);
|