@crouton-kit/tsym 0.1.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/LICENSE +21 -0
- package/README.md +51 -0
- package/bin/tsym.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +89 -0
- package/dist/commands/handlers/check.d.ts +3 -0
- package/dist/commands/handlers/check.js +94 -0
- package/dist/commands/handlers/find.d.ts +3 -0
- package/dist/commands/handlers/find.js +56 -0
- package/dist/commands/handlers/inspect.d.ts +3 -0
- package/dist/commands/handlers/inspect.js +81 -0
- package/dist/commands/handlers/outline.d.ts +3 -0
- package/dist/commands/handlers/outline.js +101 -0
- package/dist/commands/handlers/query.d.ts +3 -0
- package/dist/commands/handlers/query.js +196 -0
- package/dist/commands/handlers/rename.d.ts +3 -0
- package/dist/commands/handlers/rename.js +53 -0
- package/dist/commands/handlers/search.d.ts +5 -0
- package/dist/commands/handlers/search.js +32 -0
- package/dist/commands/handlers/server-reload.d.ts +3 -0
- package/dist/commands/handlers/server-reload.js +7 -0
- package/dist/commands/handlers/server-status.d.ts +3 -0
- package/dist/commands/handlers/server-status.js +7 -0
- package/dist/commands/handlers/server-stop.d.ts +3 -0
- package/dist/commands/handlers/server-stop.js +7 -0
- package/dist/commands/handlers/show.d.ts +3 -0
- package/dist/commands/handlers/show.js +63 -0
- package/dist/commands/handlers/type.d.ts +3 -0
- package/dist/commands/handlers/type.js +45 -0
- package/dist/commands/handlers.d.ts +12 -0
- package/dist/commands/handlers.js +44 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +27 -0
- package/dist/commands/rename-apply.d.ts +37 -0
- package/dist/commands/rename-apply.js +115 -0
- package/dist/commands/tree.d.ts +9 -0
- package/dist/commands/tree.js +277 -0
- package/dist/commands/types.d.ts +80 -0
- package/dist/commands/types.js +1 -0
- package/dist/core/declarations.d.ts +48 -0
- package/dist/core/declarations.js +153 -0
- package/dist/core/receipt.d.ts +7 -0
- package/dist/core/receipt.js +9 -0
- package/dist/core/search.d.ts +29 -0
- package/dist/core/search.js +47 -0
- package/dist/core/source.d.ts +32 -0
- package/dist/core/source.js +96 -0
- package/dist/core/workspace.d.ts +28 -0
- package/dist/core/workspace.js +137 -0
- package/dist/index/build.d.ts +22 -0
- package/dist/index/build.js +79 -0
- package/dist/index/classify.d.ts +20 -0
- package/dist/index/classify.js +60 -0
- package/dist/index/commits.d.ts +15 -0
- package/dist/index/commits.js +201 -0
- package/dist/index/descriptor.d.ts +19 -0
- package/dist/index/descriptor.js +295 -0
- package/dist/index/graph.d.ts +82 -0
- package/dist/index/graph.js +1 -0
- package/dist/index/heritage.d.ts +15 -0
- package/dist/index/heritage.js +122 -0
- package/dist/index/refresh.d.ts +28 -0
- package/dist/index/refresh.js +110 -0
- package/dist/index/scope.d.ts +4 -0
- package/dist/index/scope.js +21 -0
- package/dist/index/state.d.ts +22 -0
- package/dist/index/state.js +63 -0
- package/dist/index/walk.d.ts +23 -0
- package/dist/index/walk.js +550 -0
- package/dist/index/watch.d.ts +18 -0
- package/dist/index/watch.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.js +54 -0
- package/dist/read/read.d.ts +32 -0
- package/dist/read/read.js +89 -0
- package/dist/relations/graph.d.ts +23 -0
- package/dist/relations/graph.js +115 -0
- package/dist/relations/mermaid.d.ts +3 -0
- package/dist/relations/mermaid.js +37 -0
- package/dist/relations/store-relations.d.ts +20 -0
- package/dist/relations/store-relations.js +158 -0
- package/dist/relations/types.d.ts +58 -0
- package/dist/relations/types.js +12 -0
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +215 -0
- package/dist/server/context.d.ts +21 -0
- package/dist/server/context.js +12 -0
- package/dist/server/lifecycle.d.ts +28 -0
- package/dist/server/lifecycle.js +62 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +541 -0
- package/dist/server/projects.d.ts +21 -0
- package/dist/server/projects.js +28 -0
- package/dist/server/wire.d.ts +39 -0
- package/dist/server/wire.js +19 -0
- package/dist/store/load.d.ts +67 -0
- package/dist/store/load.js +193 -0
- package/dist/store/meta.d.ts +28 -0
- package/dist/store/meta.js +137 -0
- package/dist/store/query.d.ts +5 -0
- package/dist/store/query.js +4 -0
- package/dist/store/schema.d.ts +4 -0
- package/dist/store/schema.js +19 -0
- package/dist/store/store.d.ts +36 -0
- package/dist/store/store.js +240 -0
- package/dist/ts7/api.d.ts +35 -0
- package/dist/ts7/api.js +164 -0
- package/dist/ts7/ast.d.ts +1 -0
- package/dist/ts7/ast.js +1 -0
- package/dist/ts7/checker.d.ts +55 -0
- package/dist/ts7/checker.js +383 -0
- package/dist/ts7/projects.d.ts +27 -0
- package/dist/ts7/projects.js +94 -0
- package/package.json +47 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { chmod, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
const filesystem = { readFile, writeFile, rename, rm, stat, chmod };
|
|
5
|
+
/** Apply a complete compiler-produced plan without asking the resident to recompute it. */
|
|
6
|
+
export async function applyRenamePlan(plan, operations = filesystem) {
|
|
7
|
+
const prepared = [];
|
|
8
|
+
try {
|
|
9
|
+
for (const file of orderedFiles(plan)) {
|
|
10
|
+
const absolute = absolutePath(plan.root, file.path);
|
|
11
|
+
const original = await operations.readFile(absolute);
|
|
12
|
+
if (!original.equals(Buffer.from(file.snapshot, 'base64')))
|
|
13
|
+
return writeFailure(`The source changed after the rename preview: ${file.path}.`, 'Re-run rename to produce a plan for the current source.');
|
|
14
|
+
const replacement = applyEdits(original, file);
|
|
15
|
+
const metadata = await operations.stat(absolute);
|
|
16
|
+
const suffix = `.tsym-rename-${randomUUID()}`;
|
|
17
|
+
prepared.push({ file, original, replacement, mode: metadata.mode & 0o7777, temp: `${absolute}${suffix}.new`, restore: `${absolute}${suffix}.restore`, replaced: false });
|
|
18
|
+
}
|
|
19
|
+
for (const entry of prepared) {
|
|
20
|
+
await operations.writeFile(entry.temp, entry.replacement, { mode: entry.mode, flag: 'wx' });
|
|
21
|
+
await operations.chmod(entry.temp, entry.mode);
|
|
22
|
+
await operations.writeFile(entry.restore, entry.original, { mode: entry.mode, flag: 'wx' });
|
|
23
|
+
await operations.chmod(entry.restore, entry.mode);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
await removePrepared(prepared, operations);
|
|
28
|
+
return writeFailure(`Rename preparation failed: ${message(error)}.`, 'Fix the filesystem error, then re-run rename.');
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
for (const entry of prepared) {
|
|
32
|
+
await operations.rename(entry.temp, absolutePath(plan.root, entry.file.path));
|
|
33
|
+
entry.replaced = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const rollbackFailures = [];
|
|
38
|
+
for (const entry of [...prepared].reverse()) {
|
|
39
|
+
if (!entry.replaced)
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
await operations.rename(entry.restore, absolutePath(plan.root, entry.file.path));
|
|
43
|
+
entry.replaced = false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
rollbackFailures.push(entry.restore);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await removePrepared(prepared, operations, new Set(rollbackFailures));
|
|
50
|
+
if (rollbackFailures.length > 0) {
|
|
51
|
+
return {
|
|
52
|
+
category: 'tool',
|
|
53
|
+
code: 'rename-rollback-failed',
|
|
54
|
+
message: `Rename commit failed (${message(error)}) and restoration also failed. Original backups remain at: ${rollbackFailures.join(', ')}.`,
|
|
55
|
+
received: rollbackFailures.join(', '),
|
|
56
|
+
expected: 'the listed original-content backups to remain available for manual restoration',
|
|
57
|
+
next: 'Restore the listed backups over their source files before retrying rename.',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return writeFailure(`Rename commit failed and every replaced file was restored: ${message(error)}.`, 'Fix the filesystem error, then re-run rename.');
|
|
61
|
+
}
|
|
62
|
+
const cleanupFailures = [];
|
|
63
|
+
for (const entry of prepared) {
|
|
64
|
+
try {
|
|
65
|
+
await operations.rm(entry.restore, { force: false });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
cleanupFailures.push(entry.restore);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (cleanupFailures.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
category: 'tool',
|
|
74
|
+
code: 'rename-write-failed',
|
|
75
|
+
message: `Rename committed, but cleanup could not remove restore files. Changed files: ${prepared.map((entry) => entry.file.path).join(', ')}. Restore files still present: ${cleanupFailures.join(', ')}.`,
|
|
76
|
+
received: cleanupFailures.join(', '),
|
|
77
|
+
expected: 'all restore files to be removed after every replacement was committed',
|
|
78
|
+
next: 'Verify the changed files, then remove the listed restore files.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { applied: true };
|
|
82
|
+
}
|
|
83
|
+
function orderedFiles(plan) { return [...plan.files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0); }
|
|
84
|
+
function absolutePath(root, file) { return path.resolve(root, file); }
|
|
85
|
+
function applyEdits(source, file) {
|
|
86
|
+
const text = source.toString('utf8');
|
|
87
|
+
const edits = [...file.edits].sort((left, right) => left.startUtf16 - right.startUtf16 || left.endUtf16 - right.endUtf16);
|
|
88
|
+
let previous = 0;
|
|
89
|
+
for (const edit of edits) {
|
|
90
|
+
if (!Number.isInteger(edit.startUtf16) || !Number.isInteger(edit.endUtf16) || edit.startUtf16 < previous || edit.endUtf16 < edit.startUtf16 || edit.endUtf16 > text.length)
|
|
91
|
+
throw new Error(`Invalid or overlapping edit in ${file.path}.`);
|
|
92
|
+
previous = edit.endUtf16;
|
|
93
|
+
}
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let end = source.length;
|
|
96
|
+
for (const edit of [...edits].reverse()) {
|
|
97
|
+
const startByte = Buffer.byteLength(text.slice(0, edit.startUtf16));
|
|
98
|
+
const endByte = Buffer.byteLength(text.slice(0, edit.endUtf16));
|
|
99
|
+
chunks.unshift(source.subarray(endByte, end));
|
|
100
|
+
chunks.unshift(Buffer.from(edit.replacement));
|
|
101
|
+
end = startByte;
|
|
102
|
+
}
|
|
103
|
+
chunks.unshift(source.subarray(0, end));
|
|
104
|
+
return Buffer.concat(chunks);
|
|
105
|
+
}
|
|
106
|
+
async function removePrepared(prepared, operations, retain = new Set()) {
|
|
107
|
+
await Promise.all(prepared.flatMap((entry) => [entry.temp, entry.restore]).filter((file) => !retain.has(file)).map(async (file) => { try {
|
|
108
|
+
await operations.rm(file, { force: true });
|
|
109
|
+
}
|
|
110
|
+
catch { } }));
|
|
111
|
+
}
|
|
112
|
+
function writeFailure(message, next) {
|
|
113
|
+
return { category: 'tool', code: 'rename-write-failed', message, expected: 'every file in the preview to still match its recorded source snapshot', next };
|
|
114
|
+
}
|
|
115
|
+
function message(error) { return error instanceof Error ? error.message : String(error); }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommandDeclaration, Invocation, InvocationError } from './types.js';
|
|
2
|
+
export declare const commandTree: CommandDeclaration;
|
|
3
|
+
export declare function findNode(tokens: readonly string[]): {
|
|
4
|
+
node: CommandDeclaration;
|
|
5
|
+
consumed: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseInvocation(argv: readonly string[], cwd?: string): Invocation | InvocationError | {
|
|
8
|
+
help: CommandDeclaration;
|
|
9
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
const kinds = ['function', 'class', 'interface', 'type', 'enum', 'const', 'let', 'var', 'method', 'property', 'getter', 'setter', 'constructor', 'module'];
|
|
3
|
+
const relations = ['refs', 'callers', 'calls', 'impl'];
|
|
4
|
+
const inspectUnknownFlags = { '--limit': 'Inspect caps rows per relation with `--each N`.' };
|
|
5
|
+
const filters = [
|
|
6
|
+
{ name: '--in', type: 'glob', repeat: true, description: 'Keep only rows under matching paths.' },
|
|
7
|
+
{ name: '--exclude', type: 'glob', repeat: true, description: 'Drop matching paths. Wins over --in.' },
|
|
8
|
+
];
|
|
9
|
+
const selectedTraversal = {
|
|
10
|
+
check(input) {
|
|
11
|
+
const only = input.flags['--only'] ?? [...relations];
|
|
12
|
+
const traverses = only.some((value) => value === 'callers' || value === 'calls');
|
|
13
|
+
const problems = [];
|
|
14
|
+
if (input.explicit.has('--depth') && !traverses)
|
|
15
|
+
problems.push({ field: '--depth', received: only.join(', '), expected: 'at least one of callers or calls', next: 'Omit --depth or select callers or calls.' });
|
|
16
|
+
if (input.explicit.has('--graph') && !traverses)
|
|
17
|
+
problems.push({ field: '--graph', received: only.join(', '), expected: 'at least one of callers or calls, because refs and impl produce no edges to draw', next: 'Omit --graph or select callers or calls.' });
|
|
18
|
+
if (input.explicit.has('--each') && input.flags['--graph'] === true)
|
|
19
|
+
problems.push({ field: '--each', received: String(input.flags['--each']), expected: 'not used with --graph', next: 'Omit --each; graphs are never capped.' });
|
|
20
|
+
return problems;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const renameApplyFilters = {
|
|
24
|
+
check(input) {
|
|
25
|
+
if (input.flags['--apply'] === true && (input.explicit.has('--in') || input.explicit.has('--exclude')))
|
|
26
|
+
return [{ field: '--apply', received: '--apply with a path filter', expected: '--apply without --in or --exclude', next: 'Preview a filtered rename without --apply, or apply the complete rename.' }];
|
|
27
|
+
return [];
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const renameIdentifier = {
|
|
31
|
+
check(input) {
|
|
32
|
+
const name = input.args[1];
|
|
33
|
+
if (input.args.length !== 2 || !name || identifier.test(name) && !keywords.has(name))
|
|
34
|
+
return [];
|
|
35
|
+
return [{ field: '<newName>', received: name, expected: 'a TypeScript identifier that is not a reserved keyword', next: 'Choose a valid replacement identifier, then retry rename.' }];
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
function queryHelp() {
|
|
39
|
+
return `One Cypher statement against the selected declarations and relationships in the union of this workspace's configured TypeScript projects.
|
|
40
|
+
|
|
41
|
+
Rows lead with a path:line:col address whenever the query returns file, line, col or an at column, and every printed address is accepted by show, type, inspect, and rename. Results over --rows go to a file, named in the output; the printed count is always the true total.
|
|
42
|
+
|
|
43
|
+
<schema>
|
|
44
|
+
Two node labels.
|
|
45
|
+
(:Declaration { key, at, name, qname, kind, exported, file, dir, top, line, col, endLine, lines, declCount, container })
|
|
46
|
+
key is src/core/runtime/revive.ts#reviveNode: module path followed by enclosing names, stable across edits around it. Members are src/api/client.ts#CrtrClient.reviveNode; locals are …#reviveNode.attempt. at is the printed path:line:col address. kind is module | function | class | interface | type | enum | namespace | const | let | var | method | property | getter | setter | constructor. top is the first two path segments, such as src/core. declCount records overloads, get/set pairs, and declaration merging. container is the enclosing declaration key.
|
|
47
|
+
(:External { key: node:fs#readFile, module: node:fs, name: readFile })
|
|
48
|
+
Every configured file contributes a module Declaration at path:0:0. Nothing under node_modules is a Declaration.
|
|
49
|
+
</schema>
|
|
50
|
+
|
|
51
|
+
<relationships>
|
|
52
|
+
Compiler-resolved: (d)-[:CONTAINS]->(member); (d)-[:REFERENCES {line,col,role}]->(target), role definition|call|import|type|write|read; (d)-[:CALLS {line,col,dispatch}]->(target); (d)-[:IMPLEMENTS]->(target); (d)-[:EXTENDS]->(base); (d)-[:TYPE_MENTION {line,col,position}]->(type); (module)-[:IMPORTS {line,names}]->(module|External); (module)-[:EXPORTS {name,line,col}]->(d); (c:Commit {hash,ts,author,subject})-[:CHANGED {added,removed}]->(d).
|
|
53
|
+
Inferred: (new:Declaration)-[:RENAMED_FROM {oldKey,basis}]->(c:Commit), basis text-identical | container-renamed | file-moved.
|
|
54
|
+
Excluded internal declaration occurrences are counted as internal sites, not stored. Every stored call occurrence is both a CALLS and REFERENCES edge at the same address. A dispatch call targets an interface or abstract member with dispatch=true; concrete members are <-[:IMPLEMENTS]-. IMPLEMENTS is heritage-based; structural object-literal satisfaction is not an edge. The graph holds one revision: the working tree.
|
|
55
|
+
</relationships>
|
|
56
|
+
|
|
57
|
+
<examples>
|
|
58
|
+
Every export nothing outside its own file references:
|
|
59
|
+
MATCH (d:Declaration) WHERE d.exported AND NOT EXISTS { MATCH (s:Declaration)-[:REFERENCES]->(d) WHERE s.file <> d.file } RETURN d.key, d.file, d.line, d.col ORDER BY d.key
|
|
60
|
+
|
|
61
|
+
A module's public surface, each export with fan-in and its top three callers:
|
|
62
|
+
MATCH (d:Declaration) WHERE d.exported AND d.file = $file OPTIONAL MATCH (r:Declaration)-[ref:REFERENCES]->(d) WITH d, count(ref) AS fanIn OPTIONAL MATCH (caller:Declaration)-[call:CALLS]->(d) WITH d, fanIn, caller.key AS caller, count(call) AS sites ORDER BY sites DESC, caller SKIP 0 WITH d, fanIn, collect(CASE WHEN caller IS NOT NULL THEN {caller: caller, sites: sites} ELSE NULL END) AS callers RETURN d.key, fanIn, coalesce(callers[1:3], []) AS topCallers ORDER BY d.key
|
|
63
|
+
|
|
64
|
+
Every implementation of an interface or abstract member:
|
|
65
|
+
MATCH (impl:Declaration)-[:IMPLEMENTS]->(target:Declaration) WHERE target.key = $key RETURN DISTINCT impl.key, impl.file, impl.line, impl.col ORDER BY impl.key
|
|
66
|
+
|
|
67
|
+
Every function whose signature mentions a type:
|
|
68
|
+
MATCH (f:Declaration)-[:TYPE_MENTION]->(t:Declaration) WHERE t.key = $key AND f.kind IN ['function','method'] RETURN DISTINCT f.key, f.file, f.line, f.col ORDER BY f.key
|
|
69
|
+
|
|
70
|
+
Every call crossing a boundary it should not:
|
|
71
|
+
MATCH (caller:Declaration)-[call:CALLS]->(callee:Declaration) WHERE caller.top = 'src/commands' AND callee.top = 'src/daemon' RETURN caller.key, callee.key, call.line, call.col ORDER BY caller.key, callee.key, call.line
|
|
72
|
+
UNION
|
|
73
|
+
MATCH (caller:Declaration)-[call:CALLS]->(ext:External) WHERE caller.top = 'src/api' AND ext.module <> 'lib' RETURN caller.key, ext.key AS callee, call.line, call.col ORDER BY caller.key, callee, call.line
|
|
74
|
+
|
|
75
|
+
Every symbol this branch changed, each with its callers — pass --since main:
|
|
76
|
+
UNWIND $commits AS hash MATCH (c:Commit)-[:CHANGED]->(d:Declaration) WHERE c.hash = hash WITH DISTINCT d OPTIONAL MATCH (caller:Declaration)-[:CALLS]->(d) RETURN d.key, coalesce(collect(DISTINCT caller.key), []) AS callers ORDER BY d.key
|
|
77
|
+
|
|
78
|
+
Functions and methods with no incoming calls:
|
|
79
|
+
MATCH (f:Declaration) WHERE f.kind IN ['function','method'] AND NOT EXISTS { MATCH (:Declaration)-[:CALLS]->(f) } RETURN f.key, f.file, f.line, f.col ORDER BY f.key
|
|
80
|
+
|
|
81
|
+
Everything that reaches a symbol within three call hops:
|
|
82
|
+
MATCH (caller:Declaration)-[:CALLS*1..3]->(hub:Declaration) WHERE hub.key = $key RETURN DISTINCT caller.key ORDER BY caller.key
|
|
83
|
+
|
|
84
|
+
Everything a symbol reaches within five call hops:
|
|
85
|
+
MATCH (hub:Declaration)-[:CALLS*1..5]->(callee:Declaration) WHERE hub.key = $key RETURN DISTINCT callee.key ORDER BY callee.key
|
|
86
|
+
</examples>
|
|
87
|
+
|
|
88
|
+
<dialect>
|
|
89
|
+
The engine is LadybugDB 0.20.4, openCypher. Five runtime-verified differences from Neo4j Cypher: CALL { ... } and correlated CALL (d) { ... } are rejected; aggregate with WITH, OPTIONAL MATCH, then collect. List comprehensions [x IN list WHERE p | e] are rejected; use UNWIND/WITH, or projection and aggregation. WITH ... ORDER BY ... WITH ... needs SKIP or LIMIT between them; ORDER BY sites DESC, caller SKIP 0 imposes no cap. List slices are 1-based inclusive: [1:3] takes three elements. collect() over only nulls yields null, not []; wrap it with coalesce(collect(...), []). Always bound variable-length patterns, for example [:CALLS*1..3], never [:CALLS*]. An unbounded walk over a cyclic call graph is killed at 30 seconds.
|
|
90
|
+
</dialect>`;
|
|
91
|
+
}
|
|
92
|
+
const identifier = /^[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}]*$/u;
|
|
93
|
+
const keywords = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']);
|
|
94
|
+
function leaf(name, description, whenToUse, handler, argument, flags, output, effects = 'None.') {
|
|
95
|
+
return { name, description, whenToUse, handler, argument, flags, output, effects };
|
|
96
|
+
}
|
|
97
|
+
export const commandTree = {
|
|
98
|
+
name: 'tsym',
|
|
99
|
+
description: 'Symbol-level reading of a TypeScript codebase, from the compiler\'s own resolution.',
|
|
100
|
+
model: 'Every answer comes from a loaded TypeScript program, not text matching. With no name or address in hand, start at outline; with a name, go straight to find, show, type, or inspect. An address on a declared name or reference resolves to that symbol; any other address inside an indexed declaration resolves to the innermost enclosing declaration. Outside every declaration, the error lists the module address and top-level declaration addresses. A resident server starts when a semantic command needs it and exits after idle time. Server state is stopped, ready, or stopping.',
|
|
101
|
+
children: [
|
|
102
|
+
leaf('outline', 'Structure of a file, a directory, or the whole repository.', 'Reach for this first when you need the shape of unfamiliar code before reading declarations.', 'outline', { name: 'path', repeat: '0+', description: 'File, directory, or glob, relative to the current directory. Default: the workspace root.' }, [{ name: '--depth', type: 'int', default: 1, minimum: 1, description: 'Levels to descend. On a file, depth 1 is top-level declarations. On a directory, depth 1 is immediate file and subdirectory containers; depth 2 lists declarations in every file below it, however deeply nested. Directories never consume a level.' }, { name: '--kind', type: 'string', repeat: true, values: kinds, description: `Keep only these kinds: ${kinds.join(', ')}.` }, { name: '--exported', type: 'bool', description: 'Keep only exported declarations.' }, { name: '--members', type: 'bool', description: 'Expand members on reached class, interface, enum, and module rows. Does not consume depth.' }, { name: '--imports', type: 'bool', description: 'Include one row per import statement, which is dropped by default.' }, ...filters, { name: '--limit', type: 'int', minimum: 0, description: 'Cap rows.' }], 'One <outline> element. Symbol rows are `path:line:col kind [export] name (NL[, N members])`, ordered by source position. Container rows are `path (N files, N symbols)`; their files and symbols counts are after filters. The element symbols count is the total declarations and files is the files in scope. Count is the true total and shown appears only when capped.'),
|
|
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
|
+
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
|
+
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 four.` }, { 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 or impl 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>, and <impl> blocks; --only emits only selected blocks. Empty selected relations render count="0". 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`. Callers of a class or interface are sites that construct it, so one reached only by injection or a type annotation has zero callers; trace calls through its methods (`inspect Class.method` or `outline --members`) or `<refs>`. Each block count is the true total, files is distinct result paths, and shown appears only when capped by --each. The receipt names excluded, unresolved, and dispatch counts when nonzero. --graph instead returns one Mermaid flowchart; each node label includes a path:line:col address.', 'None.'), constraints: [selectedTraversal], unknownFlagNext: inspectUnknownFlags },
|
|
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
|
+
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
|
+
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.'),
|
|
110
|
+
{ ...leaf('rename', 'Rename a symbol across the program.', 'Reach for this instead of editing call sites by hand when a rename crosses files.', 'rename', { name: 'symbol and newName', repeat: '1+', description: 'Exactly two values: path:line:col or a name, then a TypeScript identifier that is not a reserved keyword. Address resolution follows the root command contract.' }, [{ name: '--apply', type: 'bool', description: 'Write the complete compiler-provided edit plan. Rejected with --in or --exclude.' }, ...filters], 'One <rename> element with symbol, from, to, sites, files, and applied attributes, containing one <file path="…" sites="N"> per affected file. File rows are `line:col old → new`. Applied says whether every listed file was written.', 'Without --apply: none. With --apply: every planned file is rewritten, all or none for failures observed by this process.'), constraints: [renameApplyFilters, renameIdentifier] },
|
|
111
|
+
{
|
|
112
|
+
name: 'server', description: 'The resident TypeScript process.', whenToUse: 'Reach for this only when the server is wrong rather than absent, such as after a branch switch or dependency install.',
|
|
113
|
+
model: 'One server per workspace root holds the bundled TypeScript 7 program and its index. Ordinary file edits need nothing; reload is for changes to program shape. Every semantic command starts a missing server and reuses a live one; it exits after 14400 seconds idle.',
|
|
114
|
+
state: () => null,
|
|
115
|
+
children: [
|
|
116
|
+
leaf('status', 'Whether a server is live, and what it is running.', 'Reach for this after an unexpected failure or slow command to read server identity and log path.', 'server-status', undefined, [], 'One <server> element. State is stopped, ready, or stopping. A live result reports root, pid, port, uptime, resident memory, the bundled TypeScript 7 version, store path, indexed commit, selected-scope declaration and edge counts, files outside configured projects, index state, last index duration, and log path. A stopped server is an answered empty result and exits 1.'),
|
|
117
|
+
leaf('stop', 'Stop the server for this root.', 'Reach for this to free resident memory or recover a server.', 'server-stop', undefined, [], 'One <server> element describing whether the process for this root stopped.', 'Stops the resident process; it never starts one.'),
|
|
118
|
+
leaf('reload', 'Restart the server, discarding the loaded program and index.', 'Reach for this after a branch switch, tsconfig change, or dependency install.', 'server-reload', undefined, [], 'One <server> element describing the fresh ready process.', 'Stops then starts the resident process and forces a full index rebuild.'),
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
const globals = [
|
|
124
|
+
{ name: '--root', type: 'path', description: 'Workspace root, overriding discovery from the current directory.' },
|
|
125
|
+
{ name: '--json', type: 'bool', description: 'Raw JSON on stdout for programmatic consumers.' },
|
|
126
|
+
];
|
|
127
|
+
export function findNode(tokens) {
|
|
128
|
+
let node = commandTree;
|
|
129
|
+
let consumed = 0;
|
|
130
|
+
while (node.children?.length && tokens[consumed]) {
|
|
131
|
+
const child = node.children.find((candidate) => candidate.name === tokens[consumed]);
|
|
132
|
+
if (!child)
|
|
133
|
+
break;
|
|
134
|
+
node = child;
|
|
135
|
+
consumed++;
|
|
136
|
+
}
|
|
137
|
+
return { node, consumed };
|
|
138
|
+
}
|
|
139
|
+
export function parseInvocation(argv, cwd = realpathSync(process.cwd())) {
|
|
140
|
+
const helpIndex = argv.findIndex((token) => token === '-h' || token === '--help');
|
|
141
|
+
const withoutHelp = helpIndex >= 0 ? [...argv.slice(0, helpIndex), ...argv.slice(helpIndex + 1)] : [...argv];
|
|
142
|
+
const { tokens: noHelp, commandTokens } = normalizeGlobals(withoutHelp);
|
|
143
|
+
const pathTokens = [];
|
|
144
|
+
for (const token of commandTokens) {
|
|
145
|
+
if (token === '--' || token.startsWith('-'))
|
|
146
|
+
break;
|
|
147
|
+
pathTokens.push(token);
|
|
148
|
+
}
|
|
149
|
+
const { node, consumed } = findNode(pathTokens);
|
|
150
|
+
if (helpIndex >= 0)
|
|
151
|
+
return { help: node };
|
|
152
|
+
if (node.children && consumed < pathTokens.length)
|
|
153
|
+
return invalid(node, [{ field: 'command', received: pathTokens[consumed], expected: `one of ${node.children.map((child) => child.name).join(', ')}`, next: `Run tsym ${node.name === 'tsym' ? '' : node.name + ' '}-h and read the command tree.` }]);
|
|
154
|
+
const parsed = parseArgs(noHelp.slice(consumed), node);
|
|
155
|
+
if (node.children && consumed === pathTokens.length && parsed.problems.length === 0 && parsed.input.args.length === 0)
|
|
156
|
+
return { help: node };
|
|
157
|
+
const root = parsed.input.flags['--root'];
|
|
158
|
+
const json = parsed.input.flags['--json'] === true;
|
|
159
|
+
const constraintProblems = (node.constraints ?? []).flatMap((constraint) => constraint.check(parsed.input));
|
|
160
|
+
if (parsed.problems.length > 0 || constraintProblems.length > 0)
|
|
161
|
+
return invalid(node, [...parsed.problems, ...constraintProblems]);
|
|
162
|
+
return { root, cwd, json, path: noHelp.slice(0, consumed), declaration: node, input: parsed.input };
|
|
163
|
+
}
|
|
164
|
+
function parseArgs(tokens, node) {
|
|
165
|
+
const flags = new Map([...globals, ...(node.flags ?? [])].map((flag) => [flag.name, flag]));
|
|
166
|
+
const values = {};
|
|
167
|
+
const explicit = new Set();
|
|
168
|
+
for (const declaration of flags.values())
|
|
169
|
+
if (declaration.default !== undefined)
|
|
170
|
+
values[declaration.name] = declaration.default;
|
|
171
|
+
const args = [];
|
|
172
|
+
const problems = [];
|
|
173
|
+
let positional = true;
|
|
174
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
175
|
+
const token = tokens[index];
|
|
176
|
+
if (token === '--') {
|
|
177
|
+
positional = false;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (positional && token.startsWith('-') && token !== '-') {
|
|
181
|
+
const [name, inline] = token.split('=', 2);
|
|
182
|
+
const declaration = flags.get(name);
|
|
183
|
+
if (!declaration) {
|
|
184
|
+
const renamed = node.unknownFlagNext?.[name];
|
|
185
|
+
problems.push({ field: name, received: token, expected: `a declared flag for tsym ${[...nodePath(node), node.name].filter(Boolean).join(' ')}`, next: renamed ?? helpNext(node) });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const duplicated = explicit.has(name) && !declaration.repeat;
|
|
189
|
+
explicit.add(name);
|
|
190
|
+
if (declaration.type === 'bool') {
|
|
191
|
+
if (duplicated)
|
|
192
|
+
problems.push({ field: name, received: token, expected: 'one occurrence; this flag is not repeatable', next: helpNext(node) });
|
|
193
|
+
if (inline !== undefined)
|
|
194
|
+
problems.push({ field: name, received: token, expected: 'a boolean flag with no value', next: helpNext(node) });
|
|
195
|
+
else
|
|
196
|
+
values[name] = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const raw = inline ?? tokens[++index];
|
|
200
|
+
if (raw === undefined) {
|
|
201
|
+
problems.push({ field: name, received: token, expected: `${declaration.type} value`, next: helpNext(node) });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const parsed = repeatValues(raw, declaration, problems, node);
|
|
205
|
+
if (parsed.length === 0)
|
|
206
|
+
continue;
|
|
207
|
+
if (declaration.repeat)
|
|
208
|
+
values[name] = [...(values[name] ?? []), ...parsed.map(String)];
|
|
209
|
+
else if (duplicated)
|
|
210
|
+
problems.push({ field: name, received: raw, expected: 'one value; this flag is not repeatable', next: helpNext(node) });
|
|
211
|
+
else
|
|
212
|
+
values[name] = parsed[0];
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
args.push(token);
|
|
216
|
+
}
|
|
217
|
+
const argument = node.argument;
|
|
218
|
+
if (argument) {
|
|
219
|
+
const invalidCount = (argument.repeat === '1' && args.length !== 1) || (argument.repeat === '1+' && args.length < 1);
|
|
220
|
+
if (invalidCount)
|
|
221
|
+
problems.push({ field: `<${argument.name}>`, received: args.length === 0 ? 'none' : args.join(' '), expected: argument.repeat === '1' ? 'exactly one value' : 'one or more values', next: helpNext(node) });
|
|
222
|
+
if (node.name === 'rename' && args.length !== 2)
|
|
223
|
+
problems.push({ field: '<symbol> <newName>', received: args.join(' ') || 'none', expected: 'exactly two values', next: helpNext(node) });
|
|
224
|
+
}
|
|
225
|
+
else if (args.length > 0)
|
|
226
|
+
problems.push({ field: 'arguments', received: args.join(' '), expected: 'no positional arguments', next: helpNext(node) });
|
|
227
|
+
return { input: { args, flags: values, explicit }, problems };
|
|
228
|
+
}
|
|
229
|
+
function repeatValues(raw, declaration, problems, node) {
|
|
230
|
+
const values = declaration.repeat && declaration.type === 'string' && declaration.values ? raw.split(',') : [raw];
|
|
231
|
+
return values.map((value) => parseValue(value, declaration, problems, node)).filter((value) => value !== undefined);
|
|
232
|
+
}
|
|
233
|
+
function parseValue(raw, declaration, problems, node) {
|
|
234
|
+
if (declaration.type === 'int') {
|
|
235
|
+
if (!/^-?\d+$/.test(raw)) {
|
|
236
|
+
problems.push({ field: declaration.name, received: raw, expected: 'an integer', next: helpNext(node) });
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const value = Number(raw);
|
|
240
|
+
if (declaration.minimum !== undefined && value < declaration.minimum) {
|
|
241
|
+
problems.push({ field: declaration.name, received: raw, expected: `an integer at least ${declaration.minimum}`, next: helpNext(node) });
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
if (declaration.values && !declaration.values.includes(raw)) {
|
|
247
|
+
problems.push({ field: declaration.name, received: raw, expected: `one of ${declaration.values.join(', ')}`, next: helpNext(node) });
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
return raw;
|
|
251
|
+
}
|
|
252
|
+
function normalizeGlobals(tokens) {
|
|
253
|
+
const commandTokens = [];
|
|
254
|
+
const globals = [];
|
|
255
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
256
|
+
const token = tokens[index];
|
|
257
|
+
if (token === '--json') {
|
|
258
|
+
globals.push(token);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (token === '--root') {
|
|
262
|
+
globals.push(token);
|
|
263
|
+
if (tokens[index + 1] !== undefined)
|
|
264
|
+
globals.push(tokens[++index]);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (token.startsWith('--root=')) {
|
|
268
|
+
globals.push(token);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
commandTokens.push(token);
|
|
272
|
+
}
|
|
273
|
+
return { tokens: [...commandTokens, ...globals], commandTokens };
|
|
274
|
+
}
|
|
275
|
+
function nodePath(node) { return node === commandTree ? [] : []; }
|
|
276
|
+
function helpNext(node) { return `Run \`tsym ${node.name === 'tsym' ? '' : node.name + ' '}-h\` and read the schema before re-issuing.`; }
|
|
277
|
+
function invalid(node, problems) { return { category: 'invocation', code: 'invalid-invocation', problems: problems.map((problem) => ({ ...problem, next: problem.next ?? helpNext(node) })) }; }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type Primitive = 'bool' | 'int' | 'path' | 'glob' | 'string';
|
|
2
|
+
export interface FlagDeclaration {
|
|
3
|
+
name: string;
|
|
4
|
+
type: Primitive;
|
|
5
|
+
repeat?: true;
|
|
6
|
+
default?: boolean | number | string;
|
|
7
|
+
values?: readonly string[];
|
|
8
|
+
minimum?: number;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ArgumentDeclaration {
|
|
12
|
+
name: string;
|
|
13
|
+
repeat: '0+' | '1' | '1+';
|
|
14
|
+
description: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Constraint {
|
|
17
|
+
check(input: ParsedInput): ValidationProblem[];
|
|
18
|
+
}
|
|
19
|
+
export interface CommandDeclaration {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
whenToUse?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
argument?: ArgumentDeclaration;
|
|
25
|
+
flags?: readonly FlagDeclaration[];
|
|
26
|
+
constraints?: readonly Constraint[];
|
|
27
|
+
output?: string;
|
|
28
|
+
effects?: string;
|
|
29
|
+
children?: readonly CommandDeclaration[];
|
|
30
|
+
handler?: HandlerName;
|
|
31
|
+
state?: () => string | null;
|
|
32
|
+
}
|
|
33
|
+
export type HandlerName = 'outline' | 'find' | 'show' | 'type' | 'inspect' | 'query' | 'search' | 'check' | 'rename' | 'server-status' | 'server-stop' | 'server-reload';
|
|
34
|
+
export interface ParsedInput {
|
|
35
|
+
args: string[];
|
|
36
|
+
flags: Record<string, boolean | number | string | string[]>;
|
|
37
|
+
explicit: ReadonlySet<string>;
|
|
38
|
+
}
|
|
39
|
+
export interface ValidationProblem {
|
|
40
|
+
field: string;
|
|
41
|
+
received: string;
|
|
42
|
+
expected: string;
|
|
43
|
+
next?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface Invocation {
|
|
46
|
+
root: string | undefined;
|
|
47
|
+
/** The client's real working directory. Filesystem operands resolve from it; symbol addresses resolve from root. */
|
|
48
|
+
cwd: string;
|
|
49
|
+
json: boolean;
|
|
50
|
+
path: string[];
|
|
51
|
+
declaration: CommandDeclaration;
|
|
52
|
+
input: ParsedInput;
|
|
53
|
+
}
|
|
54
|
+
export interface InvocationError {
|
|
55
|
+
category: 'invocation';
|
|
56
|
+
code: string;
|
|
57
|
+
problems: ValidationProblem[];
|
|
58
|
+
}
|
|
59
|
+
export interface ToolError {
|
|
60
|
+
category: 'tool';
|
|
61
|
+
code: string;
|
|
62
|
+
attributes?: Record<string, string>;
|
|
63
|
+
message: string;
|
|
64
|
+
logPath?: string;
|
|
65
|
+
received?: string;
|
|
66
|
+
expected?: string;
|
|
67
|
+
next?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface OutputNode {
|
|
70
|
+
element: string;
|
|
71
|
+
attributes?: Record<string, string | number | boolean | undefined>;
|
|
72
|
+
text?: string;
|
|
73
|
+
children?: OutputNode[];
|
|
74
|
+
}
|
|
75
|
+
export interface Answer {
|
|
76
|
+
data: OutputNode | OutputNode[];
|
|
77
|
+
count: number;
|
|
78
|
+
receipt?: string;
|
|
79
|
+
}
|
|
80
|
+
export type Completion = Answer | InvocationError | ToolError;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SourceReader, type SyntaxDeclaration } from './source.js';
|
|
2
|
+
export interface DeclarationAddress {
|
|
3
|
+
path: string;
|
|
4
|
+
line: number;
|
|
5
|
+
col: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DeclarationRecord {
|
|
8
|
+
address: string;
|
|
9
|
+
location: DeclarationAddress;
|
|
10
|
+
file: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
name: string;
|
|
13
|
+
qualifiedName: string;
|
|
14
|
+
nameSpan: {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
};
|
|
18
|
+
span: {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare class DeclarationError extends Error {
|
|
24
|
+
readonly code: 'no-such-path' | 'not-in-program' | 'unresolvable-symbol' | 'ambiguous-symbol' | 'invalid-address';
|
|
25
|
+
readonly candidates: string[];
|
|
26
|
+
constructor(code: 'no-such-path' | 'not-in-program' | 'unresolvable-symbol' | 'ambiguous-symbol' | 'invalid-address', message: string, candidates?: string[]);
|
|
27
|
+
}
|
|
28
|
+
export declare class DeclarationResolutionErrors extends Error {
|
|
29
|
+
readonly errors: readonly DeclarationError[];
|
|
30
|
+
constructor(errors: readonly DeclarationError[]);
|
|
31
|
+
}
|
|
32
|
+
/** Resolves every declaration identity from the committed index. */
|
|
33
|
+
export declare class DeclarationResolver {
|
|
34
|
+
readonly root: string;
|
|
35
|
+
readonly source: SourceReader;
|
|
36
|
+
private readonly storeFor;
|
|
37
|
+
constructor(root: string, source: SourceReader, storeFor: () => import('../store/store.js').Store | undefined);
|
|
38
|
+
resolve(input: string): Promise<DeclarationRecord>;
|
|
39
|
+
resolveAll(inputs: readonly string[]): Promise<DeclarationRecord[]>;
|
|
40
|
+
resolveAddress(address: DeclarationAddress): Promise<DeclarationRecord>;
|
|
41
|
+
resolveName(name: string): Promise<DeclarationRecord>;
|
|
42
|
+
fromSyntax(file: string, declaration: SyntaxDeclaration): DeclarationRecord;
|
|
43
|
+
private rows;
|
|
44
|
+
private record;
|
|
45
|
+
private unresolvableAddress;
|
|
46
|
+
private validatePosition;
|
|
47
|
+
private notInProgram;
|
|
48
|
+
}
|