@jsleekr/graft 5.7.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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/analyzer/estimator.d.ts +33 -0
  4. package/dist/analyzer/estimator.js +273 -0
  5. package/dist/analyzer/graph-checker.d.ts +13 -0
  6. package/dist/analyzer/graph-checker.js +153 -0
  7. package/dist/analyzer/scope.d.ts +21 -0
  8. package/dist/analyzer/scope.js +324 -0
  9. package/dist/analyzer/types.d.ts +17 -0
  10. package/dist/analyzer/types.js +323 -0
  11. package/dist/codegen/agents.d.ts +2 -0
  12. package/dist/codegen/agents.js +109 -0
  13. package/dist/codegen/backend.d.ts +16 -0
  14. package/dist/codegen/backend.js +1 -0
  15. package/dist/codegen/claude-backend.d.ts +9 -0
  16. package/dist/codegen/claude-backend.js +47 -0
  17. package/dist/codegen/codegen.d.ts +10 -0
  18. package/dist/codegen/codegen.js +57 -0
  19. package/dist/codegen/hooks.d.ts +2 -0
  20. package/dist/codegen/hooks.js +165 -0
  21. package/dist/codegen/orchestration.d.ts +3 -0
  22. package/dist/codegen/orchestration.js +250 -0
  23. package/dist/codegen/settings.d.ts +36 -0
  24. package/dist/codegen/settings.js +87 -0
  25. package/dist/compiler.d.ts +21 -0
  26. package/dist/compiler.js +101 -0
  27. package/dist/constants.d.ts +9 -0
  28. package/dist/constants.js +13 -0
  29. package/dist/errors/diagnostics.d.ts +21 -0
  30. package/dist/errors/diagnostics.js +25 -0
  31. package/dist/format.d.ts +12 -0
  32. package/dist/format.js +46 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +181 -0
  35. package/dist/lexer/lexer.d.ts +23 -0
  36. package/dist/lexer/lexer.js +268 -0
  37. package/dist/lexer/tokens.d.ts +96 -0
  38. package/dist/lexer/tokens.js +150 -0
  39. package/dist/lsp/features/code-actions.d.ts +7 -0
  40. package/dist/lsp/features/code-actions.js +58 -0
  41. package/dist/lsp/features/completions.d.ts +7 -0
  42. package/dist/lsp/features/completions.js +271 -0
  43. package/dist/lsp/features/definition.d.ts +3 -0
  44. package/dist/lsp/features/definition.js +32 -0
  45. package/dist/lsp/features/diagnostics.d.ts +4 -0
  46. package/dist/lsp/features/diagnostics.js +33 -0
  47. package/dist/lsp/features/hover.d.ts +7 -0
  48. package/dist/lsp/features/hover.js +88 -0
  49. package/dist/lsp/features/index.d.ts +9 -0
  50. package/dist/lsp/features/index.js +9 -0
  51. package/dist/lsp/features/references.d.ts +7 -0
  52. package/dist/lsp/features/references.js +53 -0
  53. package/dist/lsp/features/rename.d.ts +17 -0
  54. package/dist/lsp/features/rename.js +198 -0
  55. package/dist/lsp/features/symbols.d.ts +7 -0
  56. package/dist/lsp/features/symbols.js +74 -0
  57. package/dist/lsp/features/utils.d.ts +3 -0
  58. package/dist/lsp/features/utils.js +65 -0
  59. package/dist/lsp/features.d.ts +20 -0
  60. package/dist/lsp/features.js +513 -0
  61. package/dist/lsp/server.d.ts +2 -0
  62. package/dist/lsp/server.js +327 -0
  63. package/dist/parser/ast.d.ts +244 -0
  64. package/dist/parser/ast.js +10 -0
  65. package/dist/parser/parser.d.ts +95 -0
  66. package/dist/parser/parser.js +1175 -0
  67. package/dist/program-index.d.ts +21 -0
  68. package/dist/program-index.js +74 -0
  69. package/dist/resolver/resolver.d.ts +9 -0
  70. package/dist/resolver/resolver.js +136 -0
  71. package/dist/runner.d.ts +13 -0
  72. package/dist/runner.js +41 -0
  73. package/dist/runtime/executor.d.ts +56 -0
  74. package/dist/runtime/executor.js +285 -0
  75. package/dist/runtime/expr-eval.d.ts +3 -0
  76. package/dist/runtime/expr-eval.js +138 -0
  77. package/dist/runtime/flow-runner.d.ts +21 -0
  78. package/dist/runtime/flow-runner.js +230 -0
  79. package/dist/runtime/memory.d.ts +5 -0
  80. package/dist/runtime/memory.js +41 -0
  81. package/dist/runtime/prompt-builder.d.ts +12 -0
  82. package/dist/runtime/prompt-builder.js +66 -0
  83. package/dist/runtime/subprocess.d.ts +20 -0
  84. package/dist/runtime/subprocess.js +99 -0
  85. package/dist/runtime/token-tracker.d.ts +36 -0
  86. package/dist/runtime/token-tracker.js +56 -0
  87. package/dist/runtime/transforms.d.ts +2 -0
  88. package/dist/runtime/transforms.js +104 -0
  89. package/dist/types.d.ts +10 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils.d.ts +3 -0
  92. package/dist/utils.js +35 -0
  93. package/dist/version.d.ts +1 -0
  94. package/dist/version.js +11 -0
  95. package/package.json +70 -0
