@kentwynn/kgraph 0.2.21 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,39 @@
1
- # KGraph
2
-
3
- Persistent repository intelligence for AI coding tools.
1
+ <div align="center">
2
+
3
+ <img src="media/logo.svg" alt="KGraph Atom Core Logo" width="140" height="140">
4
+
5
+ # KGraph: Persistent repository intelligence for AI coding tools.
6
+
7
+ <strong>atoms · evidence · context packs</strong>
8
+
9
+ <p>
10
+ <a href="https://www.npmjs.com/package/@kentwynn/kgraph">
11
+ <img src="https://img.shields.io/npm/v/@kentwynn/kgraph?label=npm" alt="npm version">
12
+ </a>
13
+ <a href="https://github.com/kentwynn/KGraph/releases/latest">
14
+ <img src="https://img.shields.io/github/v/release/kentwynn/KGraph?label=release" alt="Latest release">
15
+ </a>
16
+ <a href="https://github.com/kentwynn/KGraph/actions/workflows/ci.yml">
17
+ <img src="https://img.shields.io/github/actions/workflow/status/kentwynn/KGraph/ci.yml?branch=main&label=ci" alt="CI">
18
+ </a>
19
+ <a href="https://github.com/kentwynn/KGraph/blob/main/LICENSE">
20
+ <img src="https://img.shields.io/github/license/kentwynn/KGraph" alt="License">
21
+ </a>
22
+ <a href="https://nodejs.org/">
23
+ <img src="https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white" alt="Node.js >=20">
24
+ </a>
25
+ <a href="https://github.com/kentwynn/KGraph/security/policy">
26
+ <img src="https://img.shields.io/badge/security-policy-0f766e" alt="Security policy">
27
+ </a>
28
+ <a href="https://github.com/kentwynn/KGraph/wiki">
29
+ <img src="https://img.shields.io/badge/docs-wiki-blue" alt="Documentation">
30
+ </a>
31
+ <a href="https://github.com/kentwynn/KGraph/stargazers">
32
+ <img src="https://img.shields.io/github/stars/kentwynn/KGraph?style=social" alt="GitHub stars">
33
+ </a>
34
+ </p>
35
+
36
+ </div>
4
37
 
5
38
  KGraph gives Codex, GitHub Copilot, Cursor, Claude Code, Gemini CLI, Windsurf, and Cline a local knowledge layer for your repo: file maps, symbols, imports, relationships, and durable knowledge atoms from previous AI sessions. The goal is simple: your assistant should not spend every session re-learning the same codebase.
6
39
 
@@ -8,22 +41,33 @@ The CLI presents this as **Atom Core**: lightweight local atoms plus determinist
8
41
 
9
42
  ## The Workflow
10
43
 
11
- Use KGraph in two steps:
44
+ Use KGraph with one setup command and one normal daily command:
12
45
 
13
46
  ```bash
14
- # Required once per repository
15
- kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline
47
+ # Required once per repository.
48
+ # Creates .kgraph/, runs the first scan, and detects likely AI tools.
49
+ kgraph init
16
50
 
17
- # Normal daily command
51
+ # Normal daily command.
52
+ # Refreshes maps, processes pending capture notes, and returns focused context.
18
53
  kgraph "auth token refresh"
19
54
  ```
20
55
 
21
- That second command runs the full practical workflow:
56
+ AI tool integrations are optional. During `kgraph init`, KGraph detects likely local tools such as Codex, Copilot, Claude Code, and Gemini, then prompts or prints a suggested `kgraph integrate add ...` command. You can accept the recommendation, choose custom integrations, skip the step, or configure them later.
57
+
58
+ If you already know exactly which integrations you want, you can pass them during init:
59
+
60
+ ```bash
61
+ kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline
62
+ ```
63
+
64
+ The daily command runs the full practical workflow:
22
65
 
23
66
  1. Refreshes the repository scan.
24
67
  2. Updates file, symbol, import, and relationship maps.
25
68
  3. Processes any Markdown capture notes waiting in `.kgraph/inbox/` into knowledge atoms.
26
- 4. Returns compact context for the topic you asked about.
69
+ 4. Reports memory health and actionable next steps.
70
+ 5. Returns compact context for the topic you asked about.
27
71
 
28
72
  You can also run just:
29
73
 
