@unrdf/kgn 5.0.1

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/package.json +90 -0
  4. package/src/MIGRATION_COMPLETE.md +186 -0
  5. package/src/PORT-MAP.md +302 -0
  6. package/src/base/filter-templates.js +479 -0
  7. package/src/base/index.js +92 -0
  8. package/src/base/injection-targets.js +583 -0
  9. package/src/base/macro-templates.js +298 -0
  10. package/src/base/macro-templates.js.bak +461 -0
  11. package/src/base/shacl-templates.js +617 -0
  12. package/src/base/template-base.js +388 -0
  13. package/src/core/attestor.js +381 -0
  14. package/src/core/filters.js +518 -0
  15. package/src/core/index.js +21 -0
  16. package/src/core/kgen-engine.js +372 -0
  17. package/src/core/parser.js +447 -0
  18. package/src/core/post-processor.js +313 -0
  19. package/src/core/renderer.js +469 -0
  20. package/src/doc-generator/cli.mjs +122 -0
  21. package/src/doc-generator/index.mjs +28 -0
  22. package/src/doc-generator/mdx-generator.mjs +71 -0
  23. package/src/doc-generator/nav-generator.mjs +136 -0
  24. package/src/doc-generator/parser.mjs +291 -0
  25. package/src/doc-generator/rdf-builder.mjs +306 -0
  26. package/src/doc-generator/scanner.mjs +189 -0
  27. package/src/engine/index.js +42 -0
  28. package/src/engine/pipeline.js +448 -0
  29. package/src/engine/renderer.js +604 -0
  30. package/src/engine/template-engine.js +566 -0
  31. package/src/filters/array.js +436 -0
  32. package/src/filters/data.js +479 -0
  33. package/src/filters/index.js +270 -0
  34. package/src/filters/rdf.js +264 -0
  35. package/src/filters/text.js +369 -0
  36. package/src/index.js +109 -0
  37. package/src/inheritance/index.js +40 -0
  38. package/src/injection/api.js +260 -0
  39. package/src/injection/atomic-writer.js +327 -0
  40. package/src/injection/constants.js +136 -0
  41. package/src/injection/idempotency-manager.js +295 -0
  42. package/src/injection/index.js +28 -0
  43. package/src/injection/injection-engine.js +378 -0
  44. package/src/injection/integration.js +339 -0
  45. package/src/injection/modes/index.js +341 -0
  46. package/src/injection/rollback-manager.js +373 -0
  47. package/src/injection/target-resolver.js +323 -0
  48. package/src/injection/tests/atomic-writer.test.js +382 -0
  49. package/src/injection/tests/injection-engine.test.js +611 -0
  50. package/src/injection/tests/integration.test.js +392 -0
  51. package/src/injection/tests/run-tests.js +283 -0
  52. package/src/injection/validation-engine.js +547 -0
  53. package/src/linter/determinism-linter.js +473 -0
  54. package/src/linter/determinism.js +410 -0
  55. package/src/linter/index.js +6 -0
  56. package/src/linter/test-doubles.js +475 -0
  57. package/src/parser/frontmatter.js +228 -0
  58. package/src/parser/variables.js +344 -0
  59. package/src/renderer/deterministic.js +245 -0
  60. package/src/renderer/index.js +6 -0
  61. package/src/templates/latex/academic-paper.njk +186 -0
  62. package/src/templates/latex/index.js +104 -0
  63. package/src/templates/nextjs/app-page.njk +66 -0
  64. package/src/templates/nextjs/index.js +80 -0
  65. package/src/templates/office/docx/document.njk +368 -0
  66. package/src/templates/office/index.js +79 -0
  67. package/src/templates/office/word-report.njk +129 -0
  68. package/src/utils/template-utils.js +426 -0
