@brainwav/diagram 1.0.7 → 1.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 (91) hide show
  1. package/.diagram/contracts/machine-command-coverage.json +73 -0
  2. package/.diagram/migration/finalization-policy.json +20 -0
  3. package/LICENSE +202 -21
  4. package/README.md +132 -339
  5. package/package.json +46 -13
  6. package/scripts/refresh-diagram-context.sh +274 -182
  7. package/src/analyzers/default-analyzer.js +11 -0
  8. package/src/analyzers/index.js +34 -0
  9. package/src/artifacts/agent-context.js +105 -0
  10. package/src/artifacts/artifact-budget.js +224 -0
  11. package/src/artifacts/brief.js +153 -0
  12. package/src/artifacts/evidence-manifest.js +206 -0
  13. package/src/artifacts/evidence-summary.js +29 -0
  14. package/src/commands/analyze.js +125 -0
  15. package/src/commands/changed.js +185 -0
  16. package/src/commands/context.js +110 -0
  17. package/src/commands/diff.js +142 -0
  18. package/src/commands/doctor.js +335 -0
  19. package/src/commands/explain.js +273 -0
  20. package/src/commands/generate-all.js +170 -0
  21. package/src/commands/generate-animated.js +50 -0
  22. package/src/commands/generate-video.js +65 -0
  23. package/src/commands/generate.js +522 -0
  24. package/src/commands/init.js +123 -0
  25. package/src/commands/output.js +76 -0
  26. package/src/commands/scan.js +624 -0
  27. package/src/commands/shared.js +396 -0
  28. package/src/commands/validate.js +328 -0
  29. package/src/commands/video-shared.js +105 -0
  30. package/src/commands/workflow-pr.js +26 -0
  31. package/src/confidence/pipeline.js +186 -0
  32. package/src/config/diagramrc.js +79 -0
  33. package/src/context/build-context-pack.js +291 -0
  34. package/src/context/normalize-diagram-manifest.js +282 -0
  35. package/src/core/analysis-generation-analyze-components.js +102 -0
  36. package/src/core/analysis-generation-analyze-dependencies.js +33 -0
  37. package/src/core/analysis-generation-analyze-files.js +48 -0
  38. package/src/core/analysis-generation-analyze-options.js +73 -0
  39. package/src/core/analysis-generation-analyze.js +63 -0
  40. package/src/core/analysis-generation-constants.js +53 -0
  41. package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
  42. package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
  43. package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
  44. package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
  45. package/src/core/analysis-generation-diagrams-core.js +12 -0
  46. package/src/core/analysis-generation-diagrams-empty.js +68 -0
  47. package/src/core/analysis-generation-diagrams-erd.js +59 -0
  48. package/src/core/analysis-generation-diagrams-limit.js +27 -0
  49. package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
  50. package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
  51. package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
  52. package/src/core/analysis-generation-diagrams-role-data.js +182 -0
  53. package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
  54. package/src/core/analysis-generation-diagrams-role-security.js +129 -0
  55. package/src/core/analysis-generation-diagrams-role.js +25 -0
  56. package/src/core/analysis-generation-diagrams.js +182 -0
  57. package/src/core/analysis-generation-role-tags-constants.js +55 -0
  58. package/src/core/analysis-generation-role-tags-imports.js +32 -0
  59. package/src/core/analysis-generation-role-tags-infer.js +49 -0
  60. package/src/core/analysis-generation-role-tags-match.js +19 -0
  61. package/src/core/analysis-generation-role-tags.js +7 -0
  62. package/src/core/analysis-generation-utils-core.js +308 -0
  63. package/src/core/analysis-generation-utils-graph.js +321 -0
  64. package/src/core/analysis-generation-utils-resolution.js +76 -0
  65. package/src/core/analysis-generation-utils.js +9 -0
  66. package/src/core/analysis-generation.js +44 -0
  67. package/src/diagram.js +180 -1760
  68. package/src/formatters/console.js +198 -0
  69. package/src/formatters/index.js +41 -0
  70. package/src/formatters/json.js +113 -0
  71. package/src/formatters/junit.js +123 -0
  72. package/src/graph.js +159 -0
  73. package/src/incremental/cache.js +210 -0
  74. package/src/ir/architecture-ir.js +48 -0
  75. package/src/migration/evidence.js +262 -0
  76. package/src/migration/finalization-policy.js +35 -0
  77. package/src/renderers/report-html.js +265 -0
  78. package/src/rules/factory.js +108 -0
  79. package/src/rules/types/base.js +54 -0
  80. package/src/rules/types/import-rule.js +286 -0
  81. package/src/rules.js +380 -0
  82. package/src/schema/erd-confidence.js +56 -0
  83. package/src/schema/erd-extractor.js +504 -0
  84. package/src/schema/erd-model.js +176 -0
  85. package/src/schema/rules-schema.js +170 -0
  86. package/src/utils/suggestions.js +67 -0
  87. package/src/video.js +4 -5
  88. package/src/workflow/git-helpers.js +576 -0
  89. package/src/workflow/pr-command.js +694 -0
  90. package/src/workflow/pr-impact.js +848 -0
  91. package/src/workflow/sort-utils.js +16 -0