@@ -101,10 +145,11 @@ KGraph's core functionality is free and local-first. It does not require account
101
145
  From the root of a repository:
102
146
 
103
147
  ```bash
104
- # 1. Create the local KGraph workspace
148
+ # 1. Create the local KGraph workspace and run the first scan
105
149
  kgraph init
106
150
 
107
- # 2. Optional: connect AI tools so they know the KGraph workflow
151
+ # 2. Optional: accept detected AI tool recommendations during init,
152
+ # or add integrations later when you want KGraph-managed instructions
108
153
  kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
109
154
 
110
155
  # 3. Run the normal workflow for a topic
@@ -114,7 +159,7 @@ kgraph "auth token refresh"
114
159
  kgraph doctor
115
160
  ```
116
161
 
117
- `kgraph init` now scans once, then prints relevant next steps. When KGraph can detect likely AI tools on the machine, it recommends matching integrations.
162
+ `kgraph init` scans once, prints repo language coverage, detects likely local AI tools, and recommends matching integrations. Integrations are still optional: they only write local instruction files so tools know when to run KGraph. They do not start background agents or call AI providers.
118
163
 
119
164
  After useful AI work, assistants save durable runtime-capture notes into `.kgraph/inbox/`. These notes are not project documentation; they are KGraph input files that the next `kgraph` run processes automatically. You can also process them directly with `kgraph update`.
120
165
 
@@ -150,13 +195,13 @@ This is optional. Claude Code can use generated hook scripts for automatic captu
150
195
  kgraph init
151
196
  ```
152
197
 
153
- Required once per repo. Creates `.kgraph/`, writes the local config, runs the first scan, and prints suggested next actions based on the detected repo languages and likely local AI tools.
198
+ Required once per repo. Creates `.kgraph/`, writes the local config, initializes the knowledge store, runs the first scan, detects likely local AI tools, and prints suggested next actions based on repo languages and detected tools.
154
199
 
155
200
  ```bash
156
201
  kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline
157
202
  ```
158
203
 
159
- Initializes KGraph and writes local instruction files for supported AI tools.
204
+ Initializes KGraph and immediately writes local instruction files for the named AI tools. This is optional; plain `kgraph init` can detect likely tools and recommend or prompt for integrations instead.
160
205
 
161
206
  ```bash
162
207
  kgraph "some topic"
@@ -323,6 +368,10 @@ Show processed capture history. Add a query to find historical work by title, su
323
368
 
324
369
  KGraph integrations are local files. They do not start background agents, call AI providers, or send data anywhere.
325
370
 
371
+ You do not need integrations to use KGraph manually. They are useful when you want Codex, Copilot, Cursor, Claude Code, Gemini, Windsurf, or Cline to see repo-local KGraph workflow instructions automatically.
372
+
373
+ `kgraph init` detects likely local tools and recommends integrations when possible. You can also manage them explicitly:
374
+
326
375
  ```bash
327
376
  kgraph integrate add codex copilot cursor claude-code gemini windsurf cline
328
377
  kgraph integrate add copilot --mode smart
@@ -391,9 +440,17 @@ KGraph deeply scans:
391
440
  - Java and Kotlin
392
441
  - C and C++
393
442
  - C#
443
+ - PHP
444
+ - Ruby
445
+ - Shell
446
+ - SQL
394
447
 
395
448
  Other languages keep practical file, import, and symbol depth without full call graph analysis. Common file types still appear in the file map with generic metadata, so context queries can still point to docs, config, SQL, CSS, HTML, YAML, and similar files.
396
449
 
450
+ KGraph also extracts basic symbols/imports for Swift, Terraform/HCL,
451
+ GraphQL, Protocol Buffers, Lua, Dart, Elixir, Scala, and R. Structured file extraction covers
452
+ YAML, JSON, TOML, Dockerfile/Containerfile, Markdown/MDX, HTML, CSS, SCSS, Sass, Less, and XML.
453
+
397
454
  ## Visualization
398
455
 
399
456
  ```bash