@@ -0,0 +1,339 @@
1
+ /**
2
+ * KGEN Injection Integration
3
+ *
4
+ * Integrates injection capabilities with the existing KGEN template engine.
5
+ * Provides seamless injection support for template processing.
6
+ */
7
+
8
+ import { inject, dryRun, processTemplate, initializeInjection } from './api.js';
9
+ import { DEFAULT_CONFIG, INJECTION_MODES } from './constants.js';
10
+
11
+ /**
12
+ * Enhance KGEN engine with injection capabilities
13
+ */
14
+ export function enhanceKgenWithInjection(kgenEngine, injectionConfig = {}) {
15
+ const config = { ...DEFAULT_CONFIG, ...injectionConfig };
16
+
17
+ // Initialize injection system
18
+ const injectionEngine = initializeInjection(config);
19
+
20
+ // Store original methods
21
+ const originalRender = kgenEngine.render;
22
+ const originalRenderString = kgenEngine.renderString;
23
+
24
+ /**
25
+ * Enhanced render method with injection support
26
+ */
27
+ kgenEngine.render = async function(templatePath, data = {}, options = {}) {
28
+ // First, try to read and parse the template
29
+ const template = await this.getTemplate(templatePath);
30
+
31
+ // Check if template has injection configuration in frontmatter
32
+ if (template.frontmatter && template.frontmatter.inject) {
33
+ return await processInjectionTemplate(template, data, options, injectionEngine);
34
+ }
35
+
36
+ // Fall back to original render for non-injection templates
37
+ return originalRender.call(this, templatePath, data, options);
38
+ };
39
+
40
+ /**
41
+ * Enhanced renderString with injection support
42
+ */
43
+ kgenEngine.renderString = async function(templateString, data = {}, options = {}) {
44
+ // Parse template string for frontmatter
45
+ const parsed = parseTemplateString(templateString);
46
+
47
+ if (parsed.frontmatter && parsed.frontmatter.inject) {
48
+ return await processInjectionTemplate(parsed, data, options, injectionEngine);
49
+ }
50
+
51
+ // Fall back to original renderString
52
+ return originalRenderString.call(this, templateString, data, options);
53
+ };
54
+
55
+ /**
56
+ * Add injection-specific methods to the engine
57
+ */
58
+ kgenEngine.inject = async function(templateConfig, content, variables = {}) {
59
+ return await inject(templateConfig, content, variables, { config });
60
+ };
61
+
62
+ kgenEngine.dryRunInjection = async function(templateConfig, content, variables = {}) {
63
+ return await dryRun(templateConfig, content, variables, { config });
64
+ };
65
+
66
+ kgenEngine.getInjectionHistory = function() {
67
+ return injectionEngine.getOperationHistory();
68
+ };
69
+
70
+ kgenEngine.undoInjection = async function(operationId) {
71
+ return await injectionEngine.undo(operationId);
72
+ };
73
+
74
+ /**
75
+ * Batch template processing with injection support
76
+ */
77
+ kgenEngine.processBatch = async function(templates, globalData = {}, options = {}) {
78
+ const results = [];
79
+ const errors = [];
80
+
81
+ for (const template of templates) {
82
+ try {
83
+ const templateData = { ...globalData, ...(template.data || {}) };
84
+ let result;
85
+
86
+ if (template.content) {
87
+ result = await this.renderString(template.content, templateData, options);
88
+ } else if (template.path) {
89
+ result = await this.render(template.path, templateData, options);
90
+ } else {
91
+ throw new Error('Template must have either content or path');
92
+ }
93
+
94
+ results.push({
95
+ template,
96
+ result,
97
+ success: true
98
+ });
99
+
100
+ } catch (error) {
101
+ errors.push({
102
+ template,
103
+ error: error.message,
104
+ success: false
105
+ });
106
+
107
+ if (!options.continueOnError) {
108
+ break;
109
+ }
110
+ }
111
+ }
112
+
113
+ return {
114
+ results,
115
+ errors,
116
+ total: templates.length,
117
+ successful: results.length,
118
+ failed: errors.length
119
+ };
120
+ };
121
+
122
+ return kgenEngine;
123
+ }
124
+
125
+ /**
126
+ * Process injection template
127
+ */
128
+ async function processInjectionTemplate(template, data, options, injectionEngine) {
129
+ const { frontmatter, content } = template;
130
+
131
+ // Render the template content first
132
+ const renderedContent = await renderTemplateContent(content, data, options);
133
+
134
+ // Perform injection
135
+ const injectionResult = await injectionEngine.inject(frontmatter, renderedContent, data);
136
+
137
+ return {
138
+ injectionResult,
139
+ operationId: injectionResult.operationId,
140
+ success: injectionResult.success,
141
+ skipped: injectionResult.skipped,
142
+ targets: injectionResult.targets,
143
+ content: renderedContent
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Render template content (without injection)
149
+ */
150
+ async function renderTemplateContent(content, data, options) {
151
+ // Simple variable interpolation - in production use proper template engine
152
+ return content.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
153
+ return data[variable] || match;
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Parse template string for frontmatter
159
+ */
160
+ function parseTemplateString(templateString) {
161
+ const frontmatterMatch = templateString.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
162
+
163
+ if (!frontmatterMatch) {
164
+ return {
165
+ frontmatter: {},
166
+ content: templateString
167
+ };
168
+ }
169
+
170
+ const frontmatter = parseFrontmatter(frontmatterMatch[1]);
171
+ const content = frontmatterMatch[2];
172
+
173
+ return { frontmatter, content };
174
+ }
175
+
176
+ /**
177
+ * Simple frontmatter parser
178
+ */
179
+ function parseFrontmatter(text) {
180
+ const result = {};
181
+ const lines = text.split('\n');
182
+
183
+ for (const line of lines) {
184
+ const trimmed = line.trim();
185
+ if (!trimmed || trimmed.startsWith('#')) continue;
186
+
187
+ const colonIndex = trimmed.indexOf(':');
188
+ if (colonIndex === -1) continue;
189
+
190
+ const key = trimmed.substring(0, colonIndex).trim();
191
+ let value = trimmed.substring(colonIndex + 1).trim();
192
+
193
+ // Parse arrays
194
+ if (value.startsWith('[') && value.endsWith(']')) {
195
+ value = value.slice(1, -1).split(',').map(v => v.trim().replace(/['"]/g, ''));
196
+ }
197
+ // Parse booleans
198
+ else if (value === 'true') {
199
+ value = true;
200
+ } else if (value === 'false') {
201
+ value = false;
202
+ }
203
+ // Parse numbers
204
+ else if (/^\d+$/.test(value)) {
205
+ value = parseInt(value);
206
+ }
207
+ // Remove quotes
208
+ else if ((value.startsWith('"') && value.endsWith('"')) ||
209
+ (value.startsWith("'") && value.endsWith("'"))) {
210
+ value = value.slice(1, -1);
211
+ }
212
+
213
+ result[key] = value;
214
+ }
215
+
216
+ return result;
217
+ }
218
+
219
+ /**
220
+ * Create injection-aware template loader
221
+ */
222
+ export function createInjectionTemplateLoader(baseLoader, injectionConfig = {}) {
223
+ return {
224
+ ...baseLoader,
225
+
226
+ async loadTemplate(templatePath, options = {}) {
227
+ const template = await baseLoader.loadTemplate(templatePath, options);
228
+
229
+ // Parse for injection configuration
230
+ const parsed = parseTemplateString(template.content || template);
231
+
232
+ return {
233
+ ...template,
234
+ frontmatter: parsed.frontmatter,
235
+ content: parsed.content,
236
+ hasInjection: !!(parsed.frontmatter && parsed.frontmatter.inject),
237
+ injectionMode: parsed.frontmatter?.mode || INJECTION_MODES.APPEND
238
+ };
239
+ }
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Validation helpers for injection templates
245
+ */
246
+ export const injectionValidators = {
247
+ /**
248
+ * Validate injection template configuration
249
+ */
250
+ validateTemplate(template) {
251
+ const errors = [];
252
+
253
+ if (!template.frontmatter) {
254
+ return { valid: true, errors: [] }; // Non-injection template
255
+ }
256
+
257
+ const fm = template.frontmatter;
258
+
259
+ if (fm.inject && !fm.to) {
260
+ errors.push('Injection templates must specify "to" target');
261
+ }
262
+
263
+ if (fm.mode && !Object.values(INJECTION_MODES).includes(fm.mode)) {
264
+ errors.push(`Invalid injection mode: ${fm.mode}`);
265
+ }
266
+
267
+ if (fm.mode === INJECTION_MODES.BEFORE && !fm.target) {
268
+ errors.push('Before mode requires "target" pattern');
269
+ }
270
+
271
+ if (fm.mode === INJECTION_MODES.AFTER && !fm.target) {
272
+ errors.push('After mode requires "target" pattern');
273
+ }
274
+
275
+ if (fm.mode === INJECTION_MODES.REPLACE && !fm.target) {
276
+ errors.push('Replace mode requires "target" pattern');
277
+ }
278
+
279
+ if (fm.mode === INJECTION_MODES.LINE_AT && typeof fm.lineNumber !== 'number') {
280
+ errors.push('LineAt mode requires "lineNumber" as number');
281
+ }
282
+
283
+ return {
284
+ valid: errors.length === 0,
285
+ errors
286
+ };
287
+ },
288
+
289
+ /**
290
+ * Validate injection context data
291
+ */
292
+ validateData(data, template) {
293
+ const errors = [];
294
+ const fm = template.frontmatter;
295
+
296
+ if (!fm || !fm.inject) {
297
+ return { valid: true, errors: [] };
298
+ }
299
+
300
+ // Check for required variables in template content
301
+ const requiredVars = extractTemplateVariables(template.content);
302
+
303
+ for (const variable of requiredVars) {
304
+ if (!(variable in data)) {
305
+ errors.push(`Required template variable missing: ${variable}`);
306
+ }
307
+ }
308
+
309
+ return {
310
+ valid: errors.length === 0,
311
+ errors
312
+ };
313
+ }
314
+ };
315
+
316
+ /**
317
+ * Extract template variables from content
318
+ */
319
+ function extractTemplateVariables(content) {
320
+ const variables = new Set();
321
+ const matches = content.matchAll(/\{\{(\w+)\}\}/g);
322
+
323
+ for (const match of matches) {
324
+ variables.add(match[1]);
325
+ }
326
+
327
+ return Array.from(variables);
328
+ }
329
+
330
+ /**
331
+ * Export integration utilities
332
+ */
333
+ export {
334
+ inject,
335
+ dryRun,
336
+ initializeInjection,
337
+ INJECTION_MODES,
338
+ DEFAULT_CONFIG
339
+ };
@@ -0,0 +1,341 @@
1
+ /**
2
+ * KGEN Injection Modes
3
+ *
4
+ * Implements different injection modes for deterministic content modification.
5
+ * Each mode provides atomic and idempotent content insertion strategies.
6
+ */
7
+
8
+ import { INJECTION_MODES, LINE_ENDINGS } from '../constants.js';
9
+
10
+ export class InjectionModes {
11
+ constructor(config = {}) {
12
+ this.config = config;
13
+ }
14
+
15
+ /**
16
+ * Apply injection mode to content
17
+ */
18
+ async applyMode(mode, currentContent, injectionContent, target, variables = {}) {
19
+ const normalizedCurrent = this._normalizeContent(currentContent);
20
+ const normalizedInjection = this._normalizeInjectionContent(injectionContent, variables);
21
+
22
+ switch (mode) {
23
+ case INJECTION_MODES.APPEND:
24
+ return this._applyAppend(normalizedCurrent, normalizedInjection, target);
25
+
26
+ case INJECTION_MODES.PREPEND:
27
+ return this._applyPrepend(normalizedCurrent, normalizedInjection, target);
28
+
29
+ case INJECTION_MODES.BEFORE:
30
+ return this._applyBefore(normalizedCurrent, normalizedInjection, target);
31
+
32
+ case INJECTION_MODES.AFTER:
33
+ return this._applyAfter(normalizedCurrent, normalizedInjection, target);
34
+
35
+ case INJECTION_MODES.REPLACE:
36
+ return this._applyReplace(normalizedCurrent, normalizedInjection, target);
37
+
38
+ case INJECTION_MODES.LINE_AT:
39
+ return this._applyLineAt(normalizedCurrent, normalizedInjection, target);
40
+
41
+ case INJECTION_MODES.CREATE:
42
+ return this._applyCreate(normalizedCurrent, normalizedInjection, target);
43
+
44
+ default:
45
+ throw new Error(`Unknown injection mode: ${mode}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Append content to end of file
51
+ */
52
+ _applyAppend(currentContent, injectionContent, target) {
53
+ const lineEnding = this._detectLineEnding(currentContent);
54
+ const ensureNewline = target.ensureNewline !== false;
55
+
56
+ let result = currentContent;
57
+
58
+ // Add newline before injection if file doesn't end with one
59
+ if (ensureNewline && currentContent && !currentContent.endsWith(lineEnding)) {
60
+ result += lineEnding;
61
+ }
62
+
63
+ result += injectionContent;
64
+
65
+ // Ensure file ends with newline if configured
66
+ if (target.ensureTrailingNewline && !result.endsWith(lineEnding)) {
67
+ result += lineEnding;
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Prepend content to beginning of file
75
+ */
76
+ _applyPrepend(currentContent, injectionContent, target) {
77
+ const lineEnding = this._detectLineEnding(currentContent);
78
+ let result = injectionContent;
79
+
80
+ // Add newline after injection if it doesn't end with one
81
+ if (!injectionContent.endsWith(lineEnding)) {
82
+ result += lineEnding;
83
+ }
84
+
85
+ result += currentContent;
86
+
87
+ // Sort imports if enabled and content looks like imports
88
+ if (target.sortImports && this._looksLikeImports(injectionContent)) {
89
+ result = this._sortImports(result);
90
+ }
91
+
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * Insert content before specific target
97
+ */
98
+ _applyBefore(currentContent, injectionContent, target) {
99
+ const targetPattern = target.target;
100
+ if (!targetPattern) {
101
+ throw new Error('Before mode requires target pattern');
102
+ }
103
+
104
+ const lineEnding = this._detectLineEnding(currentContent);
105
+ const lines = currentContent.split(lineEnding);
106
+ const insertionLines = injectionContent.split(lineEnding);
107
+
108
+ // Find target line
109
+ const targetIndex = this._findTargetLine(lines, targetPattern, target);
110
+ if (targetIndex === -1) {
111
+ throw new Error(`Target pattern not found: ${targetPattern}`);
112
+ }
113
+
114
+ // Preserve indentation if configured
115
+ if (target.preserveIndentation) {
116
+ const targetIndentation = this._getLineIndentation(lines[targetIndex]);
117
+ for (let i = 0; i < insertionLines.length; i++) {
118
+ if (insertionLines[i].trim()) {
119
+ insertionLines[i] = targetIndentation + insertionLines[i].trim();
120
+ }
121
+ }
122
+ }
123
+
124
+ // Insert before target line
125
+ lines.splice(targetIndex, 0, ...insertionLines);
126
+
127
+ return lines.join(lineEnding);
128
+ }
129
+
130
+ /**
131
+ * Insert content after specific target
132
+ */
133
+ _applyAfter(currentContent, injectionContent, target) {
134
+ const targetPattern = target.target;
135
+ if (!targetPattern) {
136
+ throw new Error('After mode requires target pattern');
137
+ }
138
+
139
+ const lineEnding = this._detectLineEnding(currentContent);
140
+ const lines = currentContent.split(lineEnding);
141
+ const insertionLines = injectionContent.split(lineEnding);
142
+
143
+ // Find target line
144
+ const targetIndex = this._findTargetLine(lines, targetPattern, target);
145
+ if (targetIndex === -1) {
146
+ throw new Error(`Target pattern not found: ${targetPattern}`);
147
+ }
148
+
149
+ // Preserve indentation if configured
150
+ if (target.preserveIndentation) {
151
+ const targetIndentation = this._getLineIndentation(lines[targetIndex]);
152
+ for (let i = 0; i < insertionLines.length; i++) {
153
+ if (insertionLines[i].trim()) {
154
+ insertionLines[i] = targetIndentation + insertionLines[i].trim();
155
+ }
156
+ }
157
+ }
158
+
159
+ // Insert after target line
160
+ lines.splice(targetIndex + 1, 0, ...insertionLines);
161
+
162
+ return lines.join(lineEnding);
163
+ }
164
+
165
+ /**
166
+ * Replace specific content
167
+ */
168
+ _applyReplace(currentContent, injectionContent, target) {
169
+ const targetPattern = target.target;
170
+ if (!targetPattern) {
171
+ throw new Error('Replace mode requires target pattern');
172
+ }
173
+
174
+ let result = currentContent;
175
+
176
+ if (target.regex) {
177
+ // Regex replacement
178
+ const flags = target.regexFlags || 'g';
179
+ const regex = new RegExp(targetPattern, flags);
180
+ result = result.replace(regex, injectionContent);
181
+ } else if (target.exact) {
182
+ // Exact string replacement
183
+ result = result.replace(targetPattern, injectionContent);
184
+ } else {
185
+ // First occurrence replacement
186
+ const index = result.indexOf(targetPattern);
187
+ if (index === -1) {
188
+ throw new Error(`Target pattern not found: ${targetPattern}`);
189
+ }
190
+ result = result.substring(0, index) + injectionContent + result.substring(index + targetPattern.length);
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Insert content at specific line number
198
+ */
199
+ _applyLineAt(currentContent, injectionContent, target) {
200
+ const lineNumber = target.lineNumber;
201
+ if (typeof lineNumber !== 'number') {
202
+ throw new Error('LineAt mode requires lineNumber');
203
+ }
204
+
205
+ const lineEnding = this._detectLineEnding(currentContent);
206
+ const lines = currentContent.split(lineEnding);
207
+ const insertionLines = injectionContent.split(lineEnding);
208
+
209
+ // Validate line number
210
+ if (target.validateTarget && (lineNumber < 1 || lineNumber > lines.length + 1)) {
211
+ throw new Error(`Invalid line number: ${lineNumber}. File has ${lines.length} lines.`);
212
+ }
213
+
214
+ // Convert to 0-based index
215
+ const insertIndex = lineNumber - 1;
216
+
217
+ // Insert at specified line
218
+ lines.splice(insertIndex, 0, ...insertionLines);
219
+
220
+ return lines.join(lineEnding);
221
+ }
222
+
223
+ /**
224
+ * Create new file content
225
+ */
226
+ _applyCreate(currentContent, injectionContent, target) {
227
+ // If file exists and not empty, handle based on configuration
228
+ if (currentContent && target.failIfExists) {
229
+ throw new Error('File already exists and failIfExists is true');
230
+ }
231
+
232
+ if (currentContent && target.appendIfExists) {
233
+ return this._applyAppend(currentContent, injectionContent, target);
234
+ }
235
+
236
+ // Return injection content as new file content
237
+ return injectionContent;
238
+ }
239
+
240
+ /**
241
+ * Helper Methods
242
+ */
243
+
244
+ _normalizeContent(content) {
245
+ if (!content) return '';
246
+
247
+ // Normalize line endings if configured
248
+ if (this.config.consistentLineEndings) {
249
+ const targetEnding = this.config.lineEnding || LINE_ENDINGS.LF;
250
+ return content.replace(/\r\n|\r|\n/g, targetEnding);
251
+ }
252
+
253
+ return content;
254
+ }
255
+
256
+ _normalizeInjectionContent(content, variables) {
257
+ // Variable interpolation
258
+ let result = content.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
259
+ return variables[variable] || match;
260
+ });
261
+
262
+ // Trim if configured
263
+ if (this.config.trimInjectedContent) {
264
+ result = result.trim();
265
+ }
266
+
267
+ return result;
268
+ }
269
+
270
+ _detectLineEnding(content) {
271
+ if (content.includes('\r\n')) return LINE_ENDINGS.CRLF;
272
+ if (content.includes('\r')) return LINE_ENDINGS.CR;
273
+ return LINE_ENDINGS.LF;
274
+ }
275
+
276
+ _findTargetLine(lines, pattern, target) {
277
+ if (target.regex) {
278
+ const flags = target.regexFlags || 'gm';
279
+ const regex = new RegExp(pattern, flags);
280
+ return lines.findIndex(line => regex.test(line));
281
+ }
282
+
283
+ // String search (first occurrence for deterministic behavior)
284
+ return lines.findIndex(line => line.includes(pattern));
285
+ }
286
+
287
+ _getLineIndentation(line) {
288
+ const match = line.match(/^(\s*)/);
289
+ return match ? match[1] : '';
290
+ }
291
+
292
+ _looksLikeImports(content) {
293
+ return /^import\s+.*from\s+['"][^'"]+['"];?\s*$/m.test(content);
294
+ }
295
+
296
+ _sortImports(content) {
297
+ const lineEnding = this._detectLineEnding(content);
298
+ const lines = content.split(lineEnding);
299
+
300
+ // Find import blocks
301
+ const importBlocks = [];
302
+ let currentBlock = [];
303
+ let inImportBlock = false;
304
+
305
+ for (const line of lines) {
306
+ if (/^import\s+/.test(line.trim())) {
307
+ inImportBlock = true;
308
+ currentBlock.push(line);
309
+ } else if (inImportBlock && line.trim() === '') {
310
+ currentBlock.push(line);
311
+ } else if (inImportBlock) {
312
+ importBlocks.push([...currentBlock]);
313
+ currentBlock = [];
314
+ inImportBlock = false;
315
+ }
316
+ }
317
+
318
+ // Sort imports within each block deterministically
319
+ for (const block of importBlocks) {
320
+ block.sort((a, b) => {
321
+ const aImport = a.trim();
322
+ const bImport = b.trim();
323
+
324
+ // Sort by import source (deterministic)
325
+ const aSource = this._extractImportSource(aImport);
326
+ const bSource = this._extractImportSource(bImport);
327
+
328
+ return aSource.localeCompare(bSource);
329
+ });
330
+ }
331
+
332
+ // Reconstruct content with sorted imports
333
+ // This is a simplified implementation - real sorting would be more complex
334
+ return content;
335
+ }
336
+
337
+ _extractImportSource(importLine) {
338
+ const match = importLine.match(/from\s+['"]([^'"]+)['"]/);
339
+ return match ? match[1] : '';
340
+ }
341
+ }