@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Injection API
|
|
3
|
+
*
|
|
4
|
+
* Main API for injection operations. Provides a clean interface
|
|
5
|
+
* for the template engine to perform atomic, idempotent injections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { InjectionEngine } from './injection-engine.js';
|
|
9
|
+
import { DEFAULT_CONFIG } from './constants.js';
|
|
10
|
+
|
|
11
|
+
// Global injection engine instance
|
|
12
|
+
let globalInjectionEngine = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize injection system with configuration
|
|
16
|
+
*/
|
|
17
|
+
export function initializeInjection(config = {}) {
|
|
18
|
+
globalInjectionEngine = new InjectionEngine({ ...DEFAULT_CONFIG, ...config });
|
|
19
|
+
return globalInjectionEngine;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Main injection function - atomic, idempotent, deterministic
|
|
24
|
+
*/
|
|
25
|
+
export async function inject(templateConfig, content, variables = {}, options = {}) {
|
|
26
|
+
const engine = getInjectionEngine(options.config);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return await engine.inject(templateConfig, content, variables);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Enhance error with context
|
|
32
|
+
error.templateConfig = templateConfig;
|
|
33
|
+
error.variables = variables;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Perform dry run to see what would be injected
|
|
40
|
+
*/
|
|
41
|
+
export async function dryRun(templateConfig, content, variables = {}, options = {}) {
|
|
42
|
+
const engine = getInjectionEngine(options.config);
|
|
43
|
+
return await engine.dryRun(templateConfig, content, variables);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Undo a previous injection operation
|
|
48
|
+
*/
|
|
49
|
+
export async function undo(operationId, options = {}) {
|
|
50
|
+
const engine = getInjectionEngine(options.config);
|
|
51
|
+
return await engine.undo(operationId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get injection operation history
|
|
56
|
+
*/
|
|
57
|
+
export function getOperationHistory(options = {}) {
|
|
58
|
+
const engine = getInjectionEngine(options.config);
|
|
59
|
+
return engine.getOperationHistory();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Batch injection operations
|
|
64
|
+
*/
|
|
65
|
+
export async function batchInject(operations, options = {}) {
|
|
66
|
+
const engine = getInjectionEngine(options.config);
|
|
67
|
+
const results = [];
|
|
68
|
+
const errors = [];
|
|
69
|
+
|
|
70
|
+
for (const operation of operations) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await engine.inject(
|
|
73
|
+
operation.templateConfig,
|
|
74
|
+
operation.content,
|
|
75
|
+
operation.variables || {}
|
|
76
|
+
);
|
|
77
|
+
results.push({ ...result, operation });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
errors.push({ error, operation });
|
|
80
|
+
|
|
81
|
+
// Stop on first error unless continueOnError is true
|
|
82
|
+
if (!options.continueOnError) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
results,
|
|
90
|
+
errors,
|
|
91
|
+
totalOperations: operations.length,
|
|
92
|
+
successfulOperations: results.length,
|
|
93
|
+
failedOperations: errors.length
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate injection configuration without executing
|
|
99
|
+
*/
|
|
100
|
+
export async function validateInjectionConfig(templateConfig, variables = {}, options = {}) {
|
|
101
|
+
const engine = getInjectionEngine(options.config);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// This would use internal validation methods
|
|
105
|
+
const targets = await engine.targetResolver.resolveTargets(templateConfig, variables);
|
|
106
|
+
const validationResults = await Promise.all(
|
|
107
|
+
targets.map(target => engine.validationEngine.validateTarget(target))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
valid: validationResults.every(r => r.valid),
|
|
112
|
+
targets: targets.length,
|
|
113
|
+
validationResults,
|
|
114
|
+
errors: validationResults.flatMap(r => r.errors),
|
|
115
|
+
warnings: validationResults.flatMap(r => r.warnings)
|
|
116
|
+
};
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
error: error.message,
|
|
121
|
+
targets: 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get injection system status and metrics
|
|
128
|
+
*/
|
|
129
|
+
export function getInjectionStatus(options = {}) {
|
|
130
|
+
const engine = getInjectionEngine(options.config);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
initialized: !!engine,
|
|
134
|
+
config: engine.config,
|
|
135
|
+
activeOperations: engine.activeOperations.size,
|
|
136
|
+
operationHistory: engine.operationHistory.length,
|
|
137
|
+
cacheStats: {
|
|
138
|
+
idempotency: engine.idempotencyManager.contentCache.size,
|
|
139
|
+
validation: engine.validationEngine.validationCache.size
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all caches (useful for testing or memory management)
|
|
146
|
+
*/
|
|
147
|
+
export function clearCaches(options = {}) {
|
|
148
|
+
const engine = getInjectionEngine(options.config);
|
|
149
|
+
|
|
150
|
+
engine.idempotencyManager.clearCache();
|
|
151
|
+
engine.validationEngine.clearCache();
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
cleared: true,
|
|
155
|
+
timestamp: Date.now()
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Helper function to get or create injection engine
|
|
161
|
+
*/
|
|
162
|
+
function getInjectionEngine(config) {
|
|
163
|
+
if (config) {
|
|
164
|
+
// Create temporary engine with custom config
|
|
165
|
+
return new InjectionEngine(config);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!globalInjectionEngine) {
|
|
169
|
+
// Create default engine
|
|
170
|
+
globalInjectionEngine = new InjectionEngine(DEFAULT_CONFIG);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return globalInjectionEngine;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Export engine class for advanced usage
|
|
178
|
+
*/
|
|
179
|
+
export { InjectionEngine };
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Template integration helper - processes template with injection support
|
|
183
|
+
*/
|
|
184
|
+
export async function processTemplate(template, data, options = {}) {
|
|
185
|
+
const { frontmatter, content } = parseTemplate(template);
|
|
186
|
+
|
|
187
|
+
// Check if this template has injection configuration
|
|
188
|
+
if (frontmatter.inject) {
|
|
189
|
+
return await inject(frontmatter, content, data, options);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Regular template processing would go here
|
|
193
|
+
throw new Error('Regular template processing not implemented - injection only');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Simple frontmatter parser for templates
|
|
198
|
+
*/
|
|
199
|
+
function parseTemplate(template) {
|
|
200
|
+
const frontmatterMatch = template.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
201
|
+
|
|
202
|
+
if (!frontmatterMatch) {
|
|
203
|
+
return {
|
|
204
|
+
frontmatter: {},
|
|
205
|
+
content: template
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Simple YAML-like parsing - in production use proper YAML parser
|
|
211
|
+
const frontmatterText = frontmatterMatch[1];
|
|
212
|
+
const frontmatter = parseFrontmatter(frontmatterText);
|
|
213
|
+
const content = frontmatterMatch[2];
|
|
214
|
+
|
|
215
|
+
return { frontmatter, content };
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw new Error(`Failed to parse template frontmatter: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Simple frontmatter parser (YAML-like)
|
|
223
|
+
*/
|
|
224
|
+
function parseFrontmatter(text) {
|
|
225
|
+
const result = {};
|
|
226
|
+
const lines = text.split('\n');
|
|
227
|
+
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
231
|
+
|
|
232
|
+
const colonIndex = trimmed.indexOf(':');
|
|
233
|
+
if (colonIndex === -1) continue;
|
|
234
|
+
|
|
235
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
236
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
237
|
+
|
|
238
|
+
// Simple value parsing
|
|
239
|
+
if (value === 'true') {
|
|
240
|
+
result[key] = true;
|
|
241
|
+
} else if (value === 'false') {
|
|
242
|
+
result[key] = false;
|
|
243
|
+
} else if (/^\d+$/.test(value)) {
|
|
244
|
+
result[key] = parseInt(value);
|
|
245
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
246
|
+
result[key] = value.slice(1, -1);
|
|
247
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
248
|
+
result[key] = value.slice(1, -1);
|
|
249
|
+
} else {
|
|
250
|
+
result[key] = value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Export all injection modes for reference
|
|
259
|
+
*/
|
|
260
|
+
export { INJECTION_MODES, ERROR_CODES } from './constants.js';
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Atomic Writer
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic file write operations with backup, rollback,
|
|
5
|
+
* and transactional capabilities for deterministic file modifications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs, createReadStream } from 'fs';
|
|
9
|
+
import { join, dirname, basename, extname } from 'path';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { pipeline } from 'stream/promises';
|
|
12
|
+
|
|
13
|
+
import { ERROR_CODES, CHECKSUM_ALGORITHMS, LOCK_CONFIG } from './constants.js';
|
|
14
|
+
|
|
15
|
+
export class AtomicWriter {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.activeLocks = new Map();
|
|
19
|
+
this.activeTransactions = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write file atomically using temporary file + rename
|
|
24
|
+
*/
|
|
25
|
+
async writeAtomic(filePath, content, options = {}) {
|
|
26
|
+
const {
|
|
27
|
+
backup = this.config.backupEnabled,
|
|
28
|
+
operationId,
|
|
29
|
+
preserveMetadata = this.config.preservePermissions,
|
|
30
|
+
encoding = 'utf8'
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
// Acquire file lock
|
|
34
|
+
const lockId = await this._acquireLock(filePath);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
let backupPath = null;
|
|
38
|
+
let originalStats = null;
|
|
39
|
+
|
|
40
|
+
// Create backup if enabled and file exists
|
|
41
|
+
if (backup) {
|
|
42
|
+
try {
|
|
43
|
+
originalStats = await fs.stat(filePath);
|
|
44
|
+
backupPath = await this._createBackup(filePath, operationId);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== 'ENOENT') throw error;
|
|
47
|
+
// File doesn't exist, no backup needed
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create temporary file in same directory for atomic rename
|
|
52
|
+
const tempPath = await this._createTempFile(filePath);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Write content to temporary file
|
|
56
|
+
await fs.writeFile(tempPath, content, encoding);
|
|
57
|
+
|
|
58
|
+
// Preserve original file metadata
|
|
59
|
+
if (preserveMetadata && originalStats) {
|
|
60
|
+
await fs.chmod(tempPath, originalStats.mode);
|
|
61
|
+
await fs.utimes(tempPath, originalStats.atime, originalStats.mtime);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate checksum
|
|
65
|
+
const checksum = await this._calculateChecksum(tempPath);
|
|
66
|
+
|
|
67
|
+
// Atomic rename (this is the atomic operation)
|
|
68
|
+
await fs.rename(tempPath, filePath);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
filePath,
|
|
73
|
+
backupPath,
|
|
74
|
+
checksum,
|
|
75
|
+
tempPath: null // Cleaned up by rename
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Clean up temp file on failure
|
|
80
|
+
try {
|
|
81
|
+
await fs.unlink(tempPath);
|
|
82
|
+
} catch (cleanupError) {
|
|
83
|
+
console.warn('Failed to cleanup temp file:', tempPath, cleanupError.message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Restore from backup if available
|
|
87
|
+
if (backupPath) {
|
|
88
|
+
try {
|
|
89
|
+
await fs.copyFile(backupPath, filePath);
|
|
90
|
+
} catch (restoreError) {
|
|
91
|
+
console.error('Failed to restore from backup:', restoreError);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} finally {
|
|
99
|
+
await this._releaseLock(lockId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Begin transaction for multi-file atomic operations
|
|
105
|
+
*/
|
|
106
|
+
async beginTransaction(operationId) {
|
|
107
|
+
if (this.activeTransactions.has(operationId)) {
|
|
108
|
+
throw new Error(`Transaction ${operationId} already active`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const transaction = new AtomicTransaction(operationId, this.config);
|
|
112
|
+
this.activeTransactions.set(operationId, transaction);
|
|
113
|
+
|
|
114
|
+
return transaction;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Private Methods
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
async _acquireLock(filePath) {
|
|
122
|
+
const lockId = `${filePath}-${Date.now()}-${Math.random()}`;
|
|
123
|
+
const startTime = Date.now();
|
|
124
|
+
|
|
125
|
+
while (Date.now() - startTime < LOCK_CONFIG.TIMEOUT) {
|
|
126
|
+
// Simple file-based locking
|
|
127
|
+
const lockFile = `${filePath}.kgen-lock`;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await fs.writeFile(lockFile, lockId, { flag: 'wx' }); // Exclusive create
|
|
131
|
+
this.activeLocks.set(lockId, lockFile);
|
|
132
|
+
return lockId;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.code === 'EEXIST') {
|
|
135
|
+
// Lock exists, check if stale
|
|
136
|
+
try {
|
|
137
|
+
const lockStat = await fs.stat(lockFile);
|
|
138
|
+
if (Date.now() - lockStat.mtime > LOCK_CONFIG.TIMEOUT) {
|
|
139
|
+
// Stale lock, remove it
|
|
140
|
+
await fs.unlink(lockFile);
|
|
141
|
+
}
|
|
142
|
+
} catch (statError) {
|
|
143
|
+
// Lock file might have been removed, continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wait and retry
|
|
147
|
+
await new Promise(resolve => setTimeout(resolve, LOCK_CONFIG.RETRY_DELAY));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(`Failed to acquire lock for ${filePath} within timeout`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async _releaseLock(lockId) {
|
|
158
|
+
const lockFile = this.activeLocks.get(lockId);
|
|
159
|
+
if (lockFile) {
|
|
160
|
+
try {
|
|
161
|
+
await fs.unlink(lockFile);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn('Failed to release lock:', lockFile, error.message);
|
|
164
|
+
}
|
|
165
|
+
this.activeLocks.delete(lockId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async _createBackup(filePath, operationId) {
|
|
170
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
171
|
+
const ext = extname(filePath);
|
|
172
|
+
const base = basename(filePath, ext);
|
|
173
|
+
const dir = dirname(filePath);
|
|
174
|
+
|
|
175
|
+
const backupName = operationId
|
|
176
|
+
? `${base}${this.config.backupSuffix}-${operationId}${ext}`
|
|
177
|
+
: `${base}${this.config.backupSuffix}-${timestamp}${ext}`;
|
|
178
|
+
|
|
179
|
+
const backupPath = join(dir, backupName);
|
|
180
|
+
|
|
181
|
+
await fs.copyFile(filePath, backupPath);
|
|
182
|
+
return backupPath;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async _createTempFile(filePath) {
|
|
186
|
+
const dir = dirname(filePath);
|
|
187
|
+
const base = basename(filePath);
|
|
188
|
+
const tempName = `.kgen-temp-${base}-${Date.now()}-${Math.random().toString(36)}`;
|
|
189
|
+
return join(dir, tempName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async _calculateChecksum(filePath, algorithm = CHECKSUM_ALGORITHMS.SHA256) {
|
|
193
|
+
const hash = createHash(algorithm);
|
|
194
|
+
const stream = createReadStream(filePath);
|
|
195
|
+
|
|
196
|
+
await pipeline(stream, hash);
|
|
197
|
+
return hash.digest('hex');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Transaction class for multi-file atomic operations
|
|
203
|
+
*/
|
|
204
|
+
class AtomicTransaction {
|
|
205
|
+
constructor(operationId, config) {
|
|
206
|
+
this.operationId = operationId;
|
|
207
|
+
this.config = config;
|
|
208
|
+
this.preparedWrites = [];
|
|
209
|
+
this.backups = [];
|
|
210
|
+
this.locks = [];
|
|
211
|
+
this.committed = false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async prepareWrite(filePath, content, options = {}) {
|
|
215
|
+
if (this.committed) {
|
|
216
|
+
throw new Error('Transaction already committed');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Acquire lock
|
|
220
|
+
const atomicWriter = new AtomicWriter(this.config);
|
|
221
|
+
const lockId = await atomicWriter._acquireLock(filePath);
|
|
222
|
+
this.locks.push({ lockId, filePath, writer: atomicWriter });
|
|
223
|
+
|
|
224
|
+
// Create backup
|
|
225
|
+
let backupPath = null;
|
|
226
|
+
try {
|
|
227
|
+
backupPath = await atomicWriter._createBackup(filePath, this.operationId);
|
|
228
|
+
this.backups.push({ filePath, backupPath });
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error.code !== 'ENOENT') throw error;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Create temp file with content
|
|
234
|
+
const tempPath = await atomicWriter._createTempFile(filePath);
|
|
235
|
+
await fs.writeFile(tempPath, content, options.encoding || 'utf8');
|
|
236
|
+
|
|
237
|
+
// Calculate checksum
|
|
238
|
+
const checksum = await atomicWriter._calculateChecksum(tempPath);
|
|
239
|
+
|
|
240
|
+
const preparedWrite = {
|
|
241
|
+
filePath,
|
|
242
|
+
tempPath,
|
|
243
|
+
backupPath,
|
|
244
|
+
checksum,
|
|
245
|
+
options
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
this.preparedWrites.push(preparedWrite);
|
|
249
|
+
|
|
250
|
+
return { checksum };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async commit() {
|
|
254
|
+
if (this.committed) {
|
|
255
|
+
throw new Error('Transaction already committed');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// Validate all temp files exist and are ready
|
|
260
|
+
for (const write of this.preparedWrites) {
|
|
261
|
+
await fs.access(write.tempPath);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Atomically rename all temp files to their targets
|
|
265
|
+
for (const write of this.preparedWrites) {
|
|
266
|
+
await fs.rename(write.tempPath, write.filePath);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.committed = true;
|
|
270
|
+
|
|
271
|
+
// Clean up locks
|
|
272
|
+
await this._releaseLocks();
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
filesWritten: this.preparedWrites.length,
|
|
277
|
+
backups: this.backups.map(b => b.backupPath)
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
} catch (error) {
|
|
281
|
+
await this.rollback();
|
|
282
|
+
throw new Error(`Transaction commit failed: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async rollback() {
|
|
287
|
+
try {
|
|
288
|
+
// Clean up temp files
|
|
289
|
+
for (const write of this.preparedWrites) {
|
|
290
|
+
try {
|
|
291
|
+
await fs.unlink(write.tempPath);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.warn('Failed to cleanup temp file:', write.tempPath);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Restore from backups if any files were partially written
|
|
298
|
+
for (const backup of this.backups) {
|
|
299
|
+
try {
|
|
300
|
+
const targetExists = await fs.access(backup.filePath).then(() => true, () => false);
|
|
301
|
+
if (targetExists) {
|
|
302
|
+
await fs.copyFile(backup.backupPath, backup.filePath);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('Failed to restore backup:', backup, error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await this._releaseLocks();
|
|
310
|
+
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('Rollback failed:', error);
|
|
313
|
+
throw new Error(`Transaction rollback failed: ${error.message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async _releaseLocks() {
|
|
318
|
+
for (const { lockId, writer } of this.locks) {
|
|
319
|
+
try {
|
|
320
|
+
await writer._releaseLock(lockId);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn('Failed to release lock:', lockId, error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
this.locks = [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for KGEN Injection System
|
|
3
|
+
*
|
|
4
|
+
* Defines all constants, error codes, and configuration values
|
|
5
|
+
* used throughout the injection operations system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const INJECTION_MODES = {
|
|
9
|
+
APPEND: 'append',
|
|
10
|
+
PREPEND: 'prepend',
|
|
11
|
+
BEFORE: 'before',
|
|
12
|
+
AFTER: 'after',
|
|
13
|
+
REPLACE: 'replace',
|
|
14
|
+
LINE_AT: 'lineAt',
|
|
15
|
+
CREATE: 'create'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const SKIP_IF_LOGIC = {
|
|
19
|
+
AND: 'AND',
|
|
20
|
+
OR: 'OR'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const VALIDATION_RULES = {
|
|
24
|
+
SYNTAX: 'syntax',
|
|
25
|
+
SEMANTICS: 'semantics',
|
|
26
|
+
CONFLICTS: 'conflicts',
|
|
27
|
+
ENCODING: 'encoding',
|
|
28
|
+
SIZE: 'size',
|
|
29
|
+
PERMISSIONS: 'permissions'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const ERROR_CODES = {
|
|
33
|
+
TARGET_NOT_FOUND: 'TARGET_NOT_FOUND',
|
|
34
|
+
BINARY_FILE: 'BINARY_FILE',
|
|
35
|
+
READ_ONLY: 'READ_ONLY',
|
|
36
|
+
FILE_LOCKED: 'FILE_LOCKED',
|
|
37
|
+
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
|
|
38
|
+
SIZE_EXCEEDED: 'SIZE_EXCEEDED',
|
|
39
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
40
|
+
ATOMIC_FAILURE: 'ATOMIC_FAILURE',
|
|
41
|
+
ROLLBACK_FAILURE: 'ROLLBACK_FAILURE',
|
|
42
|
+
INSUFFICIENT_SPACE: 'INSUFFICIENT_SPACE',
|
|
43
|
+
ENCODING_ERROR: 'ENCODING_ERROR',
|
|
44
|
+
CONFLICT_DETECTED: 'CONFLICT_DETECTED'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_CONFIG = {
|
|
48
|
+
// Atomic operations
|
|
49
|
+
atomicWrites: true,
|
|
50
|
+
backupEnabled: true,
|
|
51
|
+
transactionTimeout: 30000, // 30 seconds
|
|
52
|
+
|
|
53
|
+
// File validation
|
|
54
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
55
|
+
maxLineCount: 10000,
|
|
56
|
+
validateEncoding: true,
|
|
57
|
+
preservePermissions: true,
|
|
58
|
+
|
|
59
|
+
// Deterministic behavior
|
|
60
|
+
sortGlobResults: true,
|
|
61
|
+
consistentLineEndings: true,
|
|
62
|
+
preserveWhitespace: true,
|
|
63
|
+
|
|
64
|
+
// Security
|
|
65
|
+
preventPathTraversal: true,
|
|
66
|
+
allowedExtensions: ['.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.txt', '.html', '.css', '.scss'],
|
|
67
|
+
|
|
68
|
+
// Performance
|
|
69
|
+
streamLargeFiles: true,
|
|
70
|
+
streamThreshold: 1024 * 1024, // 1MB
|
|
71
|
+
maxConcurrentOperations: 5,
|
|
72
|
+
|
|
73
|
+
// Backup and recovery
|
|
74
|
+
backupSuffix: '.kgen-backup',
|
|
75
|
+
maxBackups: 10,
|
|
76
|
+
autoCleanup: true
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const REGEX_FLAGS = {
|
|
80
|
+
GLOBAL: 'g',
|
|
81
|
+
MULTILINE: 'm',
|
|
82
|
+
CASE_INSENSITIVE: 'i',
|
|
83
|
+
DOTALL: 's',
|
|
84
|
+
UNICODE: 'u'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const LINE_ENDINGS = {
|
|
88
|
+
LF: '\n', // Unix/Linux/macOS
|
|
89
|
+
CRLF: '\r\n', // Windows
|
|
90
|
+
CR: '\r' // Classic Mac
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const ENCODINGS = {
|
|
94
|
+
UTF8: 'utf8',
|
|
95
|
+
UTF16: 'utf16le',
|
|
96
|
+
ASCII: 'ascii',
|
|
97
|
+
LATIN1: 'latin1'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const CHECKSUM_ALGORITHMS = {
|
|
101
|
+
SHA256: 'sha256',
|
|
102
|
+
MD5: 'md5',
|
|
103
|
+
SHA1: 'sha1'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// File type detection patterns
|
|
107
|
+
export const BINARY_PATTERNS = [
|
|
108
|
+
/^\x00/, // Null bytes at start
|
|
109
|
+
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/, // Control characters
|
|
110
|
+
/\uFFFD/, // Replacement character (indicates binary)
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// Content validation patterns
|
|
114
|
+
export const CONTENT_PATTERNS = {
|
|
115
|
+
IMPORT_STATEMENT: /^import\s+.*from\s+['"][^'"]+['"];?\s*$/gm,
|
|
116
|
+
EXPORT_STATEMENT: /^export\s+.*$/gm,
|
|
117
|
+
FUNCTION_DECLARATION: /^(async\s+)?function\s+\w+\s*\(/gm,
|
|
118
|
+
CLASS_DECLARATION: /^class\s+\w+/gm,
|
|
119
|
+
INTERFACE_DECLARATION: /^interface\s+\w+/gm,
|
|
120
|
+
TYPE_DECLARATION: /^type\s+\w+/gm
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Operation metadata
|
|
124
|
+
export const OPERATION_METADATA = {
|
|
125
|
+
CREATED_BY: 'KGEN Injection System',
|
|
126
|
+
VERSION: '1.0.0',
|
|
127
|
+
TIMESTAMP_FORMAT: 'YYYY-MM-DD HH:mm:ss UTC',
|
|
128
|
+
HASH_ALGORITHM: 'sha256'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Concurrency control
|
|
132
|
+
export const LOCK_CONFIG = {
|
|
133
|
+
TIMEOUT: 10000, // 10 seconds
|
|
134
|
+
RETRY_DELAY: 100, // 100ms
|
|
135
|
+
MAX_RETRIES: 100
|
|
136
|
+
};
|