package/dist/cli/help.js CHANGED
@@ -20,7 +20,7 @@ export function renderRootHelp(useColor = supportsColor()) {
20
20
  '',
21
21
  sectionTitle(theme, `${accent} Start`),
22
22
  command('init', 'Required once: create .kgraph/ workspace'),
23
- command('init --integrations codex,gemini', 'Initialize and connect AI tools'),
23
+ command('init --integrations codex,gemini', 'Optional: initialize and connect named AI tools'),
24
24
  '',
25
25
  sectionTitle(theme, `${accent} Daily workflow`),
26
26
  command('kgraph', 'Refresh scan maps and process pending capture notes'),
@@ -68,7 +68,8 @@ export function renderRootHelp(useColor = supportsColor()) {
68
68
  command('--capture-symbol <name>', 'Attach symbol evidence to root capture'),
69
69
  '',
70
70
  sectionTitle(theme, `${accent} Examples`),
71
- ' kgraph init --integrations codex,copilot,cursor,claude-code,gemini,windsurf,cline',
71
+ ' kgraph init',
72
+ ' kgraph integrate add codex copilot cursor claude-code gemini windsurf cline',
72
73
  ' kgraph "blog admin token usage"',
73
74
  ' kgraph "blog admin token usage" --final',
74
75
  ' kgraph "blog admin token usage" --capture "Author filter now uses display names" --capture-file www/app/blog/page.tsx',
@@ -1,7 +1,7 @@
1
1
  import type { IntegrationConfig } from '../types/config.js';
2
2
  import type { RepositoryFile } from '../types/maps.js';
3
3
  import { type InitIntegrationRecommendation } from './init-recommendations.js';
4
- type CoverageLevel = 'deep' | 'basic' | 'generic';
4
+ type CoverageLevel = 'deep' | 'basic' | 'structured' | 'generic';
5
5
  export interface InitLanguageSummary {
6
6
  language: string;
7
7
  label: string;
@@ -12,15 +12,32 @@ const LANGUAGE_PRESENTATION = {
12
12
  c: { label: 'C', coverage: 'deep' },
13
13
  cpp: { label: 'C++', coverage: 'deep' },
14
14
  csharp: { label: 'C#', coverage: 'deep' },
15
- yaml: { label: 'YAML', coverage: 'generic' },
16
- json: { label: 'JSON', coverage: 'generic' },
17
- toml: { label: 'TOML', coverage: 'generic' },
18
- xml: { label: 'XML', coverage: 'generic' },
19
- graphql: { label: 'GraphQL', coverage: 'generic' },
20
- sql: { label: 'SQL', coverage: 'generic' },
21
- shell: { label: 'Shell', coverage: 'generic' },
15
+ php: { label: 'PHP', coverage: 'deep' },
16
+ swift: { label: 'Swift', coverage: 'basic' },
17
+ ruby: { label: 'Ruby', coverage: 'deep' },
18
+ shell: { label: 'Shell', coverage: 'deep' },
19
+ lua: { label: 'Lua', coverage: 'basic' },
20
+ dart: { label: 'Dart', coverage: 'basic' },
21
+ elixir: { label: 'Elixir', coverage: 'basic' },
22
+ scala: { label: 'Scala', coverage: 'basic' },
23
+ r: { label: 'R', coverage: 'basic' },
24
+ sql: { label: 'SQL', coverage: 'deep' },
25
+ terraform: { label: 'Terraform/HCL', coverage: 'basic' },
26
+ graphql: { label: 'GraphQL', coverage: 'basic' },
27
+ protobuf: { label: 'Protocol Buffers', coverage: 'basic' },
28
+ yaml: { label: 'YAML', coverage: 'structured' },
29
+ json: { label: 'JSON', coverage: 'structured' },
30
+ toml: { label: 'TOML', coverage: 'structured' },
31
+ dockerfile: { label: 'Dockerfile', coverage: 'structured' },
32
+ markdown: { label: 'Markdown', coverage: 'structured' },
33
+ html: { label: 'HTML', coverage: 'structured' },
34
+ css: { label: 'CSS', coverage: 'structured' },
35
+ scss: { label: 'SCSS', coverage: 'structured' },
36
+ sass: { label: 'Sass', coverage: 'structured' },
37
+ less: { label: 'Less', coverage: 'structured' },
38
+ xml: { label: 'XML', coverage: 'structured' },
22
39
  };
23
- const EXCLUDED_LANGUAGES = new Set(['unknown', 'markdown', 'restructuredtext']);
40
+ const EXCLUDED_LANGUAGES = new Set(['unknown', 'restructuredtext']);
24
41
  export function summarizeInitLanguages(files) {
25
42
  const byLabel = new Map();
26
43
  for (const file of files) {
@@ -103,6 +120,7 @@ function moreDetailedCoverage(left, right) {
103
120
  const rank = {
104
121
  deep: 3,
105
122
  basic: 2,
123
+ structured: 2,
106
124
  generic: 1,
107
125
  };
108
126
  return rank[left] >= rank[right] ? left : right;
@@ -113,6 +131,8 @@ function coverageDescription(coverage) {
113
131
  return 'deep built-in extraction';
114
132
  case 'basic':
115
133
  return 'basic built-in extraction';
134
+ case 'structured':
135
+ return 'structured file extraction';
116
136
  default:
117
137
  return 'generic file coverage';
118
138
  }
@@ -67,6 +67,47 @@ export const DEFAULT_CONFIG = {
67
67
  '.hxx',
68
68
  // C#
69
69
  '.cs',
70
+ // PHP / Swift / Ruby
71
+ '.php',
72
+ '.swift',
73
+ '.rb',
74
+ '.rake',
75
+ // Shell
76
+ '.sh',
77
+ '.bash',
78
+ '.zsh',
79
+ '.fish',
80
+ // Web / styles
81
+ '.html',
82
+ '.htm',
83
+ '.css',
84
+ '.scss',
85
+ '.sass',
86
+ '.less',
87
+ // Data / config / schema
88
+ '.json',
89
+ '.jsonc',
90
+ '.yaml',
91
+ '.yml',
92
+ '.toml',
93
+ '.dockerfile',
94
+ '.xml',
95
+ '.graphql',
96
+ '.gql',
97
+ '.sql',
98
+ '.tf',
99
+ '.proto',
100
+ // Docs
101
+ '.md',
102
+ '.mdx',
103
+ // Additional app languages
104
+ '.lua',
105
+ '.r',
106
+ '.R',
107
+ '.dart',
108
+ '.ex',
109
+ '.exs',
110
+ '.scala',
70
111
  ],
71
112
  },
72
113
  maxContextItems: 8,
@@ -0,0 +1,3 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractBroadSymbols(sourceText: string, filePath: string, language: string): Promise<SymbolExtractionResult>;
3
+ export declare function supportsBroadExtraction(language: string): boolean;
@@ -0,0 +1,292 @@
1
+ import { parseSource } from './tree-sitter-parser.js';
2
+ const TREE_SITTER_GRAMMAR_BY_LANGUAGE = {
3
+ yaml: 'yaml',
4
+ json: 'json',
5
+ html: 'html',
6
+ css: 'css',
7
+ lua: 'lua',
8
+ dart: 'dart',
9
+ elixir: 'elixir',
10
+ scala: 'scala',
11
+ };
12
+ export async function extractBroadSymbols(sourceText, filePath, language) {
13
+ const result = createExtractionResult();
14
+ if (!sourceText.trim()) {
15
+ return result;
16
+ }
17
+ const broadLanguage = language;
18
+ const grammar = TREE_SITTER_GRAMMAR_BY_LANGUAGE[broadLanguage];
19
+ if (grammar) {
20
+ try {
21
+ const tree = await parseSource(sourceText, grammar);
22
+ tree.delete();
23
+ }
24
+ catch (error) {
25
+ result.warnings.push(`tree-sitter ${grammar} parse failed: ${error instanceof Error ? error.message : String(error)}`);
26
+ }
27
+ }
28
+ const lines = sourceText.split(/\r?\n/);
29
+ switch (broadLanguage) {
30
+ case 'swift':
31
+ collectSwift(lines, filePath, result);
32
+ break;
33
+ case 'terraform':
34
+ collectTerraform(lines, filePath, result);
35
+ break;
36
+ case 'graphql':
37
+ collectGraphql(lines, filePath, result);
38
+ break;
39
+ case 'protobuf':
40
+ collectProtobuf(lines, filePath, result);
41
+ break;
42
+ case 'lua':
43
+ collectLua(lines, filePath, result);
44
+ break;
45
+ case 'dart':
46
+ collectDart(lines, filePath, result);
47
+ break;
48
+ case 'elixir':
49
+ collectElixir(lines, filePath, result);
50
+ break;
51
+ case 'scala':
52
+ collectScala(lines, filePath, result);
53
+ break;
54
+ case 'r':
55
+ collectR(lines, filePath, result);
56
+ break;
57
+ case 'yaml':
58
+ case 'json':
59
+ case 'toml':
60
+ case 'dockerfile':
61
+ collectConfig(lines, filePath, result, broadLanguage);
62
+ break;
63
+ case 'markdown':
64
+ collectMarkdown(lines, filePath, result);
65
+ break;
66
+ case 'html':
67
+ case 'xml':
68
+ collectMarkup(lines, filePath, result);
69
+ break;
70
+ case 'css':
71
+ case 'scss':
72
+ case 'sass':
73
+ case 'less':
74
+ collectStylesheet(lines, filePath, result);
75
+ break;
76
+ }
77
+ return result;
78
+ }
79
+ export function supportsBroadExtraction(language) {
80
+ return [
81
+ 'swift',
82
+ 'terraform',
83
+ 'graphql',
84
+ 'protobuf',
85
+ 'lua',
86
+ 'dart',
87
+ 'elixir',
88
+ 'scala',
89
+ 'r',
90
+ 'yaml',
91
+ 'json',
92
+ 'toml',
93
+ 'dockerfile',
94
+ 'markdown',
95
+ 'html',
96
+ 'css',
97
+ 'scss',
98
+ 'sass',
99
+ 'less',
100
+ 'xml',
101
+ ].includes(language);
102
+ }
103
+ function createExtractionResult() {
104
+ return { symbols: [], dependencies: [], relationships: [], warnings: [] };
105
+ }
106
+ function addSymbol(result, filePath, name, kind, line, exported = false, parentName) {
107
+ const id = [filePath, kind, parentName, name, line].filter(Boolean).join('#');
108
+ const symbol = {
109
+ id,
110
+ name,
111
+ kind,
112
+ filePath,
113
+ startLine: line,
114
+ endLine: line,
115
+ exported,
116
+ parentName,
117
+ };
118
+ result.symbols.push(symbol);
119
+ result.relationships.push({
120
+ sourceType: 'file',
121
+ sourceId: filePath,
122
+ targetType: 'symbol',
123
+ targetId: id,
124
+ relationshipType: 'contains',
125
+ confidence: 'high',
126
+ });
127
+ return symbol;
128
+ }
129
+ function addDependency(result, filePath, specifier, kind = specifier.startsWith('.') ? 'local' : 'package') {
130
+ result.dependencies.push({ fromFile: filePath, specifier, kind });
131
+ }
132
+ function collectSwift(lines, filePath, result) {
133
+ forEachLine(lines, (line, lineNumber) => {
134
+ const importMatch = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/);
135
+ if (importMatch?.[1])
136
+ addDependency(result, filePath, importMatch[1]);
137
+ const typeMatch = line.match(/^\s*(?:public|private|internal|open|final|\s)*(class|struct|enum|protocol)\s+([A-Za-z_][\w]*)/);
138
+ if (typeMatch?.[2]) {
139
+ addSymbol(result, filePath, typeMatch[2], typeMatch[1] === 'protocol' ? 'interface' : 'class', lineNumber, true);
140
+ }
141
+ const functionMatch = line.match(/^\s*(?:public|private|internal|open|static|\s)*func\s+([A-Za-z_][\w]*)/);
142
+ if (functionMatch?.[1]) {
143
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
144
+ }
145
+ });
146
+ }
147
+ function collectTerraform(lines, filePath, result) {
148
+ forEachLine(lines, (line, lineNumber) => {
149
+ const blockMatch = line.match(/^\s*(resource|data|module|variable|output|provider)\s+"([^"]+)"(?:\s+"([^"]+)")?/);
150
+ if (blockMatch?.[1] && blockMatch[2]) {
151
+ addSymbol(result, filePath, [blockMatch[1], blockMatch[2], blockMatch[3]].filter(Boolean).join('.'), 'type', lineNumber, true);
152
+ }
153
+ });
154
+ }
155
+ function collectGraphql(lines, filePath, result) {
156
+ forEachLine(lines, (line, lineNumber) => {
157
+ const typeMatch = line.match(/^\s*(type|interface|enum|input|union|scalar)\s+([A-Za-z_][\w]*)/);
158
+ if (typeMatch?.[2]) {
159
+ addSymbol(result, filePath, typeMatch[2], typeMatch[1] === 'interface' ? 'interface' : 'type', lineNumber, true);
160
+ }
161
+ });
162
+ }
163
+ function collectProtobuf(lines, filePath, result) {
164
+ forEachLine(lines, (line, lineNumber) => {
165
+ const importMatch = line.match(/^\s*import\s+"([^"]+)"/);
166
+ if (importMatch?.[1])
167
+ addDependency(result, filePath, importMatch[1], 'local');
168
+ const typeMatch = line.match(/^\s*(message|service|enum)\s+([A-Za-z_][\w]*)/);
169
+ if (typeMatch?.[2]) {
170
+ addSymbol(result, filePath, typeMatch[2], 'type', lineNumber, true);
171
+ }
172
+ });
173
+ }
174
+ function collectLua(lines, filePath, result) {
175
+ forEachLine(lines, (line, lineNumber) => {
176
+ const requireMatch = line.match(/require\s*\(?\s*['"]([^'"]+)['"]/);
177
+ if (requireMatch?.[1])
178
+ addDependency(result, filePath, requireMatch[1]);
179
+ const functionMatch = line.match(/^\s*(?:local\s+)?function\s+([A-Za-z_][\w.:]*)/);
180
+ if (functionMatch?.[1]) {
181
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
182
+ }
183
+ });
184
+ }
185
+ function collectDart(lines, filePath, result) {
186
+ forEachLine(lines, (line, lineNumber) => {
187
+ const importMatch = line.match(/^\s*import\s+['"]([^'"]+)['"]/);
188
+ if (importMatch?.[1])
189
+ addDependency(result, filePath, importMatch[1]);
190
+ const classMatch = line.match(/^\s*(?:abstract\s+)?class\s+([A-Za-z_][\w]*)/);
191
+ if (classMatch?.[1])
192
+ addSymbol(result, filePath, classMatch[1], 'class', lineNumber, true);
193
+ const functionMatch = line.match(/^\s*(?:[A-Za-z_<>,?]+\s+)+([A-Za-z_][\w]*)\s*\([^;]*\)\s*(?:async\s*)?\{/);
194
+ if (functionMatch?.[1] && !['if', 'for', 'while', 'switch'].includes(functionMatch[1])) {
195
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
196
+ }
197
+ });
198
+ }
199
+ function collectElixir(lines, filePath, result) {
200
+ forEachLine(lines, (line, lineNumber) => {
201
+ const moduleMatch = line.match(/^\s*defmodule\s+([A-Z][\w.]+)/);
202
+ if (moduleMatch?.[1])
203
+ addSymbol(result, filePath, moduleMatch[1], 'class', lineNumber, true);
204
+ const functionMatch = line.match(/^\s*defp?\s+([a-z_][\w!?]*)/);
205
+ if (functionMatch?.[1])
206
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
207
+ });
208
+ }
209
+ function collectScala(lines, filePath, result) {
210
+ forEachLine(lines, (line, lineNumber) => {
211
+ const importMatch = line.match(/^\s*import\s+(.+)/);
212
+ if (importMatch?.[1])
213
+ addDependency(result, filePath, importMatch[1].trim());
214
+ const typeMatch = line.match(/^\s*(?:case\s+)?(class|object|trait|enum)\s+([A-Za-z_][\w]*)/);
215
+ if (typeMatch?.[2]) {
216
+ addSymbol(result, filePath, typeMatch[2], typeMatch[1] === 'trait' ? 'interface' : 'class', lineNumber, true);
217
+ }
218
+ const functionMatch = line.match(/^\s*def\s+([A-Za-z_][\w]*)/);
219
+ if (functionMatch?.[1])
220
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
221
+ });
222
+ }
223
+ function collectR(lines, filePath, result) {
224
+ forEachLine(lines, (line, lineNumber) => {
225
+ const libraryMatch = line.match(/^\s*(?:library|require)\s*\(\s*([A-Za-z.][\w.]*)/);
226
+ if (libraryMatch?.[1])
227
+ addDependency(result, filePath, libraryMatch[1]);
228
+ const functionMatch = line.match(/^\s*([A-Za-z.][\w.]*)\s*(?:<-|=)\s*function\s*\(/);
229
+ if (functionMatch?.[1])
230
+ addSymbol(result, filePath, functionMatch[1], 'function', lineNumber, true);
231
+ });
232
+ }
233
+ function collectConfig(lines, filePath, result, language) {
234
+ forEachLine(lines, (line, lineNumber) => {
235
+ if (language === 'dockerfile') {
236
+ const stageMatch = line.match(/^\s*FROM\s+\S+(?:\s+AS\s+([A-Za-z_][\w-]*))?/i);
237
+ if (stageMatch?.[1])
238
+ addSymbol(result, filePath, stageMatch[1], 'type', lineNumber, true);
239
+ return;
240
+ }
241
+ const keyMatch = language === 'json'
242
+ ? line.match(/^\s*"([^"]+)"\s*:/)
243
+ : line.match(/^\s*([A-Za-z_][\w.-]*)\s*[:=]/);
244
+ if (keyMatch?.[1]) {
245
+ addSymbol(result, filePath, keyMatch[1], 'type', lineNumber);
246
+ }
247
+ });
248
+ }
249
+ function collectMarkdown(lines, filePath, result) {
250
+ forEachLine(lines, (line, lineNumber) => {
251
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
252
+ if (headingMatch?.[2]) {
253
+ addSymbol(result, filePath, headingMatch[2].trim(), 'type', lineNumber);
254
+ }
255
+ });
256
+ }
257
+ function collectMarkup(lines, filePath, result) {
258
+ forEachLine(lines, (line, lineNumber) => {
259
+ for (const idMatch of line.matchAll(/\bid=["']([^"']+)["']/g)) {
260
+ if (idMatch[1])
261
+ addSymbol(result, filePath, `#${idMatch[1]}`, 'type', lineNumber);
262
+ }
263
+ for (const classMatch of line.matchAll(/\bclass=["']([^"']+)["']/g)) {
264
+ for (const className of classMatch[1]?.split(/\s+/) ?? []) {
265
+ if (className)
266
+ addSymbol(result, filePath, `.${className}`, 'type', lineNumber);
267
+ }
268
+ }
269
+ });
270
+ }
271
+ function collectStylesheet(lines, filePath, result) {
272
+ forEachLine(lines, (line, lineNumber) => {
273
+ const importMatch = line.match(/^\s*@(import|use|forward)\s+["']([^"']+)["']/);
274
+ if (importMatch?.[2])
275
+ addDependency(result, filePath, importMatch[2]);
276
+ for (const variableMatch of line.matchAll(/(--[A-Za-z_][\w-]*|\$[A-Za-z_][\w-]*)\s*:/g)) {
277
+ if (variableMatch[1])
278
+ addSymbol(result, filePath, variableMatch[1], 'type', lineNumber);
279
+ }
280
+ const mixinMatch = line.match(/^\s*@(mixin|function|keyframes)\s+([A-Za-z_][\w-]*)/);
281
+ if (mixinMatch?.[2]) {
282
+ addSymbol(result, filePath, mixinMatch[2], mixinMatch[1] === 'function' ? 'function' : 'type', lineNumber);
283
+ }
284
+ for (const selectorMatch of line.matchAll(/([.#][A-Za-z_][\w-]*)/g)) {
285
+ if (selectorMatch[1])
286
+ addSymbol(result, filePath, selectorMatch[1], 'type', lineNumber);
287
+ }
288
+ });
289
+ }
290
+ function forEachLine(lines, callback) {
291
+ lines.forEach((line, index) => callback(line, index + 1));
292
+ }
@@ -0,0 +1,23 @@
1
+ import type { CodeSymbol, Dependency, Relationship } from '../types/maps.js';
2
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
3
+ export declare class ExtractionContext {
4
+ private readonly filePath;
5
+ readonly symbols: CodeSymbol[];
6
+ readonly dependencies: Dependency[];
7
+ readonly relationships: Relationship[];
8
+ readonly warnings: string[];
9
+ constructor(filePath: string);
10
+ addSymbol(options: {
11
+ name: string;
12
+ kind: CodeSymbol['kind'];
13
+ startLine: number;
14
+ endLine?: number;
15
+ exported?: boolean;
16
+ parentName?: string;
17
+ }): CodeSymbol;
18
+ addDependency(specifier: string, kind?: Dependency['kind'], confidence?: Relationship['confidence']): void;
19
+ addSymbolContains(parent: CodeSymbol, child: CodeSymbol): void;
20
+ addWarning(message: string): void;
21
+ toResult(): SymbolExtractionResult;
22
+ }
23
+ export declare function emptyExtractionResult(): SymbolExtractionResult;