@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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/bin/tsym.js +2 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +89 -0
  6. package/dist/commands/handlers/check.d.ts +3 -0
  7. package/dist/commands/handlers/check.js +94 -0
  8. package/dist/commands/handlers/find.d.ts +3 -0
  9. package/dist/commands/handlers/find.js +56 -0
  10. package/dist/commands/handlers/inspect.d.ts +3 -0
  11. package/dist/commands/handlers/inspect.js +81 -0
  12. package/dist/commands/handlers/outline.d.ts +3 -0
  13. package/dist/commands/handlers/outline.js +101 -0
  14. package/dist/commands/handlers/query.d.ts +3 -0
  15. package/dist/commands/handlers/query.js +196 -0
  16. package/dist/commands/handlers/rename.d.ts +3 -0
  17. package/dist/commands/handlers/rename.js +53 -0
  18. package/dist/commands/handlers/search.d.ts +5 -0
  19. package/dist/commands/handlers/search.js +32 -0
  20. package/dist/commands/handlers/server-reload.d.ts +3 -0
  21. package/dist/commands/handlers/server-reload.js +7 -0
  22. package/dist/commands/handlers/server-status.d.ts +3 -0
  23. package/dist/commands/handlers/server-status.js +7 -0
  24. package/dist/commands/handlers/server-stop.d.ts +3 -0
  25. package/dist/commands/handlers/server-stop.js +7 -0
  26. package/dist/commands/handlers/show.d.ts +3 -0
  27. package/dist/commands/handlers/show.js +63 -0
  28. package/dist/commands/handlers/type.d.ts +3 -0
  29. package/dist/commands/handlers/type.js +45 -0
  30. package/dist/commands/handlers.d.ts +12 -0
  31. package/dist/commands/handlers.js +44 -0
  32. package/dist/commands/help.d.ts +2 -0
  33. package/dist/commands/help.js +27 -0
  34. package/dist/commands/rename-apply.d.ts +37 -0
  35. package/dist/commands/rename-apply.js +115 -0
  36. package/dist/commands/tree.d.ts +9 -0
  37. package/dist/commands/tree.js +277 -0
  38. package/dist/commands/types.d.ts +80 -0
  39. package/dist/commands/types.js +1 -0
  40. package/dist/core/declarations.d.ts +48 -0
  41. package/dist/core/declarations.js +153 -0
  42. package/dist/core/receipt.d.ts +7 -0
  43. package/dist/core/receipt.js +9 -0
  44. package/dist/core/search.d.ts +29 -0
  45. package/dist/core/search.js +47 -0
  46. package/dist/core/source.d.ts +32 -0
  47. package/dist/core/source.js +96 -0
  48. package/dist/core/workspace.d.ts +28 -0
  49. package/dist/core/workspace.js +137 -0
  50. package/dist/index/build.d.ts +22 -0
  51. package/dist/index/build.js +79 -0
  52. package/dist/index/classify.d.ts +20 -0
  53. package/dist/index/classify.js +60 -0
  54. package/dist/index/commits.d.ts +15 -0
  55. package/dist/index/commits.js +201 -0
  56. package/dist/index/descriptor.d.ts +19 -0
  57. package/dist/index/descriptor.js +295 -0
  58. package/dist/index/graph.d.ts +82 -0
  59. package/dist/index/graph.js +1 -0
  60. package/dist/index/heritage.d.ts +15 -0
  61. package/dist/index/heritage.js +122 -0
  62. package/dist/index/refresh.d.ts +28 -0
  63. package/dist/index/refresh.js +110 -0
  64. package/dist/index/scope.d.ts +4 -0
  65. package/dist/index/scope.js +21 -0
  66. package/dist/index/state.d.ts +22 -0
  67. package/dist/index/state.js +63 -0
  68. package/dist/index/walk.d.ts +23 -0
  69. package/dist/index/walk.js +550 -0
  70. package/dist/index/watch.d.ts +18 -0
  71. package/dist/index/watch.js +175 -0
  72. package/dist/index.d.ts +4 -0
  73. package/dist/index.js +2 -0
  74. package/dist/output.d.ts +12 -0
  75. package/dist/output.js +54 -0
  76. package/dist/read/read.d.ts +32 -0
  77. package/dist/read/read.js +89 -0
  78. package/dist/relations/graph.d.ts +23 -0
  79. package/dist/relations/graph.js +115 -0
  80. package/dist/relations/mermaid.d.ts +3 -0
  81. package/dist/relations/mermaid.js +37 -0
  82. package/dist/relations/store-relations.d.ts +20 -0
  83. package/dist/relations/store-relations.js +158 -0
  84. package/dist/relations/types.d.ts +58 -0
  85. package/dist/relations/types.js +12 -0
  86. package/dist/server/client.d.ts +5 -0
  87. package/dist/server/client.js +215 -0
  88. package/dist/server/context.d.ts +21 -0
  89. package/dist/server/context.js +12 -0
  90. package/dist/server/lifecycle.d.ts +28 -0
  91. package/dist/server/lifecycle.js +62 -0
  92. package/dist/server/main.d.ts +1 -0
  93. package/dist/server/main.js +541 -0
  94. package/dist/server/projects.d.ts +21 -0
  95. package/dist/server/projects.js +28 -0
  96. package/dist/server/wire.d.ts +39 -0
  97. package/dist/server/wire.js +19 -0
  98. package/dist/store/load.d.ts +67 -0
  99. package/dist/store/load.js +193 -0
  100. package/dist/store/meta.d.ts +28 -0
  101. package/dist/store/meta.js +137 -0
  102. package/dist/store/query.d.ts +5 -0
  103. package/dist/store/query.js +4 -0
  104. package/dist/store/schema.d.ts +4 -0
  105. package/dist/store/schema.js +19 -0
  106. package/dist/store/store.d.ts +36 -0
  107. package/dist/store/store.js +240 -0
  108. package/dist/ts7/api.d.ts +35 -0
  109. package/dist/ts7/api.js +164 -0
  110. package/dist/ts7/ast.d.ts +1 -0
  111. package/dist/ts7/ast.js +1 -0
  112. package/dist/ts7/checker.d.ts +55 -0
  113. package/dist/ts7/checker.js +383 -0
  114. package/dist/ts7/projects.d.ts +27 -0
  115. package/dist/ts7/projects.js +94 -0
  116. package/package.json +47 -0