@@ -0,0 +1,182 @@
1
+ const chalk = require('chalk');
2
+ const crypto = require('crypto');
3
+ const path = require('path');
4
+ const { estimateTokensFromBytes } = require('../artifacts/artifact-budget');
5
+ const { findClosestMatch, formatSuggestion } = require('../utils/suggestions');
6
+ const { SUPPORTED_DIAGRAM_TYPES } = require('./analysis-generation-constants');
7
+ const { generateErdArtifact } = require('./analysis-generation-diagrams-erd');
8
+ const {
9
+ generateArchitecture,
10
+ generateSequence,
11
+ generateDependency,
12
+ generateClass,
13
+ generateFlow,
14
+ } = require('./analysis-generation-diagrams-core');
15
+ const {
16
+ generateDatabase,
17
+ generateUserInteractions,
18
+ generateEvents,
19
+ generateAuth,
20
+ generateSecurity,
21
+ generateAgent,
22
+ generateC4Context,
23
+ generateRag,
24
+ } = require('./analysis-generation-diagrams-role');
25
+
26
+ const DIAGRAM_PURPOSES = Object.freeze({
27
+ architecture: 'component_hierarchy',
28
+ sequence: 'service_interaction_flow',
29
+ dependency: 'import_dependency_graph',
30
+ class: 'class_relationships',
31
+ flow: 'process_data_flow',
32
+ database: 'persistence_code_paths',
33
+ erd: 'schema_entity_relationships',
34
+ user: 'user_entrypaths',
35
+ events: 'event_architecture_paths',
36
+ auth: 'authentication_authorization_flow',
37
+ security: 'security_boundary_trust_paths',
38
+ agent: 'multi_agent_orchestration_paths',
39
+ c4context: 'system_context_map',
40
+ rag: 'retrieval_augmented_generation_flow',
41
+ });
42
+
43
+ function defaultDiagramMetadata(type) {
44
+ return {
45
+ purpose: DIAGRAM_PURPOSES[type] || 'architecture_diagram',
46
+ consumers: ['human', 'agent'],
47
+ source: type === 'erd' ? 'schema_extraction' : 'static_analysis',
48
+ };
49
+ }
50
+
51
+ function buildDiagramArtifact(type, mermaid, metadata = {}) {
52
+ return {
53
+ mermaid,
54
+ metadata: {
55
+ ...defaultDiagramMetadata(type),
56
+ ...metadata,
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Selects and executes the appropriate diagram generator for the requested diagram type.
63
+ *
64
+ * If `type` is not recognised, logs a warning (including a nearest-match suggestion when available)
65
+ * and falls back to generating an architecture diagram.
66
+ *
67
+ * @param {any} data - Input data used by the selected generator to produce the diagram.
68
+ * @param {string} type - Diagram type identifier (e.g. "architecture", "sequence", "database").
69
+ * @param {string|undefined} [focus] - Optional focus/context passed to generators that support it.
70
+ * @returns {{mermaid: string, metadata: Object}} Generated Mermaid diagram source and artifact metadata.
71
+ */
72
+ function generateDiagramArtifact(data, type, focus) {
73
+ switch (type) {
74
+ case 'architecture': return buildDiagramArtifact(type, generateArchitecture(data, focus));
75
+ case 'sequence': return buildDiagramArtifact(type, generateSequence(data));
76
+ case 'dependency': return buildDiagramArtifact(type, generateDependency(data, focus));
77
+ case 'class': return buildDiagramArtifact(type, generateClass(data));
78
+ case 'flow': return buildDiagramArtifact(type, generateFlow(data));
79
+ case 'database': return buildDiagramArtifact(type, generateDatabase(data));
80
+ case 'erd': return generateErdArtifact(data);
81
+ case 'user': return buildDiagramArtifact(type, generateUserInteractions(data));
82
+ case 'events': return buildDiagramArtifact(type, generateEvents(data));
83
+ case 'auth': return buildDiagramArtifact(type, generateAuth(data));
84
+ case 'security': return buildDiagramArtifact(type, generateSecurity(data));
85
+ case 'agent': return buildDiagramArtifact(type, generateAgent(data));
86
+ case 'c4context': return buildDiagramArtifact(type, generateC4Context(data));
87
+ case 'rag': return buildDiagramArtifact(type, generateRag(data));
88
+ default: {
89
+ const validTypes = [...SUPPORTED_DIAGRAM_TYPES];
90
+ const suggestion = findClosestMatch(type, validTypes);
91
+ console.warn(chalk.yellow(`⚠️ Unknown diagram type "${type}", using architecture`));
92
+ if (suggestion) {
93
+ console.warn(formatSuggestion(suggestion));
94
+ }
95
+ return buildDiagramArtifact('architecture', generateArchitecture(data, focus));
96
+ }
97
+ }
98
+ }
99
+
100
+ function generate(data, type, focus) {
101
+ return generateDiagramArtifact(data, type, focus).mermaid;
102
+ }
103
+
104
+ /**
105
+ * Common placeholder note texts used to detect empty diagrams.
106
+ */
107
+ const PLACEHOLDER_NOTE_TEXTS = [
108
+ 'note["no data available"]',
109
+ 'note["no components found',
110
+ 'no services detected',
111
+ 'note "no data available"',
112
+ 'note "no classes found"',
113
+ 'note["no database-focused components found"]',
114
+ 'no supported schema sources found',
115
+ 'schema sources: none',
116
+ 'note["no user-facing components found"]',
117
+ 'note["no event/channels components found"]',
118
+ 'note["no authentication components found"]',
119
+ 'note["no security-focused components found"]',
120
+ 'no agent/llm components found',
121
+ 'no data available',
122
+ ];
123
+
124
+ /**
125
+ * Detects whether Mermaid diagram content represents a placeholder or empty diagram.
126
+ *
127
+ * @param {string|null|undefined} mermaidCode - Mermaid diagram source to inspect; non-string or falsy values are treated as placeholder content.
128
+ * @returns {boolean} `true` if the provided content is empty, not a string, or contains common placeholder notes (e.g. "no data available", "no components found"); `false` otherwise.
129
+ */
130
+ function isPlaceholderDiagram(mermaidCode) {
131
+ if (!mermaidCode || typeof mermaidCode !== 'string') return true;
132
+ const compact = mermaidCode.toLowerCase();
133
+ return PLACEHOLDER_NOTE_TEXTS.some(noteText => compact.includes(noteText));
134
+ }
135
+
136
+ /**
137
+ * Create a manifest entry describing a generated Mermaid diagram file.
138
+ *
139
+ * @param {string} type - Diagram type identifier.
140
+ * @param {string} filePath - Absolute or relative path to the generated file.
141
+ * @param {string|any} mermaidCode - Mermaid source for the diagram; may be non-string.
142
+ * @param {string} [rootPath] - Optional root path used to compute a relative outputPath.
143
+ * @returns {{type: string, file: string, outputPath: string, lines: number, bytes: number, approxTokens: number, isPlaceholder: boolean}} An object containing:
144
+ * - `type`: the diagram type,
145
+ * - `file`: basename of `filePath`,
146
+ * - `outputPath`: `filePath` relative to `rootPath` when provided, otherwise `filePath` as given,
147
+ * - `lines`: number of lines in `mermaidCode`,
148
+ * - `bytes`: UTF-8 byte size of `mermaidCode`,
149
+ * - `approxTokens`: token estimate derived from `bytes`,
150
+ * - `isPlaceholder`: `true` if `mermaidCode` is considered a placeholder/empty diagram, `false` otherwise.
151
+ */
152
+ function toManifestEntry(type, filePath, mermaidCode, rootPath, metadata = {}) {
153
+ const lines = typeof mermaidCode === 'string' ? mermaidCode.split('\n') : [];
154
+ const bytes = Buffer.byteLength(mermaidCode || '', 'utf8');
155
+ const defaults = defaultDiagramMetadata(type);
156
+ const sourceHash = crypto
157
+ .createHash('sha256')
158
+ .update(String(mermaidCode || ''))
159
+ .digest('hex');
160
+ return {
161
+ type,
162
+ file: path.basename(filePath),
163
+ outputPath: rootPath ? path.relative(rootPath, filePath) : filePath,
164
+ purpose: metadata.purpose || defaults.purpose,
165
+ consumers: Array.isArray(metadata.consumers) ? metadata.consumers : defaults.consumers,
166
+ source: metadata.source || defaults.source,
167
+ commitPolicy: 'generated_artifact',
168
+ sourceHash,
169
+ lines: lines.length,
170
+ bytes,
171
+ approxTokens: estimateTokensFromBytes(bytes),
172
+ isPlaceholder: isPlaceholderDiagram(mermaidCode),
173
+ ...(metadata && Object.keys(metadata).length > 0 ? { metadata } : {}),
174
+ };
175
+ }
176
+
177
+ module.exports = {
178
+ generate,
179
+ generateDiagramArtifact,
180
+ isPlaceholderDiagram,
181
+ toManifestEntry,
182
+ };
@@ -0,0 +1,55 @@
1
+ const ROLE_PATTERNS = {
2
+ user: [
3
+ 'route', 'routes', 'controller', 'controllers', 'handler', 'handlers',
4
+ 'api', 'middleware', 'page', 'pages', 'ui', 'frontend', 'web', 'client', 'request'
5
+ ],
6
+ auth: [
7
+ 'auth', 'authentication', 'authorization', 'session', 'signin', 'login',
8
+ 'signup', 'token', 'jwt', 'oauth', 'sso', 'passport', 'identity', 'acl',
9
+ 'guard', 'permission', 'password', 'mfa', 'security'
10
+ ],
11
+ database: [
12
+ 'db', 'database', 'data', 'datastore', 'repository', 'repo', 'model',
13
+ 'schema', 'migration', 'query', 'querybuilder', 'prisma', 'typeorm',
14
+ 'sequelize', 'mongoose', 'knex', 'drizzle', 'redis', 'postgres', 'mysql',
15
+ 'sqlite', 'mongo', 'dynamodb', 'd1'
16
+ ],
17
+ events: [
18
+ 'event', 'events', 'queue', 'worker', 'cron', 'scheduler', 'webhook',
19
+ 'pubsub', 'bus', 'publish', 'subscriber', 'consumer', 'producer',
20
+ 'listener', 'trigger'
21
+ ],
22
+ integrations: [
23
+ 'integration', 'webhook', 'gateway', 'stripe', 'pay', 'sendgrid', 'twilio',
24
+ 'sentry', 'github', 'slack', 'analytics', 'mail', 'smtp', 'storage'
25
+ ],
26
+ security: [
27
+ 'security', 'threat', 'attack', 'rate', 'encrypt', 'decrypt', 'signature',
28
+ 'hash', 'verify', 'csrf', 'xss', 'audit', 'compliance', 'policy', 'vault',
29
+ 'kms', 'secret', 'key'
30
+ ],
31
+ agent: [
32
+ 'agent', 'supervisor', 'orchestrator', 'planner', 'executor', 'swarm',
33
+ 'crew', 'runner', 'coordinator', 'assistant', 'brain'
34
+ ],
35
+ tool: [
36
+ 'tool', 'tools', 'plugin', 'plugins', 'action', 'actions',
37
+ 'function_calling', 'functioncalling', 'capability', 'skill'
38
+ ],
39
+ memory: [
40
+ 'memory', 'vector', 'vectorstore', 'embedding', 'embeddings',
41
+ 'retrieval', 'rag', 'chroma', 'pinecone', 'weaviate', 'faiss',
42
+ 'context', 'recall', 'longterm', 'shortterm'
43
+ ],
44
+ llm: [
45
+ 'llm', 'openai', 'anthropic', 'gemini', 'claude', 'ollama', 'gpt',
46
+ 'completion', 'inference', 'model', 'prompt', 'chat', 'generation'
47
+ ],
48
+ };
49
+
50
+ const SENSITIVE_RISK_TAGS = new Set(['auth', 'database', 'security']);
51
+
52
+ module.exports = {
53
+ ROLE_PATTERNS,
54
+ SENSITIVE_RISK_TAGS,
55
+ };
@@ -0,0 +1,32 @@
1
+ const {
2
+ getImportPath,
3
+ getExternalPackageName,
4
+ } = require('./analysis-generation-utils');
5
+
6
+ /**
7
+ * Collects unique external package names referenced by an array of import entries.
8
+ *
9
+ * @param {Array} importEntries - Array of import entry objects or descriptors; entries that produce no import path or a relative path (starting with ".") are ignored.
10
+ * @returns {string[]} Unique external package names found in importEntries.
11
+ */
12
+ function collectExternalImports(importEntries) {
13
+ const packages = new Set();
14
+ if (!Array.isArray(importEntries)) return [];
15
+
16
+ for (const entry of importEntries) {
17
+ const importPath = getImportPath(entry);
18
+ if (!importPath || importPath.startsWith('.')) {
19
+ continue;
20
+ }
21
+ const externalPackage = getExternalPackageName(importPath);
22
+ if (externalPackage) {
23
+ packages.add(externalPackage);
24
+ }
25
+ }
26
+
27
+ return [...packages];
28
+ }
29
+
30
+ module.exports = {
31
+ collectExternalImports,
32
+ };
@@ -0,0 +1,49 @@
1
+ const { normalizePath } = require('./analysis-generation-utils');
2
+ const { ROLE_PATTERNS, SENSITIVE_RISK_TAGS } = require('./analysis-generation-role-tags-constants');
3
+ const { textHasToken } = require('./analysis-generation-role-tags-match');
4
+ const { collectExternalImports } = require('./analysis-generation-role-tags-imports');
5
+
6
+ /**
7
+ * Infer role and risk tags for a file from its path, original name, content and external imports.
8
+ *
9
+ * @param {string} filePath - File path used for structural matching.
10
+ * @param {string} originalName - Original file name used for structural matching.
11
+ * @param {string} fileContent - Full file content used for token matching.
12
+ * @param {Array} importEntries - Import records used to collect external import identifiers.
13
+ * @param {string} type - If equal to `'service'`, the `'service'` tag will be added.
14
+ * @returns {string[]} Array of inferred tags. Includes `'service'` when `type === 'service'`; if no tags match, returns `['general']`.
15
+ */
16
+ function inferRoleTags(filePath, originalName, fileContent, importEntries, type) {
17
+ const content = (fileContent || '').toLowerCase();
18
+ const pathText = normalizePath(filePath || '').toLowerCase();
19
+ const nameText = (originalName || '').toLowerCase();
20
+ const externalImports = collectExternalImports(importEntries).join(' ').toLowerCase();
21
+ const structuralCombined = `${pathText} ${nameText} ${externalImports}`;
22
+ const fullCombined = `${structuralCombined} ${content}`;
23
+
24
+ const tags = new Set();
25
+
26
+ for (const [tag, tokens] of Object.entries(ROLE_PATTERNS)) {
27
+ const searchTarget = SENSITIVE_RISK_TAGS.has(tag) ? structuralCombined : fullCombined;
28
+ for (const token of tokens) {
29
+ if (textHasToken(searchTarget, token)) {
30
+ tags.add(tag);
31
+ break;
32
+ }
33
+ }
34
+ }
35
+
36
+ if (type === 'service') {
37
+ tags.add('service');
38
+ }
39
+
40
+ if (tags.size === 0 || (type === 'service' && tags.size === 1 && tags.has('service'))) {
41
+ tags.add('general');
42
+ }
43
+
44
+ return [...tags];
45
+ }
46
+
47
+ module.exports = {
48
+ inferRoleTags,
49
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Determines whether the given text contains the specified token as a whole token-like substring.
3
+ *
4
+ * Matching is case-insensitive and requires the token to be bounded by the start or end of the string
5
+ * or by one-character separators: '/', '\', '.', '_' or '-'.
6
+ *
7
+ * @param {string} text - The text to search within.
8
+ * @param {string} token - The token to search for; all characters in this token are matched literally.
9
+ * @returns {boolean} `true` if the token is present as a whole token-like substring, `false` otherwise.
10
+ */
11
+ function textHasToken(text, token) {
12
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13
+ const re = new RegExp(`(^|[\\\\/._-])${escaped}([\\\\/._-]|$)`, 'i');
14
+ return re.test(text);
15
+ }
16
+
17
+ module.exports = {
18
+ textHasToken,
19
+ };
@@ -0,0 +1,7 @@
1
+ const { collectExternalImports } = require('./analysis-generation-role-tags-imports');
2
+ const { inferRoleTags } = require('./analysis-generation-role-tags-infer');
3
+
4
+ module.exports = {
5
+ collectExternalImports,
6
+ inferRoleTags,
7
+ };
@@ -0,0 +1,308 @@
1
+ const crypto = require('crypto');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Identify the programming language from a file path's extension.
6
+ * @param {string} filePath - The file path whose extension will be inspected.
7
+ * @returns {string} The language label (for example `'typescript'`, `'javascript'`, `'python'`) or `'unknown'` if the extension is not recognised.
8
+ */
9
+ function detectLanguage(filePath) {
10
+ if (typeof filePath !== 'string') return 'unknown';
11
+ const ext = path.extname(filePath).toLowerCase();
12
+ const map = {
13
+ '.ts': 'typescript', '.tsx': 'typescript',
14
+ '.mts': 'typescript', '.cts': 'typescript',
15
+ '.js': 'javascript', '.jsx': 'javascript',
16
+ '.mjs': 'javascript', '.cjs': 'javascript',
17
+ '.py': 'python', '.go': 'go', '.rs': 'rust',
18
+ '.java': 'java', '.rb': 'ruby', '.php': 'php',
19
+ };
20
+ return map[ext] || 'unknown';
21
+ }
22
+
23
+ /**
24
+ * Infer a coarse file type from the file name and file content using simple heuristics.
25
+ *
26
+ * Uses the file basename to prioritise `service` and `component` classifications, and
27
+ * inspects `content` for indicative tokens to identify `class`, `function` or `module`.
28
+ * Falls back to `file` when no heuristic matches.
29
+ *
30
+ * @param {string} filePath - File path used for basename-based heuristics.
31
+ * @param {string} content - File content used for token-based heuristics.
32
+ * @returns {string} One of: `'service'`, `'component'`, `'class'`, `'function'`, `'module'`, or `'file'`.
33
+ */
34
+ function inferType(filePath, content) {
35
+ const base = typeof filePath === 'string' ? path.basename(filePath).toLowerCase() : '';
36
+ const text = typeof content === 'string' ? content : '';
37
+ if (base.includes('service')) return 'service';
38
+ if (base.includes('component') || base.endsWith('.tsx') || base.endsWith('.jsx')) return 'component';
39
+ if (text.includes('class ') && text.includes('extends')) return 'class';
40
+ if (text.includes('export default function') || text.includes('export function')) return 'function';
41
+ if (text.includes('module.exports') || text.includes('export ')) return 'module';
42
+ return 'file';
43
+ }
44
+
45
+ const IMPORT_PATTERNS = Object.freeze({
46
+ javascript: [
47
+ /import\s+(?:(?:\{[^}]*?\}|\*\s+as\s+\w+|\w+)\s+from\s+)?["']([^"']+)["']/g,
48
+ /require\s*\(\s*["']([^"']+)["']\s*\)/g,
49
+ /import\s*\(\s*["']([^"']+)["']\s*\)/g,
50
+ ],
51
+ python: [
52
+ /^\s*from\s+([\w.]+)/gm,
53
+ /^\s*import\s+([\w.]+)/gm,
54
+ ],
55
+ });
56
+
57
+ /**
58
+ * Return the list of import-matching regular expressions for a given language.
59
+ * @param {string} lang - Language identifier; supported values: `'typescript'`, `'javascript'`, `'python'`, `'go'`.
60
+ * @returns {RegExp[]} An array of regular expression patterns used to find import statements for the specified language, or an empty array if the language is not recognised.
61
+ */
62
+ function resolveImportPatterns(lang) {
63
+ if (lang === 'typescript' || lang === 'javascript') return IMPORT_PATTERNS.javascript;
64
+ if (lang === 'python') return IMPORT_PATTERNS.python;
65
+ return [];
66
+ }
67
+
68
+ /**
69
+ * Find import specifiers in source text for a given language.
70
+ *
71
+ * @param {string} content - Source file content to scan for import statements.
72
+ * @param {string} [lang] - Language key used to select import patterns (e.g. "javascript", "python", "go").
73
+ * @returns {Array<{path: string, index: number, order: number}>} An array of match records for each import specifier.
74
+ * Each record contains:
75
+ * - `path`: the captured import path string,
76
+ * - `index`: character index in `content` where the match starts,
77
+ * - `order`: traversal order to preserve precedence when multiple patterns match at the same index.
78
+ */
79
+ function collectImportMatches(content, lang) {
80
+ if (typeof content !== 'string' || content.length === 0) return [];
81
+
82
+ const matches = [];
83
+ let order = 0;
84
+ for (const pattern of resolveImportPatterns(lang)) {
85
+ for (const match of content.matchAll(pattern)) {
86
+ const importPath = match[1];
87
+ if (!importPath) continue;
88
+ matches.push({
89
+ path: importPath,
90
+ index: typeof match.index === 'number' ? match.index : 0,
91
+ order,
92
+ });
93
+ order += 1;
94
+ }
95
+ }
96
+
97
+ matches.sort((a, b) => {
98
+ if (a.index !== b.index) return a.index - b.index;
99
+ return a.order - b.order;
100
+ });
101
+ return matches;
102
+ }
103
+
104
+ /**
105
+ * Build an array of character indices for the start of each line in the given content.
106
+ * @param {string} content - The text content to analyse.
107
+ * @returns {number[]} An array of zero-based character indices marking the start of each line; the first element is 0.
108
+ */
109
+ function buildLineStarts(content) {
110
+ const starts = [0];
111
+ for (let i = 0; i < content.length; i++) {
112
+ if (content[i] === '\n') starts.push(i + 1);
113
+ }
114
+ return starts;
115
+ }
116
+
117
+ /**
118
+ * Determine the 1-based line number containing the given character index.
119
+ *
120
+ * @param {number[]} lineStarts - Sorted array of zero-based character indices for the start of each line (first element should be 0).
121
+ * @param {number} index - Character index to locate; non-finite or negative values are treated as 0.
122
+ * @returns {number} The 1-based line number in which `index` falls. Returns `1` if `lineStarts` is not a valid non-empty array.
123
+ */
124
+ function lineNumberForIndex(lineStarts, index) {
125
+ if (!Array.isArray(lineStarts) || lineStarts.length === 0) return 1;
126
+ const boundedIndex = Math.max(0, Number.isFinite(index) ? index : 0);
127
+
128
+ let low = 0;
129
+ let high = lineStarts.length - 1;
130
+ while (low <= high) {
131
+ const mid = Math.floor((low + high) / 2);
132
+ if (lineStarts[mid] <= boundedIndex) {
133
+ low = mid + 1;
134
+ } else {
135
+ high = mid - 1;
136
+ }
137
+ }
138
+ return high + 1;
139
+ }
140
+
141
+ /**
142
+ * Extract Go import statements with their positions from source code.
143
+ *
144
+ * @param {string} content - Go source code to scan.
145
+ * @returns {Array<{path: string, line: number}>} Array of import records with path and line number.
146
+ */
147
+ function extractGoImportsWithPositions(content) {
148
+ if (typeof content !== 'string' || content.length === 0) return [];
149
+
150
+ const imports = [];
151
+ const lineStarts = buildLineStarts(content);
152
+
153
+ // Match single import: import "path"
154
+ const singleImportPattern = /import\s+"([^"]+)"/g;
155
+ for (const match of content.matchAll(singleImportPattern)) {
156
+ const importPath = match[1];
157
+ if (!importPath) continue;
158
+ const index = typeof match.index === 'number' ? match.index : 0;
159
+ imports.push({
160
+ path: importPath,
161
+ line: lineNumberForIndex(lineStarts, index),
162
+ });
163
+ }
164
+
165
+ // Match import blocks: import ( ... )
166
+ const blockPattern = /import\s*\(\s*([\s\S]*?)\s*\)/g;
167
+ for (const blockMatch of content.matchAll(blockPattern)) {
168
+ const block = blockMatch[1];
169
+ if (!block) continue;
170
+ const blockStart = typeof blockMatch.index === 'number' ? blockMatch.index : 0;
171
+ const blockOffsetInMatch = blockMatch[0].indexOf(block);
172
+
173
+ // Extract individual imports from the block
174
+ const pathPattern = /"([^"]+)"/g;
175
+ for (const pathMatch of block.matchAll(pathPattern)) {
176
+ const importPath = pathMatch[1];
177
+ if (!importPath) continue;
178
+ const pathIndex = blockStart
179
+ + Math.max(0, blockOffsetInMatch)
180
+ + (typeof pathMatch.index === 'number' ? pathMatch.index : 0);
181
+ imports.push({
182
+ path: importPath,
183
+ line: lineNumberForIndex(lineStarts, pathIndex),
184
+ });
185
+ }
186
+ }
187
+
188
+ return imports;
189
+ }
190
+
191
+ /**
192
+ * Extracts import specifiers from source content for a given language.
193
+ *
194
+ * @param {string} content - The file contents to scan for import statements.
195
+ * @param {string} lang - Language identifier (e.g. 'javascript', 'python', 'go') used to select parsing patterns.
196
+ * @returns {string[]} An array of import paths found in the content; an empty array if none are found or the input is invalid.
197
+ */
198
+ function extractImports(content, lang) {
199
+ if (lang === 'go') {
200
+ return extractGoImportsWithPositions(content).map((match) => match.path);
201
+ }
202
+ return collectImportMatches(content, lang).map((match) => match.path);
203
+ }
204
+
205
+ /**
206
+ * Parse source text for import statements for the given language and return each import path with its 1-based line number.
207
+ *
208
+ * @param {string} content - Source text to search for imports.
209
+ * @param {string} [lang] - Language hint used to select import patterns (e.g. 'javascript', 'python', 'go').
210
+ * @returns {{path: string, line: number}[]} An array of objects containing the import `path` and its 1-based `line` number.
211
+ */
212
+ function extractImportsWithPositions(content, lang) {
213
+ if (lang === 'go') {
214
+ return extractGoImportsWithPositions(content);
215
+ }
216
+ const lineStarts = buildLineStarts(typeof content === 'string' ? content : '');
217
+ return collectImportMatches(content, lang).map((match) => ({
218
+ path: match.path,
219
+ line: lineNumberForIndex(lineStarts, match.index),
220
+ }));
221
+ }
222
+
223
+ /**
224
+ * Create a filesystem- and identifier-safe name from an arbitrary string.
225
+ *
226
+ * Produces a base identifier by replacing non-alphanumeric/underscore characters with `_`
227
+ * and prefixing an underscore if the name starts with a digit, then appends an
228
+ * 8-character hexadecimal hash derived from the original input to ensure stability and reduce collisions.
229
+ *
230
+ * @param {string} name - The original name to sanitise.
231
+ * @returns {string} The sanitised identifier with an appended 8-character hex hash.
232
+ */
233
+ function sanitize(name) {
234
+ const base = name.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[0-9]/, '_$&');
235
+ const hash = crypto.createHash('sha256').update(name).digest('hex').slice(0, 8);
236
+ return `${base}_${hash}`;
237
+ }
238
+
239
+ /**
240
+ * Escape characters that interfere with Mermaid diagrams by prefixing them with backslashes.
241
+ *
242
+ * @param {string} str - The input string to escape.
243
+ * @returns {string} The input with any of the characters \ " [ ] ( ) # < > { } | prefixed by a backslash; returns an empty string for falsy input.
244
+ */
245
+ function escapeMermaid(str) {
246
+ if (!str) return '';
247
+ return str.replace(/[\\"\[\]()#<>{}|]/g, '\\$&');
248
+ }
249
+
250
+ /**
251
+ * Normalise path separators by converting Windows backslashes to forward slashes.
252
+ * @param {string} inputPath - Path string that may contain backslashes.
253
+ * @returns {string} The path with all backslashes replaced by forward slashes.
254
+ */
255
+ function normalizePath(inputPath) {
256
+ return inputPath.replace(/\\/g, '/');
257
+ }
258
+
259
+ /**
260
+ * Normalises a file path to a comparable form by converting backslashes to forward slashes and removing a leading "./".
261
+ * @param {string} filePath - The input path; falsy values are treated as an empty string.
262
+ * @returns {string} The normalised path using forward slashes with any leading "./" removed.
263
+ */
264
+ function toComparablePath(filePath) {
265
+ return normalizePath(String(filePath || '')).replace(/^\.\//, '');
266
+ }
267
+
268
+ /**
269
+ * Extracts a module specifier string from an import descriptor.
270
+ *
271
+ * @param {string|{path?: string}|null|undefined} importInfo - A module import descriptor: either a string specifier or an object with a `path` string property.
272
+ * @returns {string|null} The import path when available, or `null` if no valid path is present.
273
+ */
274
+ function getImportPath(importInfo) {
275
+ if (typeof importInfo === 'string') return importInfo;
276
+ if (importInfo && typeof importInfo.path === 'string') return importInfo.path;
277
+ return null;
278
+ }
279
+
280
+ /**
281
+ * Extracts the top-level package name from a module import specifier.
282
+ *
283
+ * @param {string} importPath - Module specifier (e.g. "react", "lodash/get", "@scope/pkg/sub").
284
+ * @returns {string|null} The package name — for scoped packages returns the scope and package (e.g. "@scope/pkg"), for unscoped returns the leading segment before a slash (e.g. "lodash"); `null` if `importPath` is not a non-empty string.
285
+ */
286
+ function getExternalPackageName(importPath) {
287
+ if (typeof importPath !== 'string') return null;
288
+ if (!importPath) return null;
289
+ if (importPath.startsWith('@')) {
290
+ const [scope, pkg] = importPath.split('/');
291
+ return scope && pkg ? `${scope}/${pkg}` : scope || null;
292
+ }
293
+ return importPath.split('/')[0] || null;
294
+ }
295
+
296
+ module.exports = {
297
+ detectLanguage,
298
+ inferType,
299
+ extractImports,
300
+ extractImportsWithPositions,
301
+ extractGoImportsWithPositions,
302
+ sanitize,
303
+ escapeMermaid,
304
+ normalizePath,
305
+ toComparablePath,
306
+ getImportPath,
307
+ getExternalPackageName,
308
+ };