@@ -0,0 +1,88 @@
1
+ import { MarkupKind } from 'vscode-languageserver/node';
2
+ import { BUILTIN_FUNCTIONS } from '../../parser/ast.js';
3
+ import { formatExpr } from '../../format.js';
4
+ export const KEYWORD_DOCS = {
5
+ context: 'Declares a context schema with typed fields and a max_tokens budget.\n\n```graft\ncontext Name(max_tokens: 1k) {\n field: Type\n}\n```',
6
+ node: 'Declares a processing node with model, budget, reads, writes, and produces.\n\n```graft\nnode Name(model: sonnet, budget: 5k/2k) {\n reads: [ContextName]\n produces Output { field: Type }\n}\n```',
7
+ memory: 'Declares persistent memory with typed fields and storage backend.\n\n```graft\nmemory Name(max_tokens: 2k, storage: file) {\n field: Type\n}\n```',
8
+ graph: 'Declares an execution graph connecting nodes in a flow.\n\n```graft\ngraph Name(input: In, output: Out, budget: 10k) {\n Start -> Middle -> done\n}\n```',
9
+ edge: 'Declares a data transform between nodes.\n\n```graft\nedge Source -> Target | select(field) | compact\n```',
10
+ import: 'Imports contexts and nodes from another .gft file.\n\n```graft\nimport { Name } from "./lib.gft"\n```',
11
+ reads: 'Specifies which contexts, produces, or memories a node reads from.\n\n```graft\nreads: [ContextName, Produces.field]\n```',
12
+ writes: 'Specifies which memories a node writes to.\n\n```graft\nwrites: [MemoryName.field]\n```',
13
+ produces: 'Declares the output schema of a node.\n\n```graft\nproduces OutputName {\n field: Type\n}\n```',
14
+ model: 'Specifies the LLM model alias for a node.\n\nAliases: sonnet, opus, haiku',
15
+ max_tokens: 'Sets the maximum token budget for a context or memory.',
16
+ on_failure: 'Specifies failure handling strategy for a node.\n\nStrategies: retry(N), fallback(Node), skip, abort',
17
+ storage: 'Specifies the storage backend for a memory declaration.\n\nCurrently supported: file',
18
+ foreach: 'Iterates over a list field from a node\'s output.\n\n```graft\nforeach(Node.output.field as item, max_iterations: 5) {\n Step1 -> Step2\n}\n```',
19
+ parallel: 'Executes multiple nodes concurrently.\n\n```graft\nparallel { Node1 Node2 Node3 }\n```',
20
+ };
21
+ export function getHoverInfo(word, index) {
22
+ const keywordDoc = KEYWORD_DOCS[word];
23
+ if (keywordDoc) {
24
+ return mkHover(keywordDoc);
25
+ }
26
+ const ctx = index.contextMap.get(word);
27
+ if (ctx) {
28
+ const fields = ctx.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
29
+ return mkHover(`**context** ${ctx.name} (max_tokens: ${ctx.maxTokens})\n\`\`\`\n${fields}\n\`\`\``);
30
+ }
31
+ const node = index.nodeMap.get(word);
32
+ if (node) {
33
+ const reads = node.reads.map(r => {
34
+ if (!r.field)
35
+ return r.context;
36
+ return r.field.length === 1 ? `${r.context}.${r.field[0]}` : `${r.context}.{${r.field.join(', ')}}`;
37
+ }).join(', ');
38
+ const writes = node.writes.length > 0
39
+ ? `\nwrites: ${node.writes.map(w => w.field ? `${w.memory}.${w.field}` : w.memory).join(', ')}`
40
+ : '';
41
+ const producesFields = node.produces.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
42
+ return mkHover(`**node** ${node.name}\n` +
43
+ `- model: ${node.model}\n` +
44
+ `- budget: ${node.budgetIn}/${node.budgetOut}\n` +
45
+ `- reads: ${reads}${writes}\n` +
46
+ `- produces: ${node.produces.name}\n\`\`\`\n${producesFields}\n\`\`\``);
47
+ }
48
+ const mem = index.memoryMap.get(word);
49
+ if (mem) {
50
+ const fields = mem.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
51
+ return mkHover(`**memory** ${mem.name} (max_tokens: ${mem.maxTokens}, storage: ${mem.storage})\n\`\`\`\n${fields}\n\`\`\``);
52
+ }
53
+ // Built-in functions
54
+ if (word in BUILTIN_FUNCTIONS) {
55
+ const info = BUILTIN_FUNCTIONS[word];
56
+ return mkHover(`**${info.signature}**\n\n${info.description}`);
57
+ }
58
+ const producerNode = index.producesNodeMap.get(word);
59
+ if (producerNode) {
60
+ const fields = producerNode.produces.fields.map(f => ` ${f.name}: ${formatType(f.type)}`).join('\n');
61
+ return mkHover(`**produces** ${word} (from node ${producerNode.name})\n\`\`\`\n${fields}\n\`\`\``);
62
+ }
63
+ // Let binding variables
64
+ const letBinding = index.letBindingMap.get(word);
65
+ if (letBinding) {
66
+ const exprStr = formatExpr(letBinding.value);
67
+ return mkHover(`**let** ${word} = ${exprStr}\n\n(in graph ${letBinding.graphName})`);
68
+ }
69
+ return null;
70
+ }
71
+ export function formatType(type) {
72
+ switch (type.kind) {
73
+ case 'primitive': return type.name;
74
+ case 'primitive_range': return `Float(${type.min}..${type.max})`;
75
+ case 'list': return `List<${formatType(type.element)}>`;
76
+ case 'map': return `Map<${formatType(type.key)}, ${formatType(type.value)}>`;
77
+ case 'optional': return `${formatType(type.inner)}?`;
78
+ case 'token_bounded': return `${formatType(type.inner)}(max: ${type.max})`;
79
+ case 'enum': return type.values.join(' | ');
80
+ case 'struct': return `{ ${type.fields.map(f => `${f.name}: ${formatType(f.type)}`).join(', ')} }`;
81
+ case 'domain': return type.name;
82
+ }
83
+ }
84
+ function mkHover(value) {
85
+ return { contents: { kind: MarkupKind.Markdown, value } };
86
+ }
87
+ // formatExpr moved to src/format.ts in v5.0-R1
88
+ export { formatExpr } from '../../format.js';
@@ -0,0 +1,9 @@
1
+ export { toDiagnostics, extractUndefinedName } from './diagnostics.js';
2
+ export { getHoverInfo, KEYWORD_DOCS, formatType } from './hover.js';
3
+ export { getCompletions } from './completions.js';
4
+ export { getDefinitionLocation } from './definition.js';
5
+ export { getDocumentSymbols, makeSymbol } from './symbols.js';
6
+ export { buildAutoImportActions, buildAutoImportEdit, computeRelativeImportPath } from './code-actions.js';
7
+ export { isRenameable, collectRenameLocations, buildRenameEdits, GRAFT_KEYWORDS } from './rename.js';
8
+ export { isReferable, findReferences } from './references.js';
9
+ export { getWordAtPosition, isInComment, isInString } from './utils.js';
@@ -0,0 +1,9 @@
1
+ export { toDiagnostics, extractUndefinedName } from './diagnostics.js';
2
+ export { getHoverInfo, KEYWORD_DOCS, formatType } from './hover.js';
3
+ export { getCompletions } from './completions.js';
4
+ export { getDefinitionLocation } from './definition.js';
5
+ export { getDocumentSymbols, makeSymbol } from './symbols.js';
6
+ export { buildAutoImportActions, buildAutoImportEdit, computeRelativeImportPath } from './code-actions.js';
7
+ export { isRenameable, collectRenameLocations, buildRenameEdits, GRAFT_KEYWORDS } from './rename.js';
8
+ export { isReferable, findReferences } from './references.js';
9
+ export { getWordAtPosition, isInComment, isInString } from './utils.js';
@@ -0,0 +1,7 @@
1
+ import type { Location } from 'vscode-languageserver/node';
2
+ import type { ProgramIndex } from '../../program-index.js';
3
+ export declare function isReferable(word: string, index: ProgramIndex): boolean;
4
+ export declare function findReferences(name: string, currentDocText: string, currentDocUri: string, currentIndex: ProgramIndex, includeDeclaration: boolean, workspaceFiles: Map<string, {
5
+ text: string;
6
+ uri: string;
7
+ }>): Location[];
@@ -0,0 +1,53 @@
1
+ import { collectRenameLocations } from './rename.js';
2
+ export function isReferable(word, index) {
3
+ return (index.contextMap.has(word) ||
4
+ index.nodeMap.has(word) ||
5
+ index.memoryMap.has(word) ||
6
+ index.graphMap.has(word) ||
7
+ index.producesNodeMap.has(word));
8
+ }
9
+ /**
10
+ * Find the declaration name's 0-based position from ProgramIndex.
11
+ * Declaration locations point to the keyword token (e.g., "context" in "context Foo").
12
+ * The name starts at keyword column + keyword length + 1 (for the space).
13
+ * Uses location.length from SourceLocation rather than hardcoded keyword lengths.
14
+ */
15
+ function findDeclNamePosition(name, index) {
16
+ const decl = index.contextMap.get(name) ?? index.nodeMap.get(name) ??
17
+ index.memoryMap.get(name) ?? index.graphMap.get(name);
18
+ if (decl && decl.location.length != null) {
19
+ return {
20
+ line: decl.location.line - 1,
21
+ character: decl.location.column - 1 + decl.location.length + 1,
22
+ };
23
+ }
24
+ const prodNode = index.producesNodeMap.get(name);
25
+ if (prodNode && prodNode.produces.location.length != null) {
26
+ return {
27
+ line: prodNode.produces.location.line - 1,
28
+ character: prodNode.produces.location.column - 1 + prodNode.produces.location.length + 1,
29
+ };
30
+ }
31
+ return null;
32
+ }
33
+ export function findReferences(name, currentDocText, currentDocUri, currentIndex, includeDeclaration, workspaceFiles) {
34
+ const locations = [];
35
+ // Declaration position for filtering when includeDeclaration is false
36
+ const declPos = includeDeclaration ? null : findDeclNamePosition(name, currentIndex);
37
+ // Current file
38
+ for (const range of collectRenameLocations(currentDocText, name)) {
39
+ if (declPos &&
40
+ range.start.line === declPos.line &&
41
+ range.start.character === declPos.character) {
42
+ continue; // Skip declaration
43
+ }
44
+ locations.push({ uri: currentDocUri, range });
45
+ }
46
+ // Cross-file references
47
+ for (const [, fileInfo] of workspaceFiles) {
48
+ for (const range of collectRenameLocations(fileInfo.text, name)) {
49
+ locations.push({ uri: fileInfo.uri, range });
50
+ }
51
+ }
52
+ return locations;
53
+ }
@@ -0,0 +1,17 @@
1
+ import type { Range, TextEdit } from 'vscode-languageserver/node';
2
+ import { ProgramIndex } from '../../program-index.js';
3
+ export declare const GRAFT_KEYWORDS: Set<string>;
4
+ /**
5
+ * Pure function that builds rename edits for a Graft identifier.
6
+ * Returns { changes } on success, { error } on validation/conflict failure, or null if not renameable.
7
+ */
8
+ export declare function buildRenameEdits(oldName: string, newName: string, docText: string, docUri: string, currentFilePath: string, workspaceFiles: Map<string, {
9
+ text: string;
10
+ uri: string;
11
+ }>): {
12
+ changes: Record<string, TextEdit[]>;
13
+ } | {
14
+ error: string;
15
+ } | null;
16
+ export declare function isRenameable(word: string, index: ProgramIndex): boolean;
17
+ export declare function collectRenameLocations(docText: string, name: string): Range[];
@@ -0,0 +1,198 @@
1
+ import { Lexer } from '../../lexer/lexer.js';
2
+ import { Parser } from '../../parser/parser.js';
3
+ import { ProgramIndex } from '../../program-index.js';
4
+ import { KEYWORDS } from '../../lexer/tokens.js';
5
+ import { isInComment, isInString } from './utils.js';
6
+ const TYPE_KEYWORDS = new Set([
7
+ 'String', 'Int', 'Float', 'Bool', 'List', 'Map', 'Optional',
8
+ 'TokenBounded', 'FilePath', 'FileDiff', 'TestFile', 'IssueRef',
9
+ ]);
10
+ const CONTEXTUAL_KEYWORDS = new Set(['output']);
11
+ // Derived from lexer KEYWORDS, excluding type keywords and contextual keywords
12
+ export const GRAFT_KEYWORDS = new Set(Object.keys(KEYWORDS).filter(k => !TYPE_KEYWORDS.has(k) && !CONTEXTUAL_KEYWORDS.has(k)));
13
+ /**
14
+ * Pure function that builds rename edits for a Graft identifier.
15
+ * Returns { changes } on success, { error } on validation/conflict failure, or null if not renameable.
16
+ */
17
+ export function buildRenameEdits(oldName, newName, docText, docUri, currentFilePath, workspaceFiles) {
18
+ // Parse current file to check renameability
19
+ let index;
20
+ try {
21
+ const tokens = new Lexer(docText).tokenize();
22
+ const { program } = new Parser(tokens).parse();
23
+ index = new ProgramIndex(program);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ // Check if the name is renameable (is a declared context/node/memory/graph)
29
+ if (!isRenameable(oldName, index))
30
+ return null;
31
+ // Validate newName is a legal Graft identifier
32
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(newName)) {
33
+ return { error: `'${newName}' is not a valid identifier` };
34
+ }
35
+ // Reject Graft keywords
36
+ if (GRAFT_KEYWORDS.has(newName)) {
37
+ return { error: `'${newName}' is a reserved keyword` };
38
+ }
39
+ // Check for conflicts in current file
40
+ if (oldName !== newName && (index.contextMap.has(newName) ||
41
+ index.nodeMap.has(newName) ||
42
+ index.memoryMap.has(newName) ||
43
+ index.graphMap.has(newName))) {
44
+ return { error: `'${newName}' already exists in the current file` };
45
+ }
46
+ // Check for field name collisions
47
+ const fieldCollision = findFieldCollision(newName, index);
48
+ if (fieldCollision) {
49
+ return { error: fieldCollision };
50
+ }
51
+ // Check for conflicts in importing files (parse-based)
52
+ for (const [filePath, fileInfo] of workspaceFiles) {
53
+ try {
54
+ const wsTokens = new Lexer(fileInfo.text).tokenize();
55
+ const { program: wsProgram } = new Parser(wsTokens).parse();
56
+ const wsIndex = new ProgramIndex(wsProgram);
57
+ if (wsIndex.contextMap.has(newName) || wsIndex.nodeMap.has(newName) ||
58
+ wsIndex.memoryMap.has(newName) || wsIndex.graphMap.has(newName)) {
59
+ return { error: `Name "${newName}" conflicts with declaration in ${filePath}` };
60
+ }
61
+ }
62
+ catch {
63
+ // Parse failure -- skip conflict check for this file
64
+ }
65
+ }
66
+ const changes = {};
67
+ // Collect locations in current file
68
+ const currentLocs = collectRenameLocations(docText, oldName);
69
+ if (currentLocs.length > 0) {
70
+ changes[docUri] = currentLocs.map(range => ({ range, newText: newName }));
71
+ }
72
+ // Collect locations in importing files
73
+ for (const [, fileInfo] of workspaceFiles) {
74
+ const locs = collectRenameLocations(fileInfo.text, oldName);
75
+ if (locs.length > 0) {
76
+ changes[fileInfo.uri] = locs.map(range => ({ range, newText: newName }));
77
+ }
78
+ }
79
+ return { changes };
80
+ }
81
+ export function isRenameable(word, index) {
82
+ return (index.contextMap.has(word) ||
83
+ index.nodeMap.has(word) ||
84
+ index.memoryMap.has(word) ||
85
+ index.graphMap.has(word));
86
+ }
87
+ export function collectRenameLocations(docText, name) {
88
+ // Normalize CRLF to LF
89
+ docText = docText.replace(/\r\n/g, '\n');
90
+ // Parse document to extract import path ranges for AST-based filtering
91
+ const importPathRanges = getImportPathRanges(docText);
92
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93
+ const regex = new RegExp(`\\b${escaped}\\b`, 'g');
94
+ const ranges = [];
95
+ const lines = docText.split('\n');
96
+ const lineOffsets = [];
97
+ let offset = 0;
98
+ for (const line of lines) {
99
+ lineOffsets.push(offset);
100
+ offset += line.length + 1;
101
+ }
102
+ let match;
103
+ while ((match = regex.exec(docText)) !== null) {
104
+ const matchOffset = match.index;
105
+ let lo = 0;
106
+ let hi = lineOffsets.length - 1;
107
+ while (lo < hi) {
108
+ const mid = (lo + hi + 1) >> 1;
109
+ if (lineOffsets[mid] <= matchOffset)
110
+ lo = mid;
111
+ else
112
+ hi = mid - 1;
113
+ }
114
+ const line = lo;
115
+ const character = matchOffset - lineOffsets[line];
116
+ // Skip matches inside comments
117
+ if (isInComment(lines, line, character + 1))
118
+ continue;
119
+ // Skip matches inside strings
120
+ const lineText = lines[line];
121
+ if (isInString(lineText, character))
122
+ continue;
123
+ // Skip matches inside import path strings (AST-based)
124
+ if (isInImportPath(matchOffset, match[0].length, importPathRanges))
125
+ continue;
126
+ ranges.push({
127
+ start: { line, character },
128
+ end: { line, character: character + name.length },
129
+ });
130
+ }
131
+ return ranges;
132
+ }
133
+ /**
134
+ * Parse the document and return offset ranges for all import path strings.
135
+ * Each range is [start, end) covering the path string content (inside quotes).
136
+ */
137
+ function getImportPathRanges(docText) {
138
+ const ranges = [];
139
+ try {
140
+ const tokens = new Lexer(docText).tokenize();
141
+ const { program } = new Parser(tokens).parse();
142
+ for (const imp of program.imports) {
143
+ // Find the import path string in the source text after the import location
144
+ // The path is a string literal like "path/to/file" in `from "path/to/file"`
145
+ const searchStart = imp.location.offset;
146
+ const fromIdx = docText.indexOf('from', searchStart);
147
+ if (fromIdx === -1)
148
+ continue;
149
+ const quoteStart = docText.indexOf('"', fromIdx);
150
+ if (quoteStart === -1)
151
+ continue;
152
+ const quoteEnd = docText.indexOf('"', quoteStart + 1);
153
+ if (quoteEnd === -1)
154
+ continue;
155
+ // Range covers content inside quotes (exclusive of quotes themselves)
156
+ ranges.push({ start: quoteStart + 1, end: quoteEnd });
157
+ }
158
+ }
159
+ catch {
160
+ // If parsing fails, return empty ranges (no import filtering)
161
+ }
162
+ return ranges;
163
+ }
164
+ function isInImportPath(matchOffset, matchLength, importPathRanges) {
165
+ for (const range of importPathRanges) {
166
+ if (matchOffset >= range.start && matchOffset + matchLength <= range.end) {
167
+ return true;
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+ function findFieldCollision(newName, index) {
173
+ // Check context fields
174
+ for (const [ctxName, ctx] of index.contextMap) {
175
+ for (const field of ctx.fields) {
176
+ if (field.name === newName) {
177
+ return `'${newName}' collides with field '${newName}' in context '${ctxName}'`;
178
+ }
179
+ }
180
+ }
181
+ // Check memory fields
182
+ for (const [memName, mem] of index.memoryMap) {
183
+ for (const field of mem.fields) {
184
+ if (field.name === newName) {
185
+ return `'${newName}' collides with field '${newName}' in memory '${memName}'`;
186
+ }
187
+ }
188
+ }
189
+ // Check produces fields (node output fields)
190
+ for (const [nodeName, node] of index.nodeMap) {
191
+ for (const field of node.produces.fields) {
192
+ if (field.name === newName) {
193
+ return `'${newName}' collides with field '${newName}' in node '${nodeName}'`;
194
+ }
195
+ }
196
+ }
197
+ return null;
198
+ }
@@ -0,0 +1,7 @@
1
+ import type { DocumentSymbol } from 'vscode-languageserver/node';
2
+ import { SymbolKind } from 'vscode-languageserver/node';
3
+ import type { SourceLocation } from '../../errors/diagnostics.js';
4
+ import type { Program } from '../../parser/ast.js';
5
+ import type { ProgramIndex } from '../../program-index.js';
6
+ export declare function getDocumentSymbols(program: Program, index: ProgramIndex): DocumentSymbol[];
7
+ export declare function makeSymbol(name: string, kind: SymbolKind, loc: SourceLocation): DocumentSymbol;
@@ -0,0 +1,74 @@
1
+ import { SymbolKind } from 'vscode-languageserver/node';
2
+ export function getDocumentSymbols(program, index) {
3
+ const symbols = [];
4
+ for (const ctx of program.contexts) {
5
+ const sym = makeSymbol(ctx.name, SymbolKind.Class, ctx.location);
6
+ sym.children = makeFieldChildren(ctx.fields);
7
+ symbols.push(sym);
8
+ }
9
+ for (const node of program.nodes) {
10
+ const sym = makeSymbol(node.name, SymbolKind.Function, node.location);
11
+ sym.children = makeFieldChildren(node.produces.fields);
12
+ symbols.push(sym);
13
+ }
14
+ for (const mem of program.memories) {
15
+ const sym = makeSymbol(mem.name, SymbolKind.Variable, mem.location);
16
+ sym.children = makeFieldChildren(mem.fields);
17
+ symbols.push(sym);
18
+ }
19
+ for (const graph of program.graphs) {
20
+ const sym = makeSymbol(graph.name, SymbolKind.Module, graph.location);
21
+ sym.children = makeFlowNodeChildren(graph.flow, graph.location);
22
+ symbols.push(sym);
23
+ }
24
+ for (const edge of program.edges) {
25
+ const targetName = edge.target.kind === 'direct' ? edge.target.node : 'conditional';
26
+ symbols.push(makeSymbol(`${edge.source} -> ${targetName}`, SymbolKind.Event, edge.location));
27
+ }
28
+ return symbols;
29
+ }
30
+ function makeFieldChildren(fields) {
31
+ return fields.map(f => makeSymbol(f.name, SymbolKind.Field, f.location));
32
+ }
33
+ function makeFlowNodeChildren(flow, parentLoc) {
34
+ const children = [];
35
+ for (const node of flow) {
36
+ if (node.kind === 'node') {
37
+ children.push(makeSymbol(node.name, SymbolKind.Function, node.location ?? parentLoc));
38
+ }
39
+ else if (node.kind === 'parallel') {
40
+ const label = `parallel(${node.branches.join(', ')})`;
41
+ children.push(makeSymbol(label, SymbolKind.Function, node.location ?? parentLoc));
42
+ }
43
+ else if (node.kind === 'foreach') {
44
+ const label = `foreach(${node.source}.${node.field})`;
45
+ const sym = makeSymbol(label, SymbolKind.Function, node.location ?? parentLoc);
46
+ sym.children = makeFlowNodeChildren(node.body, node.location ?? parentLoc);
47
+ children.push(sym);
48
+ }
49
+ else if (node.kind === 'let') {
50
+ children.push(makeSymbol(`let ${node.name}`, SymbolKind.Variable, node.location ?? parentLoc));
51
+ }
52
+ else if (node.kind === 'graph_call') {
53
+ const label = `${node.name}(${node.args.map(a => a.name).join(', ')})`;
54
+ children.push(makeSymbol(label, SymbolKind.Function, node.location ?? parentLoc));
55
+ }
56
+ }
57
+ return children;
58
+ }
59
+ export function makeSymbol(name, kind, loc) {
60
+ const line = Math.max(0, loc.line - 1);
61
+ const character = Math.max(0, loc.column - 1);
62
+ // range spans from keyword start to past the name (keyword + space + name)
63
+ const rangeEnd = loc.length != null ? loc.length + 1 + name.length : name.length;
64
+ const range = {
65
+ start: { line, character },
66
+ end: { line, character: character + rangeEnd },
67
+ };
68
+ const nameStart = loc.length != null ? character + loc.length + 1 : character;
69
+ const selectionRange = {
70
+ start: { line, character: nameStart },
71
+ end: { line, character: nameStart + name.length },
72
+ };
73
+ return { name, kind, range, selectionRange };
74
+ }
@@ -0,0 +1,3 @@
1
+ export declare function isInComment(lines: string[], line: number, character: number): boolean;
2
+ export declare function isInString(lineText: string, character: number): boolean;
3
+ export declare function getWordAtPosition(text: string, line: number, character: number): string | null;
@@ -0,0 +1,65 @@
1
+ export function isInComment(lines, line, character) {
2
+ let inBlock = false;
3
+ for (let i = 0; i <= line; i++) {
4
+ const l = (lines[i] ?? '').replace(/\r$/, '');
5
+ const endCol = i === line ? character : l.length;
6
+ let j = 0;
7
+ while (j < endCol) {
8
+ if (!inBlock) {
9
+ if (l[j] === '/' && j + 1 < l.length && l[j + 1] === '/') {
10
+ if (i === line)
11
+ return true;
12
+ break;
13
+ }
14
+ if (l[j] === '/' && j + 1 < l.length && l[j + 1] === '*') {
15
+ inBlock = true;
16
+ j += 2;
17
+ continue;
18
+ }
19
+ if (l[j] === '"') {
20
+ j++;
21
+ while (j < endCol && l[j] !== '"')
22
+ j++;
23
+ if (j < endCol)
24
+ j++;
25
+ continue;
26
+ }
27
+ }
28
+ else {
29
+ if (l[j] === '*' && j + 1 < l.length && l[j + 1] === '/') {
30
+ inBlock = false;
31
+ j += 2;
32
+ continue;
33
+ }
34
+ }
35
+ j++;
36
+ }
37
+ }
38
+ return inBlock;
39
+ }
40
+ export function isInString(lineText, character) {
41
+ let inStr = false;
42
+ for (let i = 0; i < character && i < lineText.length; i++) {
43
+ if (lineText[i] === '"')
44
+ inStr = !inStr;
45
+ }
46
+ return inStr;
47
+ }
48
+ export function getWordAtPosition(text, line, character) {
49
+ const lines = text.split('\n');
50
+ if (line < 0 || line >= lines.length)
51
+ return null;
52
+ const lineText = lines[line];
53
+ if (character < 0 || character > lineText.length)
54
+ return null;
55
+ const pattern = /[A-Za-z_][A-Za-z0-9_]*/g;
56
+ let match;
57
+ while ((match = pattern.exec(lineText)) !== null) {
58
+ const start = match.index;
59
+ const end = start + match[0].length;
60
+ if (character >= start && character < end) {
61
+ return match[0];
62
+ }
63
+ }
64
+ return null;
65
+ }
@@ -0,0 +1,20 @@
1
+ import type { Diagnostic, Hover, Location, CompletionItem, DocumentSymbol } from 'vscode-languageserver/node';
2
+ import type { GraftError } from '../errors/diagnostics.js';
3
+ import type { Program, TypeExpr } from '../parser/ast.js';
4
+ import type { ProgramIndex } from '../program-index.js';
5
+ export declare function toDiagnostics(errors: GraftError[], warnings: GraftError[]): Diagnostic[];
6
+ export declare function getDocumentSymbols(program: Program, index: ProgramIndex): DocumentSymbol[];
7
+ export declare function getWordAtPosition(text: string, line: number, character: number): string | null;
8
+ export declare function getHoverInfo(word: string, index: ProgramIndex): Hover | null;
9
+ export declare function getDefinitionLocation(word: string, index: ProgramIndex, currentUri: string): Location | null;
10
+ export declare function formatType(type: TypeExpr): string;
11
+ export declare function getCompletions(text: string, line: number, character: number, cache: {
12
+ program: Program;
13
+ index: ProgramIndex;
14
+ } | null, resolveImportNames?: (importPath: string) => string[]): CompletionItem[];
15
+ export declare function extractUndefinedName(message: string, docText: string, line: number, character: number): string | null;
16
+ export declare function buildAutoImportEdit(name: string, fromPath: string, docText: string): {
17
+ insertLine: number;
18
+ newText: string;
19
+ };
20
+ export declare function computeRelativeImportPath(fromFile: string, toFile: string): string;