@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,373 @@
1
+ /**
2
+ * KGEN Rollback Manager
3
+ *
4
+ * Manages rollback operations for failed injections, including
5
+ * backup restoration and operation undo capabilities.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import { join, dirname, basename } from 'path';
10
+ import { createHash } from 'crypto';
11
+
12
+ import { ERROR_CODES, CHECKSUM_ALGORITHMS } from './constants.js';
13
+
14
+ export class RollbackManager {
15
+ constructor(config = {}) {
16
+ this.config = config;
17
+ this.rollbackHistory = new Map();
18
+ this.operationBackups = new Map();
19
+ }
20
+
21
+ /**
22
+ * Rollback a failed operation
23
+ */
24
+ async rollbackOperation(operationId, operationData) {
25
+ const rollbackEntry = {
26
+ operationId,
27
+ timestamp: Date.now(),
28
+ phase: 'starting',
29
+ restoredFiles: [],
30
+ errors: []
31
+ };
32
+
33
+ try {
34
+ rollbackEntry.phase = 'restoring-files';
35
+
36
+ // Restore files from backups
37
+ if (operationData.backups && operationData.backups.length > 0) {
38
+ for (const backup of operationData.backups) {
39
+ try {
40
+ await this._restoreFromBackup(backup.filePath, backup.backupPath);
41
+ rollbackEntry.restoredFiles.push({
42
+ file: backup.filePath,
43
+ backup: backup.backupPath,
44
+ success: true
45
+ });
46
+ } catch (error) {
47
+ rollbackEntry.errors.push({
48
+ file: backup.filePath,
49
+ error: error.message
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ // Clean up temporary files
56
+ rollbackEntry.phase = 'cleanup';
57
+ await this._cleanupTemporaryFiles(operationData);
58
+
59
+ // Release locks
60
+ await this._releaseLocks(operationData);
61
+
62
+ rollbackEntry.phase = 'completed';
63
+ rollbackEntry.success = rollbackEntry.errors.length === 0;
64
+
65
+ this.rollbackHistory.set(operationId, rollbackEntry);
66
+
67
+ return {
68
+ success: rollbackEntry.success,
69
+ filesRestored: rollbackEntry.restoredFiles.length,
70
+ errors: rollbackEntry.errors
71
+ };
72
+
73
+ } catch (error) {
74
+ rollbackEntry.phase = 'failed';
75
+ rollbackEntry.error = error.message;
76
+ this.rollbackHistory.set(operationId, rollbackEntry);
77
+
78
+ throw new Error(`Rollback failed: ${error.message}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Undo a completed operation
84
+ */
85
+ async undoOperation(operationId) {
86
+ const operationHistory = this._getOperationHistory(operationId);
87
+ if (!operationHistory) {
88
+ throw new Error(`Operation ${operationId} not found in history`);
89
+ }
90
+
91
+ if (operationHistory.phase !== 'committed') {
92
+ throw new Error(`Cannot undo operation ${operationId} - not in committed state`);
93
+ }
94
+
95
+ const undoEntry = {
96
+ originalOperationId: operationId,
97
+ undoOperationId: `undo-${operationId}`,
98
+ timestamp: Date.now(),
99
+ phase: 'starting',
100
+ restoredFiles: [],
101
+ errors: []
102
+ };
103
+
104
+ try {
105
+ // Find backups for this operation
106
+ const backups = this._findOperationBackups(operationId);
107
+ if (backups.length === 0) {
108
+ throw new Error(`No backups found for operation ${operationId}`);
109
+ }
110
+
111
+ undoEntry.phase = 'restoring';
112
+
113
+ for (const backup of backups) {
114
+ try {
115
+ await this._restoreFromBackup(backup.targetPath, backup.backupPath);
116
+ undoEntry.restoredFiles.push({
117
+ file: backup.targetPath,
118
+ backup: backup.backupPath,
119
+ success: true
120
+ });
121
+ } catch (error) {
122
+ undoEntry.errors.push({
123
+ file: backup.targetPath,
124
+ error: error.message
125
+ });
126
+ }
127
+ }
128
+
129
+ undoEntry.phase = 'completed';
130
+ undoEntry.success = undoEntry.errors.length === 0;
131
+
132
+ // Mark original operation as undone
133
+ operationHistory.undone = true;
134
+ operationHistory.undoTimestamp = Date.now();
135
+
136
+ this.rollbackHistory.set(undoEntry.undoOperationId, undoEntry);
137
+
138
+ return {
139
+ success: undoEntry.success,
140
+ filesRestored: undoEntry.restoredFiles.length,
141
+ errors: undoEntry.errors,
142
+ operationId: undoEntry.undoOperationId
143
+ };
144
+
145
+ } catch (error) {
146
+ undoEntry.phase = 'failed';
147
+ undoEntry.error = error.message;
148
+ this.rollbackHistory.set(undoEntry.undoOperationId, undoEntry);
149
+
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Register backup for tracking
156
+ */
157
+ registerBackup(operationId, targetPath, backupPath, checksum) {
158
+ if (!this.operationBackups.has(operationId)) {
159
+ this.operationBackups.set(operationId, []);
160
+ }
161
+
162
+ this.operationBackups.get(operationId).push({
163
+ targetPath,
164
+ backupPath,
165
+ checksum,
166
+ timestamp: Date.now()
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Get rollback history
172
+ */
173
+ getRollbackHistory(operationId = null) {
174
+ if (operationId) {
175
+ return this.rollbackHistory.get(operationId);
176
+ }
177
+ return Array.from(this.rollbackHistory.values());
178
+ }
179
+
180
+ /**
181
+ * Clean up old backups
182
+ */
183
+ async cleanupOldBackups(maxAge = 24 * 60 * 60 * 1000) { // 24 hours default
184
+ const cutoffTime = Date.now() - maxAge;
185
+ const toCleanup = [];
186
+
187
+ for (const [operationId, backups] of this.operationBackups.entries()) {
188
+ const oldBackups = backups.filter(backup => backup.timestamp < cutoffTime);
189
+
190
+ for (const backup of oldBackups) {
191
+ try {
192
+ await fs.unlink(backup.backupPath);
193
+ toCleanup.push(backup);
194
+ } catch (error) {
195
+ console.warn(`Failed to cleanup backup ${backup.backupPath}:`, error.message);
196
+ }
197
+ }
198
+
199
+ // Remove cleaned up backups from tracking
200
+ const remainingBackups = backups.filter(backup => backup.timestamp >= cutoffTime);
201
+ if (remainingBackups.length === 0) {
202
+ this.operationBackups.delete(operationId);
203
+ } else {
204
+ this.operationBackups.set(operationId, remainingBackups);
205
+ }
206
+ }
207
+
208
+ return {
209
+ cleanedBackups: toCleanup.length,
210
+ remainingOperations: this.operationBackups.size
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Private Methods
216
+ */
217
+
218
+ async _restoreFromBackup(targetPath, backupPath) {
219
+ // Verify backup exists
220
+ try {
221
+ await fs.access(backupPath);
222
+ } catch (error) {
223
+ throw new Error(`Backup file not found: ${backupPath}`);
224
+ }
225
+
226
+ // Verify backup integrity if checksum available
227
+ const backupChecksum = await this._calculateFileChecksum(backupPath);
228
+
229
+ // Create target directory if it doesn't exist
230
+ const targetDir = dirname(targetPath);
231
+ await fs.mkdir(targetDir, { recursive: true });
232
+
233
+ // Copy backup to target (atomic operation)
234
+ const tempPath = `${targetPath}.kgen-restore-${Date.now()}`;
235
+
236
+ try {
237
+ await fs.copyFile(backupPath, tempPath);
238
+ await fs.rename(tempPath, targetPath);
239
+
240
+ // Verify restoration
241
+ const restoredChecksum = await this._calculateFileChecksum(targetPath);
242
+ if (restoredChecksum !== backupChecksum) {
243
+ throw new Error('Restored file checksum does not match backup');
244
+ }
245
+
246
+ } catch (error) {
247
+ // Clean up temp file on failure
248
+ try {
249
+ await fs.unlink(tempPath);
250
+ } catch (cleanupError) {
251
+ console.warn('Failed to cleanup temp restore file:', tempPath);
252
+ }
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ async _cleanupTemporaryFiles(operationData) {
258
+ // Clean up any temporary files created during operation
259
+ if (operationData.tempFiles) {
260
+ for (const tempFile of operationData.tempFiles) {
261
+ try {
262
+ await fs.unlink(tempFile);
263
+ } catch (error) {
264
+ console.warn(`Failed to cleanup temp file ${tempFile}:`, error.message);
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ async _releaseLocks(operationData) {
271
+ // Release any locks held by the operation
272
+ if (operationData.locks) {
273
+ for (const lockFile of operationData.locks) {
274
+ try {
275
+ await fs.unlink(lockFile);
276
+ } catch (error) {
277
+ console.warn(`Failed to release lock ${lockFile}:`, error.message);
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ _getOperationHistory(operationId) {
284
+ // This would normally come from the injection engine's history
285
+ // For now, return a mock structure
286
+ return {
287
+ id: operationId,
288
+ phase: 'committed',
289
+ results: [],
290
+ metadata: {}
291
+ };
292
+ }
293
+
294
+ _findOperationBackups(operationId) {
295
+ return this.operationBackups.get(operationId) || [];
296
+ }
297
+
298
+ async _calculateFileChecksum(filePath, algorithm = CHECKSUM_ALGORITHMS.SHA256) {
299
+ const content = await fs.readFile(filePath);
300
+ const hash = createHash(algorithm);
301
+ hash.update(content);
302
+ return hash.digest('hex');
303
+ }
304
+
305
+ /**
306
+ * Verify operation can be undone
307
+ */
308
+ async verifyUndoable(operationId) {
309
+ const operationHistory = this._getOperationHistory(operationId);
310
+ if (!operationHistory) {
311
+ return { undoable: false, reason: 'Operation not found' };
312
+ }
313
+
314
+ if (operationHistory.undone) {
315
+ return { undoable: false, reason: 'Operation already undone' };
316
+ }
317
+
318
+ if (operationHistory.phase !== 'committed') {
319
+ return { undoable: false, reason: 'Operation not completed successfully' };
320
+ }
321
+
322
+ const backups = this._findOperationBackups(operationId);
323
+ if (backups.length === 0) {
324
+ return { undoable: false, reason: 'No backups available' };
325
+ }
326
+
327
+ // Verify all backup files exist
328
+ for (const backup of backups) {
329
+ try {
330
+ await fs.access(backup.backupPath);
331
+ } catch (error) {
332
+ return { undoable: false, reason: `Backup file missing: ${backup.backupPath}` };
333
+ }
334
+ }
335
+
336
+ return { undoable: true, backupsCount: backups.length };
337
+ }
338
+
339
+ /**
340
+ * Create operation snapshot for recovery
341
+ */
342
+ async createOperationSnapshot(operationId, targets) {
343
+ const snapshot = {
344
+ operationId,
345
+ timestamp: Date.now(),
346
+ targets: [],
347
+ checksums: new Map()
348
+ };
349
+
350
+ for (const target of targets) {
351
+ try {
352
+ const exists = await fs.access(target.resolvedPath).then(() => true, () => false);
353
+ const targetInfo = {
354
+ path: target.resolvedPath,
355
+ exists,
356
+ checksum: null,
357
+ stats: null
358
+ };
359
+
360
+ if (exists) {
361
+ targetInfo.checksum = await this._calculateFileChecksum(target.resolvedPath);
362
+ targetInfo.stats = await fs.stat(target.resolvedPath);
363
+ }
364
+
365
+ snapshot.targets.push(targetInfo);
366
+ } catch (error) {
367
+ console.warn(`Failed to snapshot target ${target.resolvedPath}:`, error.message);
368
+ }
369
+ }
370
+
371
+ return snapshot;
372
+ }
373
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * KGEN Target Resolver
3
+ *
4
+ * Resolves injection targets from template configuration with deterministic
5
+ * path resolution, glob pattern matching, and security validation.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import { resolve, join, relative, dirname, basename } from 'path';
10
+ // Simple glob implementation for basic pattern matching
11
+ // In production, use a proper glob library like 'glob' or 'fast-glob'
12
+ function simpleGlob(pattern, options = {}) {
13
+ // Very basic glob implementation for demo purposes
14
+ return Promise.resolve([]);
15
+ }
16
+ const glob = simpleGlob;
17
+
18
+ import { ERROR_CODES, DEFAULT_CONFIG } from './constants.js';
19
+
20
+ export class TargetResolver {
21
+ constructor(config = {}) {
22
+ this.config = { ...DEFAULT_CONFIG, ...config };
23
+ this.projectRoot = config.projectRoot || process.cwd();
24
+ }
25
+
26
+ /**
27
+ * Resolve all targets from template configuration
28
+ */
29
+ async resolveTargets(templateConfig, variables = {}) {
30
+ const targets = [];
31
+
32
+ // Handle single target
33
+ if (templateConfig.to) {
34
+ const target = await this._resolveSingleTarget(templateConfig, variables);
35
+ targets.push(target);
36
+ }
37
+
38
+ // Handle multiple targets
39
+ if (templateConfig.targets && Array.isArray(templateConfig.targets)) {
40
+ for (const targetConfig of templateConfig.targets) {
41
+ const target = await this._resolveSingleTarget(targetConfig, variables);
42
+ targets.push(target);
43
+ }
44
+ }
45
+
46
+ if (targets.length === 0) {
47
+ throw new Error('No injection targets specified');
48
+ }
49
+
50
+ // Sort targets for deterministic processing
51
+ targets.sort((a, b) => a.resolvedPath.localeCompare(b.resolvedPath));
52
+
53
+ return targets;
54
+ }
55
+
56
+ /**
57
+ * Private Methods
58
+ */
59
+
60
+ async _resolveSingleTarget(targetConfig, variables) {
61
+ let targetPath = targetConfig.to;
62
+
63
+ if (!targetPath) {
64
+ throw new Error('Target path (to) is required');
65
+ }
66
+
67
+ // Variable interpolation
68
+ targetPath = this._interpolateVariables(targetPath, variables);
69
+
70
+ // Handle glob patterns
71
+ if (this._isGlobPattern(targetPath)) {
72
+ return await this._resolveGlobTargets(targetPath, targetConfig, variables);
73
+ }
74
+
75
+ // Resolve single path
76
+ const resolvedPath = this._resolvePath(targetPath);
77
+
78
+ // Security validation
79
+ this._validatePath(resolvedPath);
80
+
81
+ // Create target object
82
+ const target = {
83
+ originalPath: targetConfig.to,
84
+ resolvedPath,
85
+ mode: targetConfig.inject ? targetConfig.mode || 'append' : 'create',
86
+ ...targetConfig
87
+ };
88
+
89
+ // Apply content-based filtering if configured
90
+ if (targetConfig.targetIf) {
91
+ const shouldTarget = await this._evaluateTargetCondition(target, targetConfig.targetIf);
92
+ if (!shouldTarget) {
93
+ return null; // Skip this target
94
+ }
95
+ }
96
+
97
+ return target;
98
+ }
99
+
100
+ async _resolveGlobTargets(globPattern, targetConfig, variables) {
101
+ // Use glob to find matching files
102
+ const matches = await glob(globPattern, {
103
+ cwd: this.projectRoot,
104
+ absolute: true,
105
+ dot: false // Don't match hidden files by default
106
+ });
107
+
108
+ // Apply exclusion patterns
109
+ let filteredMatches = matches;
110
+ if (targetConfig.exclude) {
111
+ filteredMatches = this._applyExclusions(matches, targetConfig.exclude);
112
+ }
113
+
114
+ // Sort for deterministic order
115
+ if (this.config.sortGlobResults) {
116
+ filteredMatches.sort();
117
+ }
118
+
119
+ // Convert to target objects
120
+ const targets = [];
121
+ for (const matchPath of filteredMatches) {
122
+ this._validatePath(matchPath);
123
+
124
+ const target = {
125
+ originalPath: targetConfig.to,
126
+ resolvedPath: matchPath,
127
+ mode: targetConfig.inject ? targetConfig.mode || 'append' : 'create',
128
+ isGlobMatch: true,
129
+ ...targetConfig
130
+ };
131
+
132
+ // Apply content-based filtering
133
+ if (targetConfig.targetIf) {
134
+ const shouldTarget = await this._evaluateTargetCondition(target, targetConfig.targetIf);
135
+ if (shouldTarget) {
136
+ targets.push(target);
137
+ }
138
+ } else {
139
+ targets.push(target);
140
+ }
141
+ }
142
+
143
+ return targets;
144
+ }
145
+
146
+ _interpolateVariables(template, variables) {
147
+ return template.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
148
+ const value = variables[variable];
149
+ if (value === undefined) {
150
+ throw new Error(`Variable '${variable}' not provided for path interpolation`);
151
+ }
152
+ return value;
153
+ });
154
+ }
155
+
156
+ _isGlobPattern(path) {
157
+ return /[*?{}\[\]]/.test(path);
158
+ }
159
+
160
+ _resolvePath(targetPath) {
161
+ // Convert to absolute path
162
+ if (!targetPath.startsWith('/')) {
163
+ return resolve(this.projectRoot, targetPath);
164
+ }
165
+ return resolve(targetPath);
166
+ }
167
+
168
+ _validatePath(resolvedPath) {
169
+ // Security: Prevent path traversal
170
+ if (this.config.preventPathTraversal) {
171
+ const relativePath = relative(this.projectRoot, resolvedPath);
172
+ if (relativePath.startsWith('..') || resolve(relativePath) === resolve('.')) {
173
+ throw new Error(`Path traversal blocked: ${resolvedPath}`, ERROR_CODES.PATH_TRAVERSAL);
174
+ }
175
+ }
176
+
177
+ // Validate file extension if configured
178
+ if (this.config.allowedExtensions && this.config.allowedExtensions.length > 0) {
179
+ const ext = this._getFileExtension(resolvedPath);
180
+ if (!this.config.allowedExtensions.includes(ext)) {
181
+ throw new Error(`File extension not allowed: ${ext}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ _applyExclusions(matches, exclusions) {
187
+ const exclusionPatterns = Array.isArray(exclusions) ? exclusions : [exclusions];
188
+
189
+ return matches.filter(match => {
190
+ const relativePath = relative(this.projectRoot, match);
191
+
192
+ return !exclusionPatterns.some(pattern => {
193
+ if (this._isGlobPattern(pattern)) {
194
+ // Use minimatch for glob pattern exclusions
195
+ return this._matchesGlob(relativePath, pattern);
196
+ } else {
197
+ // Simple string includes
198
+ return relativePath.includes(pattern);
199
+ }
200
+ });
201
+ });
202
+ }
203
+
204
+ _matchesGlob(path, pattern) {
205
+ // Simple glob matching - in production, use minimatch or similar
206
+ const regex = pattern
207
+ .replace(/\*\*/g, '.*')
208
+ .replace(/\*/g, '[^/]*')
209
+ .replace(/\?/g, '[^/]');
210
+
211
+ return new RegExp(`^${regex}$`).test(path);
212
+ }
213
+
214
+ async _evaluateTargetCondition(target, condition) {
215
+ try {
216
+ // Check if file exists first
217
+ const fileExists = await this._fileExists(target.resolvedPath);
218
+ if (!fileExists) {
219
+ return false; // Can't evaluate content conditions on non-existent files
220
+ }
221
+
222
+ // Read file content
223
+ const content = await fs.readFile(target.resolvedPath, 'utf8');
224
+
225
+ // Handle different condition types
226
+ if (typeof condition === 'string') {
227
+ // Simple string or regex match
228
+ if (condition.startsWith('/') && condition.endsWith('/')) {
229
+ // Regex pattern
230
+ const pattern = condition.slice(1, -1);
231
+ const regex = new RegExp(pattern, target.regexFlags || 'gm');
232
+ return regex.test(content);
233
+ } else {
234
+ // Simple string search
235
+ return content.includes(condition);
236
+ }
237
+ }
238
+
239
+ if (typeof condition === 'function') {
240
+ // Custom function
241
+ return await condition(target, content);
242
+ }
243
+
244
+ if (typeof condition === 'object') {
245
+ // Complex condition object
246
+ return await this._evaluateComplexCondition(target, content, condition);
247
+ }
248
+
249
+ return true;
250
+
251
+ } catch (error) {
252
+ console.warn(`Failed to evaluate target condition for ${target.resolvedPath}:`, error.message);
253
+ return false;
254
+ }
255
+ }
256
+
257
+ async _evaluateComplexCondition(target, content, condition) {
258
+ const { pattern, size, lines, encoding } = condition;
259
+
260
+ if (pattern) {
261
+ if (typeof pattern === 'string') {
262
+ return content.includes(pattern);
263
+ }
264
+ if (pattern instanceof RegExp) {
265
+ return pattern.test(content);
266
+ }
267
+ }
268
+
269
+ if (size) {
270
+ const stats = await fs.stat(target.resolvedPath);
271
+ if (size.min !== undefined && stats.size < size.min) return false;
272
+ if (size.max !== undefined && stats.size > size.max) return false;
273
+ }
274
+
275
+ if (lines) {
276
+ const lineCount = content.split('\n').length;
277
+ if (lines.min !== undefined && lineCount < lines.min) return false;
278
+ if (lines.max !== undefined && lineCount > lines.max) return false;
279
+ }
280
+
281
+ if (encoding) {
282
+ // Check if file appears to be in expected encoding
283
+ const detectedEncoding = this._detectEncoding(content);
284
+ if (detectedEncoding !== encoding) return false;
285
+ }
286
+
287
+ return true;
288
+ }
289
+
290
+ async _fileExists(filePath) {
291
+ try {
292
+ await fs.access(filePath);
293
+ return true;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
299
+ _getFileExtension(filePath) {
300
+ const name = basename(filePath);
301
+ const dotIndex = name.lastIndexOf('.');
302
+ return dotIndex === -1 ? '' : name.slice(dotIndex);
303
+ }
304
+
305
+ _detectEncoding(content) {
306
+ // Simple encoding detection - in production use a proper library
307
+ try {
308
+ // Check for UTF-8 BOM
309
+ if (content.startsWith('\uFEFF')) {
310
+ return 'utf8-bom';
311
+ }
312
+
313
+ // Check for non-ASCII characters
314
+ if (/[^\x00-\x7F]/.test(content)) {
315
+ return 'utf8';
316
+ }
317
+
318
+ return 'ascii';
319
+ } catch {
320
+ return 'unknown';
321
+ }
322
+ }
323
+ }