@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,295 @@
1
+ /**
2
+ * KGEN Idempotency Manager
3
+ *
4
+ * Manages idempotent injection operations using skipIf conditions
5
+ * to prevent duplicate content and ensure deterministic behavior.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import { createHash } from 'crypto';
10
+
11
+ import { SKIP_IF_LOGIC, CHECKSUM_ALGORITHMS } from './constants.js';
12
+
13
+ export class IdempotencyManager {
14
+ constructor(config = {}) {
15
+ this.config = config;
16
+ this.contentCache = new Map();
17
+ this.hashCache = new Map();
18
+ }
19
+
20
+ /**
21
+ * Check if injection should be skipped based on skipIf conditions
22
+ */
23
+ async shouldSkipInjection(target, content, variables) {
24
+ const skipConditions = target.skipIf;
25
+
26
+ if (!skipConditions) {
27
+ return { skip: false, reason: null };
28
+ }
29
+
30
+ // Handle different skipIf condition types
31
+ if (typeof skipConditions === 'string') {
32
+ return await this._evaluateSingleCondition(skipConditions, target, content, variables);
33
+ }
34
+
35
+ if (Array.isArray(skipConditions)) {
36
+ return await this._evaluateMultipleConditions(skipConditions, target, content, variables);
37
+ }
38
+
39
+ if (typeof skipConditions === 'object') {
40
+ return await this._evaluateObjectCondition(skipConditions, target, content, variables);
41
+ }
42
+
43
+ return { skip: false, reason: 'Invalid skipIf condition' };
44
+ }
45
+
46
+ /**
47
+ * Generate content hash for duplicate detection
48
+ */
49
+ async generateContentHash(content, target) {
50
+ const cacheKey = `${target.resolvedPath}:${content}`;
51
+
52
+ if (this.hashCache.has(cacheKey)) {
53
+ return this.hashCache.get(cacheKey);
54
+ }
55
+
56
+ const hash = createHash(CHECKSUM_ALGORITHMS.SHA256);
57
+ hash.update(content);
58
+ hash.update(target.resolvedPath);
59
+ hash.update(target.mode);
60
+
61
+ const result = hash.digest('hex');
62
+ this.hashCache.set(cacheKey, result);
63
+
64
+ return result;
65
+ }
66
+
67
+ /**
68
+ * Check if content already exists in target file
69
+ */
70
+ async contentExists(target, content, options = {}) {
71
+ try {
72
+ const fileContent = await this._getFileContent(target.resolvedPath);
73
+ const { exact = false, ignoreWhitespace = false, regex = false } = options;
74
+
75
+ if (regex) {
76
+ const pattern = new RegExp(content, options.regexFlags || 'gm');
77
+ return pattern.test(fileContent);
78
+ }
79
+
80
+ let searchContent = content;
81
+ let searchFileContent = fileContent;
82
+
83
+ if (ignoreWhitespace) {
84
+ searchContent = content.replace(/\s+/g, ' ').trim();
85
+ searchFileContent = fileContent.replace(/\s+/g, ' ').trim();
86
+ }
87
+
88
+ if (exact) {
89
+ return searchFileContent === searchContent;
90
+ }
91
+
92
+ return searchFileContent.includes(searchContent);
93
+
94
+ } catch (error) {
95
+ if (error.code === 'ENOENT') {
96
+ return false; // File doesn't exist, content can't exist
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Private Methods
104
+ */
105
+
106
+ async _evaluateSingleCondition(condition, target, content, variables) {
107
+ // Built-in conditions
108
+ if (condition === 'file_exists') {
109
+ const exists = await this._fileExists(target.resolvedPath);
110
+ return {
111
+ skip: exists,
112
+ reason: exists ? 'Target file already exists' : null
113
+ };
114
+ }
115
+
116
+ if (condition.startsWith('file_size >')) {
117
+ const sizeLimit = parseInt(condition.split('>')[1].trim());
118
+ const actualSize = await this._getFileSize(target.resolvedPath);
119
+ const skip = actualSize > sizeLimit;
120
+ return {
121
+ skip,
122
+ reason: skip ? `File size exceeds limit (${actualSize} > ${sizeLimit} bytes)` : null
123
+ };
124
+ }
125
+
126
+ if (condition.startsWith('line_count >')) {
127
+ const lineLimit = parseInt(condition.split('>')[1].trim());
128
+ const actualLines = await this._getLineCount(target.resolvedPath);
129
+ const skip = actualLines > lineLimit;
130
+ return {
131
+ skip,
132
+ reason: skip ? `File has too many lines (${actualLines} > ${lineLimit})` : null
133
+ };
134
+ }
135
+
136
+ // Pattern matching conditions
137
+ if (condition.startsWith('/') && condition.endsWith('/')) {
138
+ // Regex pattern
139
+ const pattern = condition.slice(1, -1);
140
+ const exists = await this.contentExists(target, pattern, { regex: true });
141
+ return {
142
+ skip: exists,
143
+ reason: exists ? `Pattern already exists: ${condition}` : null
144
+ };
145
+ }
146
+
147
+ // Variable interpolation
148
+ const interpolatedCondition = this._interpolateVariables(condition, variables);
149
+
150
+ // Check if interpolated content exists in file
151
+ const exists = await this.contentExists(target, interpolatedCondition);
152
+ return {
153
+ skip: exists,
154
+ reason: exists ? `Content already exists: ${interpolatedCondition}` : null
155
+ };
156
+ }
157
+
158
+ async _evaluateMultipleConditions(conditions, target, content, variables) {
159
+ const logic = target.skipIfLogic || SKIP_IF_LOGIC.OR;
160
+ const results = [];
161
+
162
+ for (const condition of conditions) {
163
+ const result = await this._evaluateSingleCondition(condition, target, content, variables);
164
+ results.push(result);
165
+ }
166
+
167
+ if (logic === SKIP_IF_LOGIC.AND) {
168
+ const allTrue = results.every(r => r.skip);
169
+ return {
170
+ skip: allTrue,
171
+ reason: allTrue ? `All conditions matched (AND logic): ${results.map(r => r.reason).join(', ')}` : null
172
+ };
173
+ } else {
174
+ // OR logic (default)
175
+ const anyTrue = results.some(r => r.skip);
176
+ const matchingReasons = results.filter(r => r.skip).map(r => r.reason);
177
+ return {
178
+ skip: anyTrue,
179
+ reason: anyTrue ? `Condition matched (OR logic): ${matchingReasons[0]}` : null
180
+ };
181
+ }
182
+ }
183
+
184
+ async _evaluateObjectCondition(conditionObj, target, content, variables) {
185
+ // Complex object-based conditions
186
+ const { pattern, exists, custom, hash } = conditionObj;
187
+
188
+ if (pattern) {
189
+ return await this._evaluateSingleCondition(pattern, target, content, variables);
190
+ }
191
+
192
+ if (exists !== undefined) {
193
+ const fileExists = await this._fileExists(target.resolvedPath);
194
+ return {
195
+ skip: fileExists === exists,
196
+ reason: fileExists === exists ? `File existence matches condition: ${exists}` : null
197
+ };
198
+ }
199
+
200
+ if (hash) {
201
+ const contentHash = await this.generateContentHash(content, target);
202
+ const existingHash = await this._getExistingContentHash(target);
203
+ const skip = contentHash === existingHash;
204
+ return {
205
+ skip,
206
+ reason: skip ? 'Content hash already exists' : null
207
+ };
208
+ }
209
+
210
+ if (custom && typeof custom === 'function') {
211
+ const result = await custom(target, content, variables);
212
+ return {
213
+ skip: result.skip,
214
+ reason: result.reason || 'Custom condition matched'
215
+ };
216
+ }
217
+
218
+ return { skip: false, reason: 'Invalid object condition' };
219
+ }
220
+
221
+ _interpolateVariables(template, variables) {
222
+ return template.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
223
+ return variables[variable] || match;
224
+ });
225
+ }
226
+
227
+ async _getFileContent(filePath) {
228
+ const cacheKey = filePath;
229
+
230
+ if (this.contentCache.has(cacheKey)) {
231
+ return this.contentCache.get(cacheKey);
232
+ }
233
+
234
+ try {
235
+ const content = await fs.readFile(filePath, 'utf8');
236
+ this.contentCache.set(cacheKey, content);
237
+ return content;
238
+ } catch (error) {
239
+ if (error.code === 'ENOENT') {
240
+ this.contentCache.set(cacheKey, '');
241
+ return '';
242
+ }
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ async _fileExists(filePath) {
248
+ try {
249
+ await fs.access(filePath);
250
+ return true;
251
+ } catch (error) {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ async _getFileSize(filePath) {
257
+ try {
258
+ const stats = await fs.stat(filePath);
259
+ return stats.size;
260
+ } catch (error) {
261
+ if (error.code === 'ENOENT') {
262
+ return 0;
263
+ }
264
+ throw error;
265
+ }
266
+ }
267
+
268
+ async _getLineCount(filePath) {
269
+ try {
270
+ const content = await this._getFileContent(filePath);
271
+ return content.split('\n').length;
272
+ } catch (error) {
273
+ return 0;
274
+ }
275
+ }
276
+
277
+ async _getExistingContentHash(target) {
278
+ try {
279
+ const content = await this._getFileContent(target.resolvedPath);
280
+ const hash = createHash(CHECKSUM_ALGORITHMS.SHA256);
281
+ hash.update(content);
282
+ return hash.digest('hex');
283
+ } catch (error) {
284
+ return null;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Clear caches (useful for testing or long-running processes)
290
+ */
291
+ clearCache() {
292
+ this.contentCache.clear();
293
+ this.hashCache.clear();
294
+ }
295
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * KGEN Injection Operations System
3
+ *
4
+ * Provides atomic, idempotent, and deterministic code injection capabilities
5
+ * for modifying existing files without complete overwriting.
6
+ *
7
+ * Key Features:
8
+ * - Atomic operations with rollback capability
9
+ * - Idempotent with skipIf conditions
10
+ * - Deterministic behavior across runs
11
+ * - Marker-based precise targeting
12
+ * - Multiple injection modes (append, prepend, before, after, replace)
13
+ * - Comprehensive validation and error handling
14
+ */
15
+
16
+ export { InjectionEngine } from './injection-engine.js';
17
+ export { InjectionModes } from './modes/index.js';
18
+ export { TargetResolver } from './target-resolver.js';
19
+ export { AtomicWriter } from './atomic-writer.js';
20
+ export { IdempotencyManager } from './idempotency-manager.js';
21
+ export { ValidationEngine } from './validation-engine.js';
22
+ export { RollbackManager } from './rollback-manager.js';
23
+
24
+ // Main injection API
25
+ export { inject } from './api.js';
26
+
27
+ // Types and constants
28
+ export { INJECTION_MODES, VALIDATION_RULES, ERROR_CODES } from './constants.js';
@@ -0,0 +1,378 @@
1
+ /**
2
+ * KGEN Injection Engine
3
+ *
4
+ * Main orchestrator for all injection operations. Provides atomic,
5
+ * idempotent, and deterministic file modification capabilities.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import { join, dirname, resolve, relative } from 'path';
10
+ import { createHash } from 'crypto';
11
+
12
+ import { TargetResolver } from './target-resolver.js';
13
+ import { AtomicWriter } from './atomic-writer.js';
14
+ import { IdempotencyManager } from './idempotency-manager.js';
15
+ import { ValidationEngine } from './validation-engine.js';
16
+ import { RollbackManager } from './rollback-manager.js';
17
+ import { InjectionModes } from './modes/index.js';
18
+
19
+ import {
20
+ INJECTION_MODES,
21
+ ERROR_CODES,
22
+ DEFAULT_CONFIG,
23
+ OPERATION_METADATA
24
+ } from './constants.js';
25
+
26
+ export class InjectionEngine {
27
+ constructor(config = {}) {
28
+ this.config = { ...DEFAULT_CONFIG, ...config };
29
+
30
+ // Initialize components
31
+ this.targetResolver = new TargetResolver(this.config);
32
+ this.atomicWriter = new AtomicWriter(this.config);
33
+ this.idempotencyManager = new IdempotencyManager(this.config);
34
+ this.validationEngine = new ValidationEngine(this.config);
35
+ this.rollbackManager = new RollbackManager(this.config);
36
+ this.injectionModes = new InjectionModes(this.config);
37
+
38
+ // Operation state
39
+ this.activeOperations = new Map();
40
+ this.operationHistory = [];
41
+ }
42
+
43
+ /**
44
+ * Main injection method - atomic, idempotent, deterministic
45
+ */
46
+ async inject(templateConfig, content, variables = {}) {
47
+ const operationId = this._generateOperationId(templateConfig, content);
48
+
49
+ try {
50
+ // Start atomic operation
51
+ await this._beginOperation(operationId, templateConfig);
52
+
53
+ // Resolve targets deterministically
54
+ const targets = await this.targetResolver.resolveTargets(templateConfig, variables);
55
+
56
+ // Validate all targets before any modifications
57
+ await this._validateAllTargets(targets);
58
+
59
+ // Check idempotency for all targets
60
+ const filteredTargets = await this._filterIdempotentTargets(targets, content, variables);
61
+
62
+ if (filteredTargets.length === 0) {
63
+ return {
64
+ success: true,
65
+ skipped: true,
66
+ message: 'All injections skipped - idempotent conditions met',
67
+ targets: targets.length
68
+ };
69
+ }
70
+
71
+ // Execute atomic multi-target injection
72
+ const results = await this._executeAtomicInjection(filteredTargets, content, variables, operationId);
73
+
74
+ // Commit operation
75
+ await this._commitOperation(operationId, results);
76
+
77
+ return {
78
+ success: true,
79
+ operationId,
80
+ results,
81
+ targets: filteredTargets.length,
82
+ skipped: targets.length - filteredTargets.length
83
+ };
84
+
85
+ } catch (error) {
86
+ // Atomic rollback on any failure
87
+ await this._rollbackOperation(operationId, error);
88
+ throw this._wrapError(error, operationId);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Dry run - shows what would be done without making changes
94
+ */
95
+ async dryRun(templateConfig, content, variables = {}) {
96
+ const targets = await this.targetResolver.resolveTargets(templateConfig, variables);
97
+ const validationResults = await this._validateAllTargets(targets, false); // Non-failing validation
98
+ const idempotencyResults = await this._checkIdempotency(targets, content, variables);
99
+
100
+ return {
101
+ targets: targets.map((target, index) => ({
102
+ path: target.resolvedPath,
103
+ mode: target.mode,
104
+ valid: validationResults[index].valid,
105
+ wouldSkip: idempotencyResults[index].skip,
106
+ reason: idempotencyResults[index].reason,
107
+ validation: validationResults[index]
108
+ }))
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Undo previous injection
114
+ */
115
+ async undo(operationId) {
116
+ return await this.rollbackManager.undoOperation(operationId);
117
+ }
118
+
119
+ /**
120
+ * Get operation history
121
+ */
122
+ getOperationHistory() {
123
+ return [...this.operationHistory];
124
+ }
125
+
126
+ /**
127
+ * Private Methods
128
+ */
129
+
130
+ _generateOperationId(templateConfig, content) {
131
+ const hash = createHash('sha256');
132
+ hash.update(JSON.stringify(templateConfig, null, 0));
133
+ hash.update(content);
134
+ hash.update(Date.now().toString());
135
+ return `injection-${hash.digest('hex').substring(0, 16)}`;
136
+ }
137
+
138
+ async _beginOperation(operationId, templateConfig) {
139
+ this.activeOperations.set(operationId, {
140
+ id: operationId,
141
+ config: templateConfig,
142
+ startTime: Date.now(),
143
+ phase: 'initializing',
144
+ targets: [],
145
+ backups: []
146
+ });
147
+ }
148
+
149
+ async _validateAllTargets(targets, throwOnError = true) {
150
+ const results = [];
151
+
152
+ for (const target of targets) {
153
+ try {
154
+ const result = await this.validationEngine.validateTarget(target);
155
+ results.push(result);
156
+
157
+ if (throwOnError && !result.valid) {
158
+ throw new Error(`Target validation failed: ${result.errors.join(', ')}`);
159
+ }
160
+ } catch (error) {
161
+ if (throwOnError) throw error;
162
+ results.push({ valid: false, errors: [error.message] });
163
+ }
164
+ }
165
+
166
+ return results;
167
+ }
168
+
169
+ async _filterIdempotentTargets(targets, content, variables) {
170
+ const filtered = [];
171
+
172
+ for (const target of targets) {
173
+ const shouldSkip = await this.idempotencyManager.shouldSkipInjection(
174
+ target, content, variables
175
+ );
176
+
177
+ if (!shouldSkip.skip) {
178
+ filtered.push(target);
179
+ } else {
180
+ console.log(`Skipping ${target.resolvedPath}: ${shouldSkip.reason}`);
181
+ }
182
+ }
183
+
184
+ return filtered;
185
+ }
186
+
187
+ async _checkIdempotency(targets, content, variables) {
188
+ const results = [];
189
+
190
+ for (const target of targets) {
191
+ const result = await this.idempotencyManager.shouldSkipInjection(
192
+ target, content, variables
193
+ );
194
+ results.push(result);
195
+ }
196
+
197
+ return results;
198
+ }
199
+
200
+ async _executeAtomicInjection(targets, content, variables, operationId) {
201
+ const operation = this.activeOperations.get(operationId);
202
+ operation.phase = 'executing';
203
+ operation.targets = targets;
204
+
205
+ // If single target, use simple atomic write
206
+ if (targets.length === 1) {
207
+ return [await this._injectSingleTarget(targets[0], content, variables, operationId)];
208
+ }
209
+
210
+ // Multiple targets require transaction
211
+ return await this._executeTransaction(targets, content, variables, operationId);
212
+ }
213
+
214
+ async _injectSingleTarget(target, content, variables, operationId) {
215
+ // Read current file content
216
+ const currentContent = await this._readTargetFile(target);
217
+
218
+ // Apply injection mode
219
+ const modifiedContent = await this.injectionModes.applyMode(
220
+ target.mode,
221
+ currentContent,
222
+ content,
223
+ target,
224
+ variables
225
+ );
226
+
227
+ // Validate resulting content
228
+ await this.validationEngine.validateContent(modifiedContent, target);
229
+
230
+ // Write atomically
231
+ const result = await this.atomicWriter.writeAtomic(
232
+ target.resolvedPath,
233
+ modifiedContent,
234
+ {
235
+ backup: this.config.backupEnabled,
236
+ operationId,
237
+ preserveMetadata: this.config.preservePermissions
238
+ }
239
+ );
240
+
241
+ return {
242
+ target: target.resolvedPath,
243
+ mode: target.mode,
244
+ success: true,
245
+ backup: result.backupPath,
246
+ checksum: result.checksum
247
+ };
248
+ }
249
+
250
+ async _executeTransaction(targets, content, variables, operationId) {
251
+ const transaction = await this.atomicWriter.beginTransaction(operationId);
252
+
253
+ try {
254
+ const results = [];
255
+
256
+ // Prepare all operations
257
+ for (const target of targets) {
258
+ const currentContent = await this._readTargetFile(target);
259
+ const modifiedContent = await this.injectionModes.applyMode(
260
+ target.mode,
261
+ currentContent,
262
+ content,
263
+ target,
264
+ variables
265
+ );
266
+
267
+ await this.validationEngine.validateContent(modifiedContent, target);
268
+
269
+ const result = await transaction.prepareWrite(
270
+ target.resolvedPath,
271
+ modifiedContent
272
+ );
273
+
274
+ results.push({
275
+ target: target.resolvedPath,
276
+ mode: target.mode,
277
+ prepared: true,
278
+ checksum: result.checksum
279
+ });
280
+ }
281
+
282
+ // Commit all changes atomically
283
+ await transaction.commit();
284
+
285
+ return results.map(r => ({ ...r, success: true, prepared: false }));
286
+
287
+ } catch (error) {
288
+ await transaction.rollback();
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ async _readTargetFile(target) {
294
+ if (target.mode === INJECTION_MODES.CREATE && target.createIfMissing) {
295
+ try {
296
+ return await fs.readFile(target.resolvedPath, 'utf8');
297
+ } catch (error) {
298
+ if (error.code === 'ENOENT') {
299
+ // Create directory structure if needed
300
+ if (target.createDirectories) {
301
+ await fs.mkdir(dirname(target.resolvedPath), { recursive: true });
302
+ }
303
+ return '';
304
+ }
305
+ throw error;
306
+ }
307
+ }
308
+
309
+ return await fs.readFile(target.resolvedPath, 'utf8');
310
+ }
311
+
312
+ async _commitOperation(operationId, results) {
313
+ const operation = this.activeOperations.get(operationId);
314
+ operation.phase = 'committed';
315
+ operation.endTime = Date.now();
316
+ operation.results = results;
317
+
318
+ // Record in history
319
+ this.operationHistory.push({
320
+ ...operation,
321
+ metadata: {
322
+ ...OPERATION_METADATA,
323
+ timestamp: new Date().toISOString(),
324
+ duration: operation.endTime - operation.startTime
325
+ }
326
+ });
327
+
328
+ // Clean up active operation
329
+ this.activeOperations.delete(operationId);
330
+ }
331
+
332
+ async _rollbackOperation(operationId, error) {
333
+ const operation = this.activeOperations.get(operationId);
334
+
335
+ if (operation) {
336
+ operation.phase = 'rolling-back';
337
+
338
+ try {
339
+ await this.rollbackManager.rollbackOperation(operationId, operation);
340
+ operation.phase = 'rolled-back';
341
+ } catch (rollbackError) {
342
+ operation.phase = 'rollback-failed';
343
+ operation.rollbackError = rollbackError.message;
344
+ console.error('Rollback failed:', rollbackError);
345
+ }
346
+
347
+ operation.error = error.message;
348
+ operation.endTime = Date.now();
349
+
350
+ // Record failed operation in history
351
+ this.operationHistory.push({ ...operation });
352
+ this.activeOperations.delete(operationId);
353
+ }
354
+ }
355
+
356
+ _wrapError(error, operationId) {
357
+ return new InjectionError(
358
+ error.message,
359
+ error.code || ERROR_CODES.ATOMIC_FAILURE,
360
+ operationId,
361
+ error
362
+ );
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Custom error class for injection operations
368
+ */
369
+ export class InjectionError extends Error {
370
+ constructor(message, code, operationId, originalError = null) {
371
+ super(message);
372
+ this.name = 'InjectionError';
373
+ this.code = code;
374
+ this.operationId = operationId;
375
+ this.originalError = originalError;
376
+ this.timestamp = new Date().toISOString();
377
+ }
378
+ }