@@ -0,0 +1,175 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, statSync, watch } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { discoverDirectories, isProgramFileCandidate, skippedDirectories } from '../ts7/projects.js';
5
+ /** Watches configured source directories; events supply text changes, never semantic invalidation. */
6
+ export function watchIndex(root, projects, refresh, reportError, markStale) {
7
+ let knownFiles = new Set();
8
+ let knownDirectories = new Set();
9
+ let rootWatcher;
10
+ let gitWatcher;
11
+ let timer;
12
+ let firstEventAt;
13
+ let invalidated = false;
14
+ let full = false;
15
+ let git = false;
16
+ const events = new Map();
17
+ let chain = Promise.resolve();
18
+ const debounceMs = configuredDebounceMs();
19
+ const resetDirectories = () => {
20
+ gitWatcher?.close();
21
+ const sourceFiles = projects.records().flatMap((project) => [...project.files]);
22
+ const configs = projects.records().map((project) => project.config);
23
+ // Configs are known files: rewriting one is a change, and calling it a creation leaves TypeScript on the stale config.
24
+ knownFiles = new Set([...sourceFiles, ...configs]);
25
+ knownDirectories = new Set(discoverDirectories(root));
26
+ gitWatcher = watchGit(root, () => { git = true; invalidate(); });
27
+ };
28
+ const onRootEvent = (_event, name) => {
29
+ // Deleting the root arrives as one event named after the root itself, which no file filter below would keep.
30
+ if (name === null || !existsSync(root)) {
31
+ invalidate();
32
+ return;
33
+ }
34
+ const file = path.resolve(root, name.toString());
35
+ const relative = path.relative(root, file);
36
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || relative.split(path.sep).some((part) => skippedDirectories.has(part)))
37
+ return;
38
+ // fs.watch reports renames for a directory move and does not enumerate every moved child.
39
+ if (isDirectory(file) || knownDirectories.has(file) || [...knownFiles].some((known) => known.startsWith(`${file}${path.sep}`))) {
40
+ invalidate();
41
+ return;
42
+ }
43
+ const known = knownFiles.has(file);
44
+ if (!known && !isProgramFileCandidate(file))
45
+ return;
46
+ const configuration = path.basename(file) === 'tsconfig.json' || path.basename(file) === 'package.json';
47
+ // A membership change needs the full path, which resets knownFiles from the reloaded project records.
48
+ if (existsSync(file))
49
+ record(file, known ? 'changed' : 'created', configuration || !known);
50
+ else
51
+ record(file, 'deleted', configuration || known);
52
+ };
53
+ function record(file, event, requiresFull) {
54
+ if (event === 'created')
55
+ knownFiles.add(file);
56
+ events.set(file, event);
57
+ schedule(requiresFull);
58
+ }
59
+ function invalidate() {
60
+ invalidated = true;
61
+ schedule(true);
62
+ }
63
+ /** Marks the index stale on a batch's first event and debounces the flush that clears it. */
64
+ function schedule(requiresFull) {
65
+ full ||= requiresFull;
66
+ const now = Date.now();
67
+ if (firstEventAt === undefined)
68
+ markStale(flush);
69
+ firstEventAt ??= now;
70
+ if (timer)
71
+ clearTimeout(timer);
72
+ const cap = Math.max(0, 5_000 - (now - firstEventAt));
73
+ timer = setTimeout(() => { void flush(); }, Math.min(debounceMs, cap));
74
+ timer.unref();
75
+ }
76
+ /** Refreshes whatever has accumulated and resolves when every batch queued so far has been applied. */
77
+ function flush() {
78
+ if (timer)
79
+ clearTimeout(timer);
80
+ timer = undefined;
81
+ if (!invalidated && events.size === 0)
82
+ return chain;
83
+ const batch = {
84
+ fileChanges: invalidated ? { invalidateAll: true } : { changed: files('changed'), created: files('created'), deleted: files('deleted') },
85
+ full,
86
+ git,
87
+ };
88
+ invalidated = false;
89
+ full = false;
90
+ git = false;
91
+ events.clear();
92
+ firstEventAt = undefined;
93
+ chain = chain.then(() => refresh(batch), () => refresh(batch));
94
+ void chain.catch(reportError);
95
+ return chain;
96
+ }
97
+ function files(event) {
98
+ return [...events].filter(([, recorded]) => recorded === event).map(([file]) => file);
99
+ }
100
+ resetDirectories();
101
+ rootWatcher = watch(root, { recursive: true }, onRootEvent);
102
+ return {
103
+ close() {
104
+ if (timer)
105
+ clearTimeout(timer);
106
+ rootWatcher?.close();
107
+ gitWatcher?.close();
108
+ },
109
+ refresh: resetDirectories,
110
+ };
111
+ }
112
+ function isDirectory(file) {
113
+ try {
114
+ return statSync(file).isDirectory();
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ function watchGit(root, changed) {
121
+ let state = gitState(root);
122
+ const gitDir = gitPath(root, '--git-dir');
123
+ const commonDir = gitPath(root, '--git-common-dir');
124
+ let watchers = [];
125
+ const bind = () => {
126
+ for (const watcher of watchers)
127
+ watcher.close();
128
+ const paths = new Set([
129
+ gitDir === undefined ? undefined : path.join(gitDir, 'HEAD'),
130
+ commonDir === undefined ? undefined : path.join(commonDir, 'packed-refs'),
131
+ commonDir === undefined ? undefined : path.join(commonDir, 'refs', 'heads'),
132
+ commonDir === undefined || state.ref === undefined ? undefined : path.dirname(path.join(commonDir, state.ref)),
133
+ commonDir === undefined || state.ref === undefined ? undefined : path.join(commonDir, state.ref),
134
+ ].filter((target) => target !== undefined));
135
+ watchers = [];
136
+ for (const target of paths) {
137
+ if (!existsSync(target))
138
+ continue;
139
+ try {
140
+ watchers.push(watch(target, check));
141
+ }
142
+ catch { /* A ref can disappear while its branch changes; the remaining watcher reconnects when HEAD moves. */ }
143
+ }
144
+ };
145
+ const check = () => {
146
+ const next = gitState(root);
147
+ if (next.head === state.head && next.ref === state.ref)
148
+ return;
149
+ state = next;
150
+ bind();
151
+ changed();
152
+ };
153
+ bind();
154
+ return { close: () => { for (const watcher of watchers)
155
+ watcher.close(); } };
156
+ }
157
+ function gitState(root) {
158
+ return { head: gitOutput(root, ['rev-parse', 'HEAD']), ref: gitOutput(root, ['symbolic-ref', '-q', 'HEAD']) };
159
+ }
160
+ function gitPath(root, value) {
161
+ const output = gitOutput(root, ['rev-parse', value]);
162
+ return output === undefined ? undefined : path.resolve(root, output);
163
+ }
164
+ function gitOutput(root, args) {
165
+ try {
166
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ function configuredDebounceMs() {
173
+ const value = process.env.TSYM_DEBOUNCE_MS;
174
+ return value !== undefined && /^\d+$/.test(value) ? Number(value) : 300;
175
+ }
@@ -0,0 +1,4 @@
1
+ export { parseInvocation } from './commands/tree.js';
2
+ export { renderCompletion } from './output.js';
3
+ export type { Answer, Completion, Invocation, InvocationError, ToolError } from './commands/types.js';
4
+ export type { Graph, GraphEdge, GraphNode, RelationRow, TraversalResult } from './relations/types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { parseInvocation } from './commands/tree.js';
2
+ export { renderCompletion } from './output.js';
@@ -0,0 +1,12 @@
1
+ import type { Completion, OutputNode } from './commands/types.js';
2
+ /** Assembles chrome — help and error blocks — whose body is already rendered markup. Answer data goes through renderNode instead. */
3
+ export declare function xml(name: string, attributes?: Record<string, string | number | boolean | undefined>, body?: string): string;
4
+ /** Output is XML-shaped prose for an agent, never parsed as XML; only a double quote inside an attribute value is escaped, so the value's boundary stays readable. */
5
+ export declare function escapeAttribute(value: string): string;
6
+ export declare function renderCompletion(completion: Completion, json: boolean): {
7
+ stdout: string;
8
+ stderr: string;
9
+ exitCode: 0 | 1 | 2 | 3;
10
+ };
11
+ export declare function completionCode(completion: Completion): 0 | 1 | 2 | 3;
12
+ export declare function renderNode(node: OutputNode): string;
package/dist/output.js ADDED
@@ -0,0 +1,54 @@
1
+ /** Assembles chrome — help and error blocks — whose body is already rendered markup. Answer data goes through renderNode instead. */
2
+ export function xml(name, attributes = {}, body = '') {
3
+ const rendered = Object.entries(attributes).filter(([, value]) => value !== undefined).map(([key, value]) => ` ${key}="${escapeAttribute(String(value))}"`).join('');
4
+ return body === '' ? `<${name}${rendered}/>` : `<${name}${rendered}>\n${body}\n</${name}>`;
5
+ }
6
+ /** Output is XML-shaped prose for an agent, never parsed as XML; only a double quote inside an attribute value is escaped, so the value's boundary stays readable. */
7
+ export function escapeAttribute(value) { return value.replace(/"/g, '&quot;'); }
8
+ export function renderCompletion(completion, json) {
9
+ const exitCode = completionCode(completion);
10
+ const payload = json ? JSON.stringify(completion) + '\n' : render(completion) + '\n';
11
+ const stdout = payload;
12
+ const stderr = receipt(completion);
13
+ return { stdout, stderr, exitCode };
14
+ }
15
+ export function completionCode(completion) {
16
+ if (isAnswer(completion))
17
+ return completion.count > 0 ? 0 : 1;
18
+ if (completion.category === 'invocation')
19
+ return 2;
20
+ return 3;
21
+ }
22
+ function render(completion) {
23
+ if (isAnswer(completion))
24
+ return nodes(completion.data);
25
+ if (completion.category === 'invocation')
26
+ return renderInvocationError(completion);
27
+ return renderToolError(completion);
28
+ }
29
+ function nodes(data) { return (Array.isArray(data) ? data : [data]).map(renderNode).join('\n'); }
30
+ export function renderNode(node) {
31
+ const attributes = Object.entries(node.attributes ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => ` ${key}="${escapeAttribute(String(value))}"`).join('');
32
+ const body = [node.text ?? '', ...(node.children ?? []).map(renderNode)].filter(Boolean).join('\n');
33
+ return body === '' ? `<${node.element}${attributes}/>` : `<${node.element}${attributes}>\n${body}\n</${node.element}>`;
34
+ }
35
+ function renderInvocationError(error) {
36
+ const body = error.problems.map((problem) => {
37
+ const expected = problem.expected.endsWith('.') ? problem.expected : `${problem.expected}.`;
38
+ return `${problem.field}: received ${problem.received}; expected ${expected}\nNext: ${problem.next ?? ''}`;
39
+ }).join('\n');
40
+ return xml('error', { code: error.code }, body);
41
+ }
42
+ function renderToolError(error) {
43
+ const lines = [error.message, error.received ? `received: ${error.received}` : null, error.expected ? `expected: ${error.expected}` : null, error.logPath ? `log: ${error.logPath}` : null, error.next ? `Next: ${error.next}` : null].filter((line) => line !== null);
44
+ return xml('error', { code: error.code, ...error.attributes }, lines.join('\n'));
45
+ }
46
+ function receipt(completion) {
47
+ if (isAnswer(completion)) {
48
+ const answer = completion;
49
+ const base = answer.receipt ?? `${answer.count} answer${answer.count === 1 ? '' : 's'}, 0 ms`;
50
+ return answer.startupMs === undefined ? base : `${base} (server started, ${answer.startupMs} ms)`;
51
+ }
52
+ return completion.category === 'invocation' ? `invocation rejected, 0 ms` : `tool failed, 0 ms`;
53
+ }
54
+ function isAnswer(completion) { return !('category' in completion); }
@@ -0,0 +1,32 @@
1
+ import type { SemanticContext } from '../server/context.js';
2
+ export interface Position {
3
+ line: number;
4
+ offset: number;
5
+ }
6
+ export interface Span {
7
+ start: Position;
8
+ end: Position;
9
+ }
10
+ export interface ReadSymbol {
11
+ address: string;
12
+ path: string;
13
+ name: string;
14
+ qualifiedName: string;
15
+ kind: string;
16
+ exported: boolean;
17
+ span: Span;
18
+ children: ReadSymbol[];
19
+ }
20
+ export declare function displayPath(root: string, file: string): string;
21
+ export declare function positionOffset(source: string, position: Position): number;
22
+ export declare function lineCount(source: string, span: Span): number;
23
+ export declare function isContainer(symbol: ReadSymbol): boolean;
24
+ export declare function keep(symbol: ReadSymbol, flags: Record<string, boolean | number | string | string[]>): boolean;
25
+ /** Operands resolve from the client cwd; an empty operand list means the workspace root. */
26
+ export declare function scopeFiles(context: SemanticContext, cwd: string, inputs: readonly string[]): Promise<{
27
+ files: string[];
28
+ notInProgram: number;
29
+ }>;
30
+ export declare function sourceSpan(source: string, span: Span): string;
31
+ export declare function declarationText(source: string, span: Span, kind: string): string;
32
+ export declare function normalKind(kind: string): string;
@@ -0,0 +1,89 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DeclarationError } from '../core/declarations.js';
4
+ import { matchesPathFilters } from '../core/receipt.js';
5
+ import { openWorkspace } from '../core/workspace.js';
6
+ const containers = new Set(['class', 'interface', 'enum', 'namespace']);
7
+ const extensions = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
8
+ export function displayPath(root, file) { return path.relative(root, file).split(path.sep).join('/'); }
9
+ export function positionOffset(source, position) {
10
+ let line = 1;
11
+ let offset = 0;
12
+ while (line < position.line && offset < source.length) {
13
+ const newline = source.indexOf('\n', offset);
14
+ offset = newline < 0 ? source.length : newline + 1;
15
+ line++;
16
+ }
17
+ return offset + position.offset - 1;
18
+ }
19
+ export function lineCount(source, span) {
20
+ const start = positionOffset(source, span.start);
21
+ const end = positionOffset(source, span.end);
22
+ return source.slice(start, end).split('\n').length - (end > start && source[end - 1] === '\n' ? 1 : 0);
23
+ }
24
+ export function isContainer(symbol) { return containers.has(symbol.kind); }
25
+ export function keep(symbol, flags) {
26
+ const kinds = flags['--kind'];
27
+ return (!kinds || kinds.includes(symbol.kind)) && (flags['--exported'] !== true || symbol.exported) && matchesPathFilters(symbol.path, { in: flags['--in'], exclude: flags['--exclude'] });
28
+ }
29
+ /** Operands resolve from the client cwd; an empty operand list means the workspace root. */
30
+ export async function scopeFiles(context, cwd, inputs) {
31
+ const workspace = openWorkspace(context.root);
32
+ const listed = workspace.files().filter((file) => extensions.has(path.extname(file)));
33
+ const selected = new Set();
34
+ for (const input of inputs.length === 0 ? [context.root] : inputs) {
35
+ const absolute = path.resolve(cwd, input);
36
+ const relative = displayPath(context.root, absolute);
37
+ const glob = /[*?[{]/.test(input);
38
+ if (existsSync(absolute)) {
39
+ const stat = statSync(absolute);
40
+ if (stat.isFile()) {
41
+ if (!extensions.has(path.extname(absolute)))
42
+ throw new DeclarationError('not-in-program', `${input} is not a TypeScript-family source file`);
43
+ const membership = await context.projects.membership(absolute);
44
+ if (!membership)
45
+ throw new DeclarationError('not-in-program', `${input} exists but is not part of a TypeScript project under ${context.root}`);
46
+ selected.add(displayPath(context.root, absolute));
47
+ }
48
+ else if (stat.isDirectory()) {
49
+ for (const file of listed)
50
+ if (relative === '' || file === relative || file.startsWith(`${relative.replace(/\/$/, '')}/`))
51
+ selected.add(file);
52
+ }
53
+ }
54
+ else if (glob) {
55
+ for (const file of listed)
56
+ if (workspace.files(relative).includes(file))
57
+ selected.add(file);
58
+ }
59
+ else
60
+ throw new DeclarationError('no-such-path', `${input} does not exist`);
61
+ }
62
+ let notInProgram = 0;
63
+ const files = [];
64
+ for (const relative of [...selected].sort()) {
65
+ const absolute = path.join(context.root, relative);
66
+ if (await context.projects.membership(absolute))
67
+ files.push(absolute);
68
+ else
69
+ notInProgram++;
70
+ }
71
+ return { files, notInProgram };
72
+ }
73
+ export function sourceSpan(source, span) { return source.slice(positionOffset(source, span.start), positionOffset(source, span.end)); }
74
+ export function declarationText(source, span, kind) {
75
+ if (!['const', 'let', 'var'].includes(kind))
76
+ return sourceSpan(source, span);
77
+ const start = positionOffset(source, span.start);
78
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
79
+ const prefix = source.slice(lineStart, start);
80
+ const end = positionOffset(source, span.end);
81
+ const statementEnd = source.indexOf(';', end);
82
+ return /^(\s*)(?:export\s+)?(?:declare\s+)?(?:const|let|var)\b/.test(prefix) ? source.slice(lineStart, statementEnd < 0 ? end : statementEnd + 1) : sourceSpan(source, span);
83
+ }
84
+ export function normalKind(kind) {
85
+ const declarations = { FunctionDeclaration: 'function', ClassDeclaration: 'class', InterfaceDeclaration: 'interface', TypeAliasDeclaration: 'type', EnumDeclaration: 'enum', VariableDeclaration: 'const', MethodDeclaration: 'method', PropertyDeclaration: 'property', GetAccessor: 'getter', SetAccessor: 'setter', Constructor: 'constructor', ModuleDeclaration: 'module', ImportSpecifier: 'alias', ExportSpecifier: 'alias' };
86
+ if (kind === 'alias' || kind === 'enum member')
87
+ return kind === 'enum member' ? 'property' : kind;
88
+ return declarations[kind] ?? kind;
89
+ }
@@ -0,0 +1,23 @@
1
+ import type { Graph, GraphNode, RelationRow, TraversalResult, UnresolvedSite } from './types.js';
2
+ import type { Direction, EdgeKind } from './types.js';
3
+ export declare class GraphBuilder {
4
+ readonly nodes: Map<string, GraphNode>;
5
+ private readonly edges;
6
+ private readonly unresolved;
7
+ addNode(node: GraphNode): void;
8
+ addEdge(from: string, to: string, kind: EdgeKind, siteId: string): void;
9
+ addUnresolved(site: UnresolvedSite): void;
10
+ build(): Graph;
11
+ }
12
+ /** Remove excluded declarations, then retain only declarations still reachable from surviving seeds. */
13
+ export declare function graphAfterExclusion(graph: Graph, exclude: readonly string[], direction: Direction): Graph;
14
+ /** Keep returned rows, seeds, and every declaration connecting a seed to a returned row. */
15
+ export declare function graphForItems(graph: Graph, rowIds: ReadonlySet<string>, direction: Direction): Graph;
16
+ export declare function projectGraph(graph: Graph, direction: Direction, inGlobs: readonly string[]): TraversalResult;
17
+ export declare function mergeGraphs(graphs: readonly Graph[]): Graph;
18
+ export declare function rowsForGraph(graph: Graph, direction: Direction): RelationRow[];
19
+ export declare function traversalLinks(graph: Graph, allowed: ReadonlySet<string>, direction: Direction): {
20
+ children: Map<string, string[]>;
21
+ parents: Map<string, string[]>;
22
+ };
23
+ export declare function selectGraph(graph: Graph, ids: ReadonlySet<string>): Graph;
@@ -0,0 +1,115 @@
1
+ import { matchesPathFilters } from '../core/receipt.js';
2
+ export class GraphBuilder {
3
+ nodes = new Map();
4
+ edges = new Map();
5
+ unresolved = new Map();
6
+ addNode(node) {
7
+ const prior = this.nodes.get(node.id);
8
+ if (!prior || node.depth < prior.depth)
9
+ this.nodes.set(node.id, node);
10
+ }
11
+ addEdge(from, to, kind, siteId) {
12
+ const key = `${from}\0${to}\0${kind}`;
13
+ const edge = this.edges.get(key) ?? { from, to, kind, siteIds: new Set() };
14
+ edge.siteIds.add(siteId);
15
+ this.edges.set(key, edge);
16
+ }
17
+ addUnresolved(site) { this.unresolved.set(site.siteId, site); }
18
+ build() {
19
+ return {
20
+ nodes: [...this.nodes.values()].sort(compareNodes),
21
+ edges: [...this.edges.values()].map((edge) => ({ ...edge, siteIds: [...edge.siteIds].sort(), callSites: edge.siteIds.size })).sort(compareEdges),
22
+ unresolved: [...this.unresolved.values()].sort(compareUnresolved),
23
+ };
24
+ }
25
+ }
26
+ /** Remove excluded declarations, then retain only declarations still reachable from surviving seeds. */
27
+ export function graphAfterExclusion(graph, exclude, direction) {
28
+ const allowed = new Set(graph.nodes.filter((node) => matchesPathFilters(node.path, { exclude })).map((node) => node.id));
29
+ const links = traversalLinks(graph, allowed, direction);
30
+ const kept = new Set(graph.nodes.filter((node) => node.depth === 0 && allowed.has(node.id)).map((node) => node.id));
31
+ const queue = [...kept];
32
+ for (let index = 0; index < queue.length; index++)
33
+ for (const child of links.children.get(queue[index]) ?? [])
34
+ if (!kept.has(child)) {
35
+ kept.add(child);
36
+ queue.push(child);
37
+ }
38
+ return selectGraph(graph, kept);
39
+ }
40
+ /** Keep returned rows, seeds, and every declaration connecting a seed to a returned row. */
41
+ export function graphForItems(graph, rowIds, direction) {
42
+ const ids = new Set(graph.nodes.map((node) => node.id));
43
+ const links = traversalLinks(graph, ids, direction);
44
+ const kept = new Set(graph.nodes.filter((node) => node.depth === 0).map((node) => node.id));
45
+ const queue = [...rowIds].filter((id) => ids.has(id));
46
+ for (let index = 0; index < queue.length; index++) {
47
+ const id = queue[index];
48
+ if (kept.has(id))
49
+ continue;
50
+ kept.add(id);
51
+ for (const parent of links.parents.get(id) ?? [])
52
+ queue.push(parent);
53
+ }
54
+ const selected = selectGraph(graph, kept);
55
+ return { ...selected, nodes: selected.nodes.map((node) => node.depth === 0 || rowIds.has(node.id) ? withoutConnecting(node) : { ...node, connecting: true }) };
56
+ }
57
+ export function projectGraph(graph, direction, inGlobs) {
58
+ const preProjection = graph.nodes.filter((node) => node.depth > 0).length;
59
+ const rowIds = new Set(graph.nodes.filter((node) => node.depth > 0 && matchesPathFilters(node.path, { in: inGlobs })).map((node) => node.id));
60
+ const projected = graphForItems(graph, rowIds, direction);
61
+ const rows = rowsForGraph(projected, direction);
62
+ const unresolved = projected.unresolved.length;
63
+ const dispatch = new Set([
64
+ ...projected.unresolved.filter((site) => site.dispatch).map((site) => site.siteId),
65
+ ...projected.edges.filter((edge) => edge.kind === 'impl').flatMap((edge) => edge.siteIds),
66
+ ]).size;
67
+ return { graph: projected, rows, excluded: preProjection - rows.length, unresolved, dispatch };
68
+ }
69
+ export function mergeGraphs(graphs) {
70
+ const builder = new GraphBuilder();
71
+ for (const graph of graphs) {
72
+ for (const node of graph.nodes) {
73
+ const prior = builder.nodes.get(node.id);
74
+ const connecting = node.connecting && prior?.connecting;
75
+ builder.addNode(connecting ? { ...node, connecting: true } : withoutConnecting(node));
76
+ }
77
+ for (const edge of graph.edges)
78
+ for (const siteId of edge.siteIds)
79
+ builder.addEdge(edge.from, edge.to, edge.kind, siteId);
80
+ for (const site of graph.unresolved)
81
+ builder.addUnresolved(site);
82
+ }
83
+ return builder.build();
84
+ }
85
+ export function rowsForGraph(graph, direction) {
86
+ const rows = new Map();
87
+ for (const node of graph.nodes)
88
+ if (node.depth > 0 && !node.connecting)
89
+ rows.set(node.id, { ...node, callSites: 0, edgeKinds: [] });
90
+ for (const edge of graph.edges) {
91
+ const row = rows.get(direction === 'calls' ? edge.to : edge.from);
92
+ if (!row)
93
+ continue;
94
+ row.callSites += edge.callSites;
95
+ row.edgeKinds = [...new Set([...row.edgeKinds, edge.kind])];
96
+ }
97
+ return [...rows.values()].sort(compareRows);
98
+ }
99
+ export function traversalLinks(graph, allowed, direction) {
100
+ const children = new Map();
101
+ const parents = new Map();
102
+ const link = (parent, child) => { children.set(parent, [...(children.get(parent) ?? []), child]); parents.set(child, [...(parents.get(child) ?? []), parent]); };
103
+ for (const edge of graph.edges)
104
+ if (allowed.has(edge.from) && allowed.has(edge.to))
105
+ direction === 'calls' ? link(edge.from, edge.to) : link(edge.to, edge.from);
106
+ return { children, parents };
107
+ }
108
+ export function selectGraph(graph, ids) {
109
+ return { nodes: graph.nodes.filter((node) => ids.has(node.id)), edges: graph.edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to)), unresolved: graph.unresolved.filter((site) => ids.has(site.from)) };
110
+ }
111
+ function withoutConnecting(node) { const { connecting: _connecting, ...rest } = node; return rest; }
112
+ function compareNodes(a, b) { return a.depth - b.depth || a.id.localeCompare(b.id); }
113
+ function compareEdges(a, b) { return a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind); }
114
+ function compareUnresolved(a, b) { return a.siteId.localeCompare(b.siteId); }
115
+ function compareRows(a, b) { return a.depth - b.depth || a.path.localeCompare(b.path) || a.start.line - b.start.line || a.start.col - b.start.col; }
@@ -0,0 +1,3 @@
1
+ import type { Graph } from './types.js';
2
+ /** Pure graph-to-Mermaid renderer. */
3
+ export declare function renderGraph(graph: Graph): string;
@@ -0,0 +1,37 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ /** Pure graph-to-Mermaid renderer. */
4
+ export function renderGraph(graph) {
5
+ const lines = ['flowchart TD'];
6
+ const ids = new Map(graph.nodes.map((node) => [node.id, mermaidId(node.id)]));
7
+ const directories = new Map();
8
+ for (const node of graph.nodes) {
9
+ const directory = path.posix.dirname(node.path);
10
+ const nodes = directories.get(directory) ?? [];
11
+ nodes.push(node);
12
+ directories.set(directory, nodes);
13
+ }
14
+ for (const [directory, nodes] of [...directories].sort(([a], [b]) => a.localeCompare(b))) {
15
+ lines.push(` subgraph ${mermaidId(`directory:${directory}`)}["${escape(directory)}"]`);
16
+ for (const node of nodes.sort((a, b) => a.id.localeCompare(b.id)))
17
+ lines.push(` ${ids.get(node.id)}["${escape(node.qualifiedName)}<br/>${escape(node.id)}"]`);
18
+ lines.push(' end');
19
+ }
20
+ const connecting = graph.nodes.filter((node) => node.connecting).map((node) => ids.get(node.id));
21
+ if (connecting.length) {
22
+ lines.push(' classDef connecting stroke-dasharray: 5 5');
23
+ lines.push(` class ${connecting.join(',')} connecting`);
24
+ }
25
+ for (const edge of graph.edges) {
26
+ const label = `${edge.kind === 'impl' ? 'impl' : ''}${edge.callSites > 1 ? `${edge.kind === 'impl' ? ' ' : ''}${edge.callSites}` : ''}`;
27
+ lines.push(` ${ids.get(edge.from)} ${edge.kind === 'impl' ? '-.->' : '-->'}${label ? `|${label}|` : ''} ${ids.get(edge.to)}`);
28
+ }
29
+ const reasons = [...new Set(graph.unresolved.map((site) => site.reason))].sort();
30
+ for (const reason of reasons)
31
+ lines.push(` unresolved_${reason}["${reason}"]`);
32
+ for (const site of graph.unresolved)
33
+ lines.push(` ${ids.get(site.from)} -.-> unresolved_${site.reason}`);
34
+ return `${lines.join('\n')}\n`;
35
+ }
36
+ function mermaidId(value) { return `n_${createHash('sha256').update(value).digest('hex').slice(0, 16)}`; }
37
+ function escape(value) { return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
@@ -0,0 +1,20 @@
1
+ import type { DeclarationRecord } from '../core/declarations.js';
2
+ import type { SemanticContext } from '../server/context.js';
3
+ import { type Direction, type TraversalResult } from './types.js';
4
+ export interface ReferenceRow {
5
+ path: string;
6
+ line: number;
7
+ col: number;
8
+ kind: string;
9
+ qualifiedName: string;
10
+ role: string;
11
+ }
12
+ export declare function references(context: SemanticContext, record: DeclarationRecord, filters: {
13
+ in: readonly string[];
14
+ exclude: readonly string[];
15
+ }): Promise<ReferenceRow[]>;
16
+ export declare function implementations(context: SemanticContext, record: DeclarationRecord): Promise<DeclarationRecord[]>;
17
+ export declare function traverse(context: SemanticContext, seeds: readonly DeclarationRecord[], direction: Direction, depth: number, filters: {
18
+ in: readonly string[];
19
+ exclude: readonly string[];
20
+ }): Promise<TraversalResult>;