@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.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/package.json +90 -0
- package/src/MIGRATION_COMPLETE.md +186 -0
- package/src/PORT-MAP.md +302 -0
- package/src/base/filter-templates.js +479 -0
- package/src/base/index.js +92 -0
- package/src/base/injection-targets.js +583 -0
- package/src/base/macro-templates.js +298 -0
- package/src/base/macro-templates.js.bak +461 -0
- package/src/base/shacl-templates.js +617 -0
- package/src/base/template-base.js +388 -0
- package/src/core/attestor.js +381 -0
- package/src/core/filters.js +518 -0
- package/src/core/index.js +21 -0
- package/src/core/kgen-engine.js +372 -0
- package/src/core/parser.js +447 -0
- package/src/core/post-processor.js +313 -0
- package/src/core/renderer.js +469 -0
- package/src/doc-generator/cli.mjs +122 -0
- package/src/doc-generator/index.mjs +28 -0
- package/src/doc-generator/mdx-generator.mjs +71 -0
- package/src/doc-generator/nav-generator.mjs +136 -0
- package/src/doc-generator/parser.mjs +291 -0
- package/src/doc-generator/rdf-builder.mjs +306 -0
- package/src/doc-generator/scanner.mjs +189 -0
- package/src/engine/index.js +42 -0
- package/src/engine/pipeline.js +448 -0
- package/src/engine/renderer.js +604 -0
- package/src/engine/template-engine.js +566 -0
- package/src/filters/array.js +436 -0
- package/src/filters/data.js +479 -0
- package/src/filters/index.js +270 -0
- package/src/filters/rdf.js +264 -0
- package/src/filters/text.js +369 -0
- package/src/index.js +109 -0
- package/src/inheritance/index.js +40 -0
- package/src/injection/api.js +260 -0
- package/src/injection/atomic-writer.js +327 -0
- package/src/injection/constants.js +136 -0
- package/src/injection/idempotency-manager.js +295 -0
- package/src/injection/index.js +28 -0
- package/src/injection/injection-engine.js +378 -0
- package/src/injection/integration.js +339 -0
- package/src/injection/modes/index.js +341 -0
- package/src/injection/rollback-manager.js +373 -0
- package/src/injection/target-resolver.js +323 -0
- package/src/injection/tests/atomic-writer.test.js +382 -0
- package/src/injection/tests/injection-engine.test.js +611 -0
- package/src/injection/tests/integration.test.js +392 -0
- package/src/injection/tests/run-tests.js +283 -0
- package/src/injection/validation-engine.js +547 -0
- package/src/linter/determinism-linter.js +473 -0
- package/src/linter/determinism.js +410 -0
- package/src/linter/index.js +6 -0
- package/src/linter/test-doubles.js +475 -0
- package/src/parser/frontmatter.js +228 -0
- package/src/parser/variables.js +344 -0
- package/src/renderer/deterministic.js +245 -0
- package/src/renderer/index.js +6 -0
- package/src/templates/latex/academic-paper.njk +186 -0
- package/src/templates/latex/index.js +104 -0
- package/src/templates/nextjs/app-page.njk +66 -0
- package/src/templates/nextjs/index.js +80 -0
- package/src/templates/office/docx/document.njk +368 -0
- package/src/templates/office/index.js +79 -0
- package/src/templates/office/word-report.njk +129 -0
- 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
|
+
}
|