@ruso-0/nreki 7.1.2 → 7.3.0
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/CHANGELOG.md +805 -774
- package/README.md +308 -442
- package/dist/ast-sandbox.d.ts.map +1 -1
- package/dist/ast-sandbox.js +17 -1
- package/dist/ast-sandbox.js.map +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +10 -4
- package/dist/audit.js.map +1 -1
- package/dist/chronos-memory.d.ts.map +1 -1
- package/dist/chronos-memory.js +10 -2
- package/dist/chronos-memory.js.map +1 -1
- package/dist/compressor.d.ts.map +1 -1
- package/dist/compressor.js +13 -1
- package/dist/compressor.js.map +1 -1
- package/dist/database.d.ts +12 -1
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +81 -29
- package/dist/database.js.map +1 -1
- package/dist/embedder.d.ts.map +1 -1
- package/dist/embedder.js +7 -2
- package/dist/embedder.js.map +1 -1
- package/dist/handlers/code.d.ts.map +1 -1
- package/dist/handlers/code.js +198 -243
- package/dist/handlers/code.js.map +1 -1
- package/dist/handlers/guard.d.ts.map +1 -1
- package/dist/handlers/guard.js +10 -1
- package/dist/handlers/guard.js.map +1 -1
- package/dist/hologram/shadow-generator.d.ts.map +1 -1
- package/dist/hologram/shadow-generator.js +20 -1
- package/dist/hologram/shadow-generator.js.map +1 -1
- package/dist/kernel/backends/lsp-sidecar-base.d.ts +49 -5
- package/dist/kernel/backends/lsp-sidecar-base.d.ts.map +1 -1
- package/dist/kernel/backends/lsp-sidecar-base.js +209 -69
- package/dist/kernel/backends/lsp-sidecar-base.js.map +1 -1
- package/dist/kernel/backends/ts-compiler-wrapper.d.ts +1 -1
- package/dist/kernel/backends/ts-compiler-wrapper.d.ts.map +1 -1
- package/dist/kernel/backends/ts-compiler-wrapper.js +7 -4
- package/dist/kernel/backends/ts-compiler-wrapper.js.map +1 -1
- package/dist/kernel/backends/ts-corsa-sidecar.d.ts +26 -0
- package/dist/kernel/backends/ts-corsa-sidecar.d.ts.map +1 -0
- package/dist/kernel/backends/ts-corsa-sidecar.js +30 -0
- package/dist/kernel/backends/ts-corsa-sidecar.js.map +1 -0
- package/dist/kernel/nreki-kernel.d.ts +37 -0
- package/dist/kernel/nreki-kernel.d.ts.map +1 -1
- package/dist/kernel/nreki-kernel.js +669 -290
- package/dist/kernel/nreki-kernel.js.map +1 -1
- package/dist/kernel/spectral-topology.d.ts.map +1 -1
- package/dist/kernel/spectral-topology.js +32 -16
- package/dist/kernel/spectral-topology.js.map +1 -1
- package/dist/middleware/circuit-breaker.d.ts.map +1 -1
- package/dist/middleware/circuit-breaker.js +18 -2
- package/dist/middleware/circuit-breaker.js.map +1 -1
- package/dist/middleware/file-lock.d.ts.map +1 -1
- package/dist/middleware/file-lock.js +8 -3
- package/dist/middleware/file-lock.js.map +1 -1
- package/dist/monitor.d.ts.map +1 -1
- package/dist/monitor.js +1 -0
- package/dist/monitor.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +19 -2
- package/dist/parser.js.map +1 -1
- package/dist/pin-memory.d.ts +2 -2
- package/dist/pin-memory.d.ts.map +1 -1
- package/dist/pin-memory.js.map +1 -1
- package/dist/repo-map.d.ts.map +1 -1
- package/dist/repo-map.js +26 -0
- package/dist/repo-map.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +58 -18
- package/dist/router.js.map +1 -1
- package/dist/undo.js +1 -1
- package/dist/undo.js.map +1 -1
- package/dist/utils/imports.d.ts.map +1 -1
- package/dist/utils/imports.js +8 -4
- package/dist/utils/imports.js.map +1 -1
- package/dist/utils/latency-tracker.d.ts +22 -0
- package/dist/utils/latency-tracker.d.ts.map +1 -0
- package/dist/utils/latency-tracker.js +49 -0
- package/dist/utils/latency-tracker.js.map +1 -0
- package/dist/utils/logger.d.ts +5 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +29 -6
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path-jail.d.ts.map +1 -1
- package/dist/utils/path-jail.js +3 -0
- package/dist/utils/path-jail.js.map +1 -1
- package/package.json +96 -79
|
@@ -5,7 +5,8 @@ import * as crypto from "crypto";
|
|
|
5
5
|
import { isSensitivePath } from "../utils/path-jail.js";
|
|
6
6
|
import { readSource } from "../utils/read-source.js";
|
|
7
7
|
import { toPosix as toPosixUtil } from "../utils/to-posix.js";
|
|
8
|
-
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { logger, setTxId, clearTxId } from "../utils/logger.js";
|
|
9
|
+
import { latencyTracker } from "../utils/latency-tracker.js";
|
|
9
10
|
import { TsCompilerWrapper } from "./backends/ts-compiler-wrapper.js";
|
|
10
11
|
// ─── Async FIFO Mutex (P10) ────────────────────────────────────────
|
|
11
12
|
export class AsyncMutex {
|
|
@@ -49,7 +50,7 @@ export class AsyncMutex {
|
|
|
49
50
|
async withLock(fn) {
|
|
50
51
|
const unlock = await this.lock();
|
|
51
52
|
try {
|
|
52
|
-
return await
|
|
53
|
+
return await fn();
|
|
53
54
|
}
|
|
54
55
|
finally {
|
|
55
56
|
unlock();
|
|
@@ -115,6 +116,124 @@ export class NrekiKernel {
|
|
|
115
116
|
return true;
|
|
116
117
|
return false;
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Convierte una posición LSP (Línea, Carácter 0-indexed)
|
|
121
|
+
* a un índice absoluto de string. Sobrevive \r\n (Windows).
|
|
122
|
+
*/
|
|
123
|
+
getLspOffset(content, line, character) {
|
|
124
|
+
let currentLine = 0;
|
|
125
|
+
let offset = 0;
|
|
126
|
+
while (currentLine < line && offset < content.length) {
|
|
127
|
+
const nl = content.indexOf('\n', offset);
|
|
128
|
+
if (nl === -1)
|
|
129
|
+
break;
|
|
130
|
+
offset = nl + 1;
|
|
131
|
+
currentLine++;
|
|
132
|
+
}
|
|
133
|
+
return Math.min(offset + character, content.length);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* TTRD Sintáctico - Micro-Scanner Híbrido (O(N), Cero dependencias).
|
|
137
|
+
* Extrae firmas de Python y Go crudas del VFS a prueba de
|
|
138
|
+
* formateadores (Black/Ruff/gofmt) y strings embebidos.
|
|
139
|
+
* Usa Regex SOLO para anclar el inicio (def/func), y un
|
|
140
|
+
* Bracket Balancer para parsear el interior.
|
|
141
|
+
*/
|
|
142
|
+
extractRawSignatures(content, ext) {
|
|
143
|
+
const signatures = new Map();
|
|
144
|
+
if (ext !== ".py" && ext !== ".go")
|
|
145
|
+
return signatures;
|
|
146
|
+
const startRegex = ext === ".py"
|
|
147
|
+
? /^(?:async\s+)?def\s+([a-zA-Z_]\w*)\s*\(/gm
|
|
148
|
+
: /^func\s+(?:\([^)]*\)\s+)?([A-Z][a-zA-Z0-9_]*)\s*\(/gm;
|
|
149
|
+
let match;
|
|
150
|
+
while ((match = startRegex.exec(content)) !== null) {
|
|
151
|
+
const symbolName = match[1];
|
|
152
|
+
const startIndex = match.index;
|
|
153
|
+
let i = startIndex + match[0].length - 1;
|
|
154
|
+
let parenDepth = 0;
|
|
155
|
+
let inString = null;
|
|
156
|
+
let foundEnd = false;
|
|
157
|
+
for (; i < content.length; i++) {
|
|
158
|
+
const ch = content[i];
|
|
159
|
+
if (inString) {
|
|
160
|
+
if (ch === '\\')
|
|
161
|
+
i++;
|
|
162
|
+
else if (ch === inString)
|
|
163
|
+
inString = null;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
167
|
+
inString = ch;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === '(')
|
|
171
|
+
parenDepth++;
|
|
172
|
+
else if (ch === ')') {
|
|
173
|
+
parenDepth--;
|
|
174
|
+
if (parenDepth === 0) {
|
|
175
|
+
foundEnd = true;
|
|
176
|
+
i++;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (foundEnd) {
|
|
182
|
+
let sigEnd = i;
|
|
183
|
+
if (ext === ".py") {
|
|
184
|
+
while (sigEnd < content.length && content[sigEnd] !== ':')
|
|
185
|
+
sigEnd++;
|
|
186
|
+
if (sigEnd < content.length)
|
|
187
|
+
sigEnd++;
|
|
188
|
+
}
|
|
189
|
+
else if (ext === ".go") {
|
|
190
|
+
while (sigEnd < content.length && content[sigEnd] !== '{')
|
|
191
|
+
sigEnd++;
|
|
192
|
+
}
|
|
193
|
+
const rawSig = content.substring(startIndex, sigEnd).replace(/\s+/g, ' ').trim();
|
|
194
|
+
signatures.set(symbolName, rawSig);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return signatures;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* TTRD Sintáctico - Detector de Regresión por Toxicidad Estructural.
|
|
201
|
+
* NO cuenta caracteres. Detecta 3 crímenes específicos:
|
|
202
|
+
* 1. Inyección tóxica (Any/interface{} donde no existía)
|
|
203
|
+
* 2. Colapso de retorno (pérdida de -> en Python, return type en Go)
|
|
204
|
+
* 3. Amnesia de parámetros (pérdida total de anotaciones de tipo)
|
|
205
|
+
*/
|
|
206
|
+
detectSignatureRegression(oldSig, newSig, ext) {
|
|
207
|
+
// 1. INYECCIÓN TÓXICA
|
|
208
|
+
const toxicRegex = ext === ".py" ? /\b(Any|any)\b/ : /\b(any|interface\{\})\b/;
|
|
209
|
+
if (!toxicRegex.test(oldSig) && toxicRegex.test(newSig)) {
|
|
210
|
+
return { isRegression: true, reason: `Injected toxic untyped '${ext === ".py" ? "Any" : "interface{}"}'` };
|
|
211
|
+
}
|
|
212
|
+
// 2. COLAPSO ESTRUCTURAL (Pérdida del Retorno)
|
|
213
|
+
if (ext === ".py") {
|
|
214
|
+
const hadReturn = /->\s*[^:]+/.test(oldSig);
|
|
215
|
+
const hasReturn = /->\s*[^:]+/.test(newSig);
|
|
216
|
+
if (hadReturn && !hasReturn) {
|
|
217
|
+
return { isRegression: true, reason: "Lost return type annotation (->)" };
|
|
218
|
+
}
|
|
219
|
+
// 3. AMNESIA DE PARÁMETROS (Pérdida total)
|
|
220
|
+
const oldParams = oldSig.match(/\((.*)\)/)?.[1] || "";
|
|
221
|
+
const newParams = newSig.match(/\((.*)\)/)?.[1] || "";
|
|
222
|
+
const oldAnnotations = (oldParams.match(/:/g) || []).length;
|
|
223
|
+
const newAnnotations = (newParams.match(/:/g) || []).length;
|
|
224
|
+
if (oldAnnotations > 0 && newAnnotations === 0) {
|
|
225
|
+
return { isRegression: true, reason: "Stripped all parameter type annotations" };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (ext === ".go") {
|
|
229
|
+
const oldReturn = oldSig.substring(oldSig.lastIndexOf(')') + 1).replace('{', '').trim();
|
|
230
|
+
const newReturn = newSig.substring(newSig.lastIndexOf(')') + 1).replace('{', '').trim();
|
|
231
|
+
if (oldReturn.length > 0 && newReturn.length === 0) {
|
|
232
|
+
return { isRegression: true, reason: "Lost return type annotation" };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { isRegression: false, reason: "" };
|
|
236
|
+
}
|
|
118
237
|
// DRY: Config loading delegated to backend (Strangler Fig Phase 2A).
|
|
119
238
|
// Kernel copies references after the call. All 32 usages of this.rootNames
|
|
120
239
|
// and 13 usages of this.compilerOptions continue working unchanged.
|
|
@@ -279,7 +398,7 @@ export class NrekiKernel {
|
|
|
279
398
|
const posixPath = self.toPosix(path.resolve(self.projectRoot, fileName));
|
|
280
399
|
if (self.vfsClock.has(posixPath))
|
|
281
400
|
return self.vfsClock.get(posixPath);
|
|
282
|
-
return ts.sys.getModifiedTime
|
|
401
|
+
return ts.sys.getModifiedTime?.(fileName) ?? new Date();
|
|
283
402
|
},
|
|
284
403
|
directoryExists(dirName) {
|
|
285
404
|
const posixDir = self.toPosix(path.resolve(self.projectRoot, dirName));
|
|
@@ -350,8 +469,31 @@ export class NrekiKernel {
|
|
|
350
469
|
if (this.mode === "project") {
|
|
351
470
|
this.tsBackend.captureBaseline(undefined, this.mode);
|
|
352
471
|
}
|
|
353
|
-
// D2:
|
|
472
|
+
// D2: WAL-aware crash recovery.
|
|
473
|
+
// If a previous session crashed mid-commit, the WAL tells us
|
|
474
|
+
// exactly which files to restore from their .bak backups.
|
|
354
475
|
const txDir = path.join(this.projectRoot, ".nreki", "transactions");
|
|
476
|
+
const walPath = path.join(txDir, "wal.json");
|
|
477
|
+
if (fs.existsSync(walPath)) {
|
|
478
|
+
try {
|
|
479
|
+
const wal = JSON.parse(fs.readFileSync(walPath, "utf-8"));
|
|
480
|
+
if (wal.status === "pending" && Array.isArray(wal.files)) {
|
|
481
|
+
logger.warn(`WAL recovery: restoring ${wal.files.length} file(s) from crashed transaction`);
|
|
482
|
+
for (const entry of wal.files) {
|
|
483
|
+
if (entry.backup && fs.existsSync(entry.backup)) {
|
|
484
|
+
fs.renameSync(entry.backup, entry.target);
|
|
485
|
+
}
|
|
486
|
+
else if (!entry.backup && fs.existsSync(entry.target)) {
|
|
487
|
+
// File was newly created in the crashed tx — remove it
|
|
488
|
+
fs.unlinkSync(entry.target);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (walErr) {
|
|
494
|
+
logger.error(`WAL recovery failed: ${walErr}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
355
497
|
if (fs.existsSync(txDir)) {
|
|
356
498
|
try {
|
|
357
499
|
fs.rmSync(txDir, { recursive: true, force: true });
|
|
@@ -565,6 +707,146 @@ export class NrekiKernel {
|
|
|
565
707
|
finalErrors: []
|
|
566
708
|
};
|
|
567
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* NREKI L3.4: LSP Auto-Healing (Go / Python)
|
|
712
|
+
* DISEÑO ACORAZADO:
|
|
713
|
+
* - Latencia JSON-RPC es alta (~300ms/ciclo). Límite estricto de 2 iteraciones.
|
|
714
|
+
* - Whitelist ultra-conservadora: SOLO CodeActions con "import" o "add" en título.
|
|
715
|
+
* - Rechaza explícitamente "remove" y "delete" (destructivos).
|
|
716
|
+
* - Micro-rollback dual: VFS de NREKI + VFS del sidecar (cura Split-Brain).
|
|
717
|
+
* - Macro-rollback completo si quedan errores al final.
|
|
718
|
+
*/
|
|
719
|
+
async attemptLspAutoHealing(sidecar, initialErrors, parentEditedFiles) {
|
|
720
|
+
if (initialErrors.length === 0 || sidecar.isDead) {
|
|
721
|
+
return { healed: false, appliedFixes: [], newlyTouchedFiles: new Set(), finalErrors: initialErrors };
|
|
722
|
+
}
|
|
723
|
+
const MAX_ITERATIONS = 2;
|
|
724
|
+
const fixDescriptions = new Set();
|
|
725
|
+
const localEditedFiles = new Set(parentEditedFiles);
|
|
726
|
+
const newlyTouchedFiles = new Set();
|
|
727
|
+
const healUndoLog = new Map();
|
|
728
|
+
const failedFixHashes = new Set();
|
|
729
|
+
let currentErrors = initialErrors;
|
|
730
|
+
let iteration = 0;
|
|
731
|
+
while (currentErrors.length > 0 && iteration < MAX_ITERATIONS) {
|
|
732
|
+
let appliedAnyFix = false;
|
|
733
|
+
for (const error of currentErrors) {
|
|
734
|
+
const lspRange = {
|
|
735
|
+
start: { line: error.line - 1, character: Math.max(0, error.column - 1) },
|
|
736
|
+
end: { line: error.line - 1, character: error.column }
|
|
737
|
+
};
|
|
738
|
+
const diagnostic = {
|
|
739
|
+
range: lspRange,
|
|
740
|
+
message: error.message,
|
|
741
|
+
code: error.code.replace(`${sidecar.languageId}-`, ""),
|
|
742
|
+
};
|
|
743
|
+
let fixes = [];
|
|
744
|
+
try {
|
|
745
|
+
fixes = await sidecar.requestCodeActions(error.file, diagnostic);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (!fixes || fixes.length === 0)
|
|
751
|
+
continue;
|
|
752
|
+
// EL MURO DE HIELO: Whitelist estricta por title
|
|
753
|
+
const safeFixes = fixes.filter((f) => {
|
|
754
|
+
const title = (f.title || "").toLowerCase();
|
|
755
|
+
if (title.includes("remove") || title.includes("delete"))
|
|
756
|
+
return false;
|
|
757
|
+
return title.includes("import") || title.includes("add ");
|
|
758
|
+
});
|
|
759
|
+
if (safeFixes.length === 0)
|
|
760
|
+
continue;
|
|
761
|
+
const bestFix = safeFixes[0];
|
|
762
|
+
const fixDesc = bestFix.title || `Add import in ${path.basename(error.file)}`;
|
|
763
|
+
const fixHash = `${error.file}:${error.line}:${fixDesc}`;
|
|
764
|
+
if (failedFixHashes.has(fixHash))
|
|
765
|
+
continue;
|
|
766
|
+
const changePath = this.toPosix(path.resolve(this.projectRoot, bestFix.filePath || error.file));
|
|
767
|
+
// Backup VFS (Micro-Rollback log)
|
|
768
|
+
const state = {
|
|
769
|
+
content: this.vfs.has(changePath) ? this.vfs.get(changePath) : undefined,
|
|
770
|
+
time: this.vfsClock.get(changePath)
|
|
771
|
+
};
|
|
772
|
+
if (!healUndoLog.has(changePath))
|
|
773
|
+
healUndoLog.set(changePath, state);
|
|
774
|
+
// Aplicar Fix traduciendo coordenadas LSP → byte offsets
|
|
775
|
+
let content = this.vfs.get(changePath) ?? fs.readFileSync(changePath, "utf-8");
|
|
776
|
+
const startIdx = this.getLspOffset(content, bestFix.range.start.line, bestFix.range.start.character);
|
|
777
|
+
const endIdx = this.getLspOffset(content, bestFix.range.end.line, bestFix.range.end.character);
|
|
778
|
+
content = content.slice(0, startIdx) + bestFix.newText + content.slice(endIdx);
|
|
779
|
+
this.vfs.set(changePath, content);
|
|
780
|
+
this.vfsClock.set(changePath, new Date(this.logicalTime));
|
|
781
|
+
localEditedFiles.add(changePath);
|
|
782
|
+
if (!parentEditedFiles.has(changePath))
|
|
783
|
+
newlyTouchedFiles.add(changePath);
|
|
784
|
+
// Re-evaluar contra el Sidecar (Round-Trip)
|
|
785
|
+
this.logicalTime += 1000;
|
|
786
|
+
let newErrors = [];
|
|
787
|
+
try {
|
|
788
|
+
const scEdits = Array.from(localEditedFiles).map(f => ({
|
|
789
|
+
filePath: f, content: this.vfs.get(f) ?? null
|
|
790
|
+
}));
|
|
791
|
+
newErrors = await sidecar.validateEdits(scEdits);
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
newErrors = currentErrors;
|
|
795
|
+
}
|
|
796
|
+
// TEOREMA DE REDUCCIÓN ESTRICTA
|
|
797
|
+
const oldFileErrors = currentErrors.filter(e => e.file === error.file).length;
|
|
798
|
+
const newFileErrors = newErrors.filter(e => e.file === error.file).length;
|
|
799
|
+
if (newFileErrors >= oldFileErrors) {
|
|
800
|
+
// MICRO-ROLLBACK NREKI VFS
|
|
801
|
+
if (state.content !== undefined)
|
|
802
|
+
this.vfs.set(changePath, state.content);
|
|
803
|
+
else
|
|
804
|
+
this.vfs.delete(changePath);
|
|
805
|
+
if (state.time)
|
|
806
|
+
this.vfsClock.set(changePath, state.time);
|
|
807
|
+
else
|
|
808
|
+
this.vfsClock.delete(changePath);
|
|
809
|
+
if (!parentEditedFiles.has(changePath))
|
|
810
|
+
localEditedFiles.delete(changePath);
|
|
811
|
+
newlyTouchedFiles.delete(changePath);
|
|
812
|
+
// MICRO-ROLLBACK SIDECAR VFS (Cura Split-Brain)
|
|
813
|
+
sidecar.validateEdits([{ filePath: changePath, content: state.content ?? null }]).catch(() => { });
|
|
814
|
+
failedFixHashes.add(fixHash);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
// FIX ACEPTADO
|
|
818
|
+
fixDescriptions.add(`- LSP Auto-Heal (${sidecar.languageId}): ${fixDesc}`);
|
|
819
|
+
appliedAnyFix = true;
|
|
820
|
+
currentErrors = currentErrors.filter(e => e.file !== error.file).concat(newErrors.filter(e => e.file === error.file));
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (!appliedAnyFix)
|
|
825
|
+
break;
|
|
826
|
+
iteration++;
|
|
827
|
+
}
|
|
828
|
+
// MACRO-ROLLBACK si quedan errores
|
|
829
|
+
if (currentErrors.length > 0) {
|
|
830
|
+
const rollbacks = [];
|
|
831
|
+
for (const [p, state] of healUndoLog.entries()) {
|
|
832
|
+
if (state.content !== undefined)
|
|
833
|
+
this.vfs.set(p, state.content);
|
|
834
|
+
else
|
|
835
|
+
this.vfs.delete(p);
|
|
836
|
+
rollbacks.push({ filePath: p, content: state.content ?? null });
|
|
837
|
+
}
|
|
838
|
+
if (rollbacks.length > 0) {
|
|
839
|
+
this.logicalTime += 1000;
|
|
840
|
+
sidecar.validateEdits(rollbacks).catch(() => { });
|
|
841
|
+
}
|
|
842
|
+
this._healingStats.failed++;
|
|
843
|
+
return { healed: false, appliedFixes: [], newlyTouchedFiles: new Set(), finalErrors: initialErrors };
|
|
844
|
+
}
|
|
845
|
+
this._healingStats.applied++;
|
|
846
|
+
for (const file of localEditedFiles)
|
|
847
|
+
parentEditedFiles.add(file);
|
|
848
|
+
return { healed: true, appliedFixes: Array.from(fixDescriptions), newlyTouchedFiles, finalErrors: [] };
|
|
849
|
+
}
|
|
568
850
|
/**
|
|
569
851
|
* SINGLE ENTRY POINT (P21, P22).
|
|
570
852
|
* Atomic batch validation: inject all edits into VFS, evaluate macro-state.
|
|
@@ -581,218 +863,352 @@ export class NrekiKernel {
|
|
|
581
863
|
logger.warn("Rebuilding after timeout-corrupted state.");
|
|
582
864
|
}
|
|
583
865
|
return this.mutex.withLock(async () => {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
866
|
+
const txId = crypto.randomBytes(4).toString("hex");
|
|
867
|
+
setTxId(txId);
|
|
868
|
+
logger.info(`interceptAtomicBatch: ${edits.length} file(s)`);
|
|
869
|
+
try {
|
|
870
|
+
const t0 = performance.now();
|
|
871
|
+
const rollbackState = new Map();
|
|
872
|
+
// B6: Save logicalTime for rollback
|
|
873
|
+
const savedLogicalTime = this.logicalTime;
|
|
874
|
+
this.logicalTime += 1000;
|
|
875
|
+
const explicitlyEditedFiles = new Set();
|
|
876
|
+
// A-02: Snapshot vfsDirectories for rollback
|
|
877
|
+
const savedDirectories = new Set(this.vfsDirectories);
|
|
878
|
+
// PATCH-1: Track which files THIS transaction adds to mutatedFiles.
|
|
879
|
+
// On rollback, only these are removed — preserving prior valid mutations.
|
|
880
|
+
const transactionMutated = new Set();
|
|
881
|
+
// HOLOGRAM: set currentEditTargets so VFS hooks show them as real .ts
|
|
882
|
+
if (this.mode === "hologram") {
|
|
883
|
+
for (const edit of edits) {
|
|
884
|
+
if (edit.proposedContent !== null) {
|
|
885
|
+
const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
|
|
886
|
+
this.currentEditTargets.add(posixPath);
|
|
887
|
+
// Ensure edited file is in rootNames for hologram lazy subgraph
|
|
888
|
+
if (this.isTypeScriptFile(posixPath) && !this.rootNames.has(posixPath)) {
|
|
889
|
+
this.rootNames.add(posixPath);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// TTRD PRE-SCAN: Identify files that will be edited (before VFS mutation)
|
|
594
895
|
for (const edit of edits) {
|
|
595
896
|
if (edit.proposedContent !== null) {
|
|
596
897
|
const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
|
|
597
|
-
|
|
598
|
-
// Ensure edited file is in rootNames for hologram lazy subgraph
|
|
599
|
-
if (this.isTypeScriptFile(posixPath) && !this.rootNames.has(posixPath)) {
|
|
600
|
-
this.rootNames.add(posixPath);
|
|
601
|
-
}
|
|
898
|
+
explicitlyEditedFiles.add(posixPath);
|
|
602
899
|
}
|
|
603
900
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
filesToEvaluate.add(posixDep);
|
|
619
|
-
if (this.prunedTsLookup.has(posixDep)) {
|
|
620
|
-
this.prunedTsLookup.delete(posixDep);
|
|
621
|
-
this.shadowDtsLookup.delete(posixDep.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
|
|
622
|
-
if (!this.rootNames.has(posixDep)) {
|
|
623
|
-
this.rootNames.add(posixDep);
|
|
901
|
+
// TOPOLOGICAL INJECTION: unveil dependents in hologram mode
|
|
902
|
+
const filesToEvaluate = new Set(explicitlyEditedFiles);
|
|
903
|
+
const temporarilyUnveiled = new Set();
|
|
904
|
+
if (this.mode === "hologram") {
|
|
905
|
+
for (const dep of dependents) {
|
|
906
|
+
const posixDep = this.toPosix(path.resolve(this.projectRoot, dep));
|
|
907
|
+
filesToEvaluate.add(posixDep);
|
|
908
|
+
if (this.prunedTsLookup.has(posixDep)) {
|
|
909
|
+
this.prunedTsLookup.delete(posixDep);
|
|
910
|
+
this.shadowDtsLookup.delete(posixDep.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
|
|
911
|
+
if (!this.rootNames.has(posixDep)) {
|
|
912
|
+
this.rootNames.add(posixDep);
|
|
913
|
+
}
|
|
914
|
+
temporarilyUnveiled.add(posixDep);
|
|
624
915
|
}
|
|
625
|
-
temporarilyUnveiled.add(posixDep);
|
|
626
916
|
}
|
|
627
917
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
918
|
+
// JIT baseline: recapture baseline scoped to files we will evaluate
|
|
919
|
+
if (this.mode === "hologram" || this.mode === "file") {
|
|
920
|
+
this.tsBackend.updateProgram();
|
|
921
|
+
// DR/LS synced via tsBackend
|
|
922
|
+
this.tsBackend.captureBaseline(filesToEvaluate, this.mode);
|
|
923
|
+
if (this.bootErrorCount === -1) {
|
|
924
|
+
this.bootErrorCount = this.getBaselineErrorCount();
|
|
925
|
+
}
|
|
636
926
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
// Sidecar edits hoisted for catch-path compensatory rollback (Bomba 1)
|
|
642
|
-
const sidecarEdits = new Map();
|
|
643
|
-
try {
|
|
644
|
-
// PHASE 1: Inject entire batch into VFS
|
|
927
|
+
// TTRD: Extract pre-mutation type contracts (before VFS injection)
|
|
928
|
+
const preContracts = await this.tsBackend.extractCanonicalTypes(explicitlyEditedFiles);
|
|
929
|
+
// TTRD SINTÁCTICO PRE-SCAN: Python/Go
|
|
930
|
+
const preRawSignatures = new Map();
|
|
645
931
|
for (const edit of edits) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const currentlyInRoots = this.rootNames.has(posixPath);
|
|
654
|
-
// P25: Idempotent undo-log - first touch only
|
|
655
|
-
if (!rollbackState.has(posixPath)) {
|
|
656
|
-
rollbackState.set(posixPath, {
|
|
657
|
-
content: this.vfs.has(posixPath) ? this.vfs.get(posixPath) : undefined,
|
|
658
|
-
time: this.vfsClock.get(posixPath),
|
|
659
|
-
wasInRoot: currentlyInRoots,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
this.vfs.set(posixPath, edit.proposedContent);
|
|
663
|
-
this.vfsClock.set(posixPath, new Date(this.logicalTime));
|
|
664
|
-
this.mutatedFiles.add(posixPath);
|
|
665
|
-
// P29: Tombstone removes from rootNames
|
|
666
|
-
// P30: Only TS files enter rootNames
|
|
667
|
-
if (edit.proposedContent === null) {
|
|
668
|
-
this.rootNames.delete(posixPath);
|
|
932
|
+
if (edit.proposedContent !== null) {
|
|
933
|
+
const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
|
|
934
|
+
const ext = path.extname(posixPath).toLowerCase();
|
|
935
|
+
if (ext === ".py" || ext === ".go") {
|
|
936
|
+
const oldContent = this.vfs.get(posixPath) ?? this.tsBackend.host.readFile(posixPath) ?? "";
|
|
937
|
+
preRawSignatures.set(posixPath, this.extractRawSignatures(oldContent, ext));
|
|
938
|
+
}
|
|
669
939
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
940
|
+
}
|
|
941
|
+
// A-01: Wrap Phase 1-4 so partial VFS mutations are rolled back on throw
|
|
942
|
+
// Sidecar edits hoisted for catch-path compensatory rollback (Bomba 1)
|
|
943
|
+
const sidecarEdits = new Map();
|
|
944
|
+
try {
|
|
945
|
+
// PHASE 1: Inject entire batch into VFS
|
|
946
|
+
for (const edit of edits) {
|
|
947
|
+
const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
|
|
948
|
+
const rootPosix = this.toPosix(path.resolve(this.projectRoot));
|
|
949
|
+
// A1: Path Jail - block traversal attempts at kernel level
|
|
950
|
+
if (!posixPath.startsWith(rootPosix + "/") && posixPath !== rootPosix) {
|
|
951
|
+
throw new Error(`[NREKI] Security rejection: Path traversal blocked. ` +
|
|
952
|
+
`"${edit.targetFile}" resolves outside project root.`);
|
|
674
953
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
954
|
+
const currentlyInRoots = this.rootNames.has(posixPath);
|
|
955
|
+
// P25: Idempotent undo-log - first touch only
|
|
956
|
+
if (!rollbackState.has(posixPath)) {
|
|
957
|
+
rollbackState.set(posixPath, {
|
|
958
|
+
content: this.vfs.has(posixPath) ? this.vfs.get(posixPath) : undefined,
|
|
959
|
+
time: this.vfsClock.get(posixPath),
|
|
960
|
+
wasInRoot: currentlyInRoots,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
this.vfs.set(posixPath, edit.proposedContent);
|
|
964
|
+
this.vfsClock.set(posixPath, new Date(this.logicalTime));
|
|
965
|
+
if (!this.mutatedFiles.has(posixPath))
|
|
966
|
+
transactionMutated.add(posixPath);
|
|
967
|
+
this.mutatedFiles.add(posixPath);
|
|
968
|
+
// P29: Tombstone removes from rootNames
|
|
969
|
+
// P30: Only TS files enter rootNames
|
|
970
|
+
if (edit.proposedContent === null) {
|
|
971
|
+
this.rootNames.delete(posixPath);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
explicitlyEditedFiles.add(posixPath);
|
|
975
|
+
if (!currentlyInRoots && this.isTypeScriptFile(posixPath)) {
|
|
976
|
+
this.rootNames.add(posixPath);
|
|
977
|
+
}
|
|
978
|
+
// E1: Build directory hierarchy for O(1) lookup
|
|
979
|
+
let dir = path.posix.dirname(posixPath);
|
|
980
|
+
while (dir.length >= rootPosix.length && dir !== rootPosix && dir !== ".") {
|
|
981
|
+
this.vfsDirectories.add(dir);
|
|
982
|
+
dir = path.posix.dirname(dir);
|
|
983
|
+
}
|
|
680
984
|
}
|
|
681
985
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
986
|
+
// PHASE 2: Rebuild incremental program
|
|
987
|
+
this.tsBackend.updateProgram();
|
|
988
|
+
// DR/LS synced via tsBackend
|
|
989
|
+
// BUG 3 FIXED: Compute AI errors exactly ONCE.
|
|
990
|
+
// getFatalErrors consumes the stateful getSemanticDiagnosticsOfNextAffectedFile iterator.
|
|
991
|
+
// Cannot call it again without updateProgram() in between.
|
|
992
|
+
const originalFatalErrors = await this.tsBackend.getDiagnostics(filesToEvaluate, explicitlyEditedFiles, this.mode);
|
|
993
|
+
// ─── Phase 2.5: LSP Sidecar Validation (Go, Python) ─────────
|
|
994
|
+
const sidecarWarnings = [];
|
|
995
|
+
// Collect non-TS edits grouped by sidecar
|
|
996
|
+
sidecarEdits.clear();
|
|
997
|
+
for (const edit of edits) {
|
|
998
|
+
const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
|
|
999
|
+
const ext = path.extname(posixPath).toLowerCase();
|
|
1000
|
+
const sidecar = this.lspSidecars.get(ext);
|
|
1001
|
+
if (!sidecar)
|
|
1002
|
+
continue;
|
|
1003
|
+
const list = sidecarEdits.get(sidecar) || [];
|
|
1004
|
+
list.push({ filePath: posixPath, content: edit.proposedContent });
|
|
1005
|
+
sidecarEdits.set(sidecar, list);
|
|
1006
|
+
}
|
|
1007
|
+
// Boot + validate each sidecar
|
|
1008
|
+
for (const [sidecar, scEdits] of sidecarEdits) {
|
|
1009
|
+
if (!sidecar.isHealthy()) {
|
|
1010
|
+
try {
|
|
1011
|
+
await sidecar.boot();
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
sidecarWarnings.push(`[⚠️ NREKI WARNING: '${sidecar.command[0]}' not found or crashed. ` +
|
|
1015
|
+
`Layer 2 Semantic Shield OFFLINE for ${sidecar.languageId} files. ` +
|
|
1016
|
+
`Code passed syntax only. Install to enable strict validation.]`);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
707
1020
|
try {
|
|
708
|
-
await sidecar.
|
|
1021
|
+
const sidecarErrors = await sidecar.validateEdits(scEdits);
|
|
1022
|
+
// ACID: Merge sidecar errors. If ANY backend rejects, NOTHING touches disk.
|
|
1023
|
+
originalFatalErrors.push(...sidecarErrors);
|
|
709
1024
|
}
|
|
710
1025
|
catch {
|
|
711
|
-
sidecarWarnings.push(`[⚠️ NREKI WARNING:
|
|
712
|
-
`
|
|
713
|
-
`Code passed syntax only. Install to enable strict validation.]`);
|
|
714
|
-
continue;
|
|
1026
|
+
sidecarWarnings.push(`[⚠️ NREKI WARNING: ${sidecar.languageId} validation timed out. ` +
|
|
1027
|
+
`Code passed without semantic check.]`);
|
|
715
1028
|
}
|
|
716
1029
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
// LSP sidecars have supportsAutoHealing: false in v1.
|
|
733
|
-
const tsOnlyErrors = originalFatalErrors.filter(e => this.isTypeScriptFile(e.file));
|
|
734
|
-
const lspOnlyErrors = originalFatalErrors.filter(e => !this.isTypeScriptFile(e.file));
|
|
735
|
-
const tHealStart = performance.now();
|
|
736
|
-
const healing = tsOnlyErrors.length > 0
|
|
737
|
-
? await this.attemptAutoHealing(tsOnlyErrors, explicitlyEditedFiles, filesToEvaluate)
|
|
738
|
-
: { healed: false, appliedFixes: [], newlyTouchedFiles: new Set(), finalErrors: [] };
|
|
739
|
-
// If TS healed but LSP still has errors, overall still fails
|
|
740
|
-
if (healing.healed && lspOnlyErrors.length > 0) {
|
|
741
|
-
healing.healed = false;
|
|
742
|
-
healing.finalErrors = lspOnlyErrors;
|
|
1030
|
+
// ─── End Phase 2.5 ───────────────────────────────────────────
|
|
1031
|
+
// ─── TTRD SINTÁCTICO: Evaluación Post-Mutación ───
|
|
1032
|
+
for (const [posixPath, oldSigsMap] of preRawSignatures.entries()) {
|
|
1033
|
+
const newContent = this.vfs.get(posixPath) ?? "";
|
|
1034
|
+
const ext = path.extname(posixPath).toLowerCase();
|
|
1035
|
+
const newSigsMap = this.extractRawSignatures(newContent, ext);
|
|
1036
|
+
for (const [symbol, oldSig] of oldSigsMap.entries()) {
|
|
1037
|
+
const newSig = newSigsMap.get(symbol);
|
|
1038
|
+
if (newSig) {
|
|
1039
|
+
const { isRegression, reason } = this.detectSignatureRegression(oldSig, newSig, ext);
|
|
1040
|
+
if (isRegression) {
|
|
1041
|
+
sidecarWarnings.push(`[⚠️ TTRD WARNING] Type degradation detected in ${path.basename(posixPath)}::${symbol}().\n Reason: ${reason}\n Old: \`${oldSig}\`\n New: \`${newSig}\``);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
743
1045
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1046
|
+
// PHASE 4: Verdict
|
|
1047
|
+
if (originalFatalErrors.length > 0) {
|
|
1048
|
+
// ─── NREKI L3.3: Iterative Auto-Healing (Dual Cascade) ─────────────────
|
|
1049
|
+
const tsOnlyErrors = originalFatalErrors.filter(e => this.isTypeScriptFile(e.file));
|
|
1050
|
+
const lspOnlyErrors = originalFatalErrors.filter(e => !this.isTypeScriptFile(e.file));
|
|
1051
|
+
const tHealStart = performance.now();
|
|
1052
|
+
let isFullyHealed = true;
|
|
1053
|
+
const allAppliedFixes = [];
|
|
1054
|
+
const allNewlyTouchedFiles = new Set();
|
|
1055
|
+
// 1. Sanación TypeScript (CodeFix API, ~20ms/ciclo)
|
|
1056
|
+
let remainingTsErrors = tsOnlyErrors;
|
|
1057
|
+
if (tsOnlyErrors.length > 0) {
|
|
1058
|
+
const tsHealing = await this.attemptAutoHealing(tsOnlyErrors, explicitlyEditedFiles, filesToEvaluate);
|
|
1059
|
+
if (!tsHealing.healed)
|
|
1060
|
+
isFullyHealed = false;
|
|
1061
|
+
remainingTsErrors = tsHealing.finalErrors;
|
|
1062
|
+
allAppliedFixes.push(...tsHealing.appliedFixes);
|
|
1063
|
+
for (const f of tsHealing.newlyTouchedFiles)
|
|
1064
|
+
allNewlyTouchedFiles.add(f);
|
|
753
1065
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
this.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1066
|
+
// 2. Sanación LSP (Go/Python, ~300ms/ciclo) — SOLO SI TS SANÓ
|
|
1067
|
+
let remainingLspErrors = lspOnlyErrors;
|
|
1068
|
+
if (isFullyHealed && lspOnlyErrors.length > 0) {
|
|
1069
|
+
const errorsBySidecar = new Map();
|
|
1070
|
+
for (const err of lspOnlyErrors) {
|
|
1071
|
+
const ext = path.extname(err.file).toLowerCase();
|
|
1072
|
+
const sidecar = this.lspSidecars.get(ext);
|
|
1073
|
+
if (sidecar) {
|
|
1074
|
+
const arr = errorsBySidecar.get(sidecar) || [];
|
|
1075
|
+
arr.push(err);
|
|
1076
|
+
errorsBySidecar.set(sidecar, arr);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
remainingLspErrors = [];
|
|
1080
|
+
for (const [sidecar, errors] of errorsBySidecar.entries()) {
|
|
1081
|
+
const lspHealing = await this.attemptLspAutoHealing(sidecar, errors, explicitlyEditedFiles);
|
|
1082
|
+
if (!lspHealing.healed) {
|
|
1083
|
+
isFullyHealed = false;
|
|
1084
|
+
remainingLspErrors.push(...lspHealing.finalErrors);
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
allAppliedFixes.push(...lspHealing.appliedFixes);
|
|
1088
|
+
for (const f of lspHealing.newlyTouchedFiles)
|
|
1089
|
+
allNewlyTouchedFiles.add(f);
|
|
1090
|
+
}
|
|
773
1091
|
}
|
|
774
1092
|
}
|
|
775
|
-
|
|
1093
|
+
// Construir resultado unificado
|
|
1094
|
+
const healing = {
|
|
1095
|
+
healed: isFullyHealed && (remainingTsErrors.length + remainingLspErrors.length) === 0,
|
|
1096
|
+
appliedFixes: allAppliedFixes,
|
|
1097
|
+
newlyTouchedFiles: allNewlyTouchedFiles,
|
|
1098
|
+
finalErrors: [...remainingTsErrors, ...remainingLspErrors],
|
|
1099
|
+
};
|
|
1100
|
+
if (healing.healed) {
|
|
1101
|
+
const latency = (performance.now() - t0).toFixed(2);
|
|
1102
|
+
const healLatency = (performance.now() - tHealStart).toFixed(2);
|
|
1103
|
+
// Patch notice: tell the agent what NOT to overwrite in its next edit
|
|
1104
|
+
let extraFilesWarning = "";
|
|
1105
|
+
if (healing.newlyTouchedFiles.size > 0) {
|
|
1106
|
+
const files = Array.from(healing.newlyTouchedFiles)
|
|
1107
|
+
.map(f => path.relative(this.projectRoot, f)).join(", ");
|
|
1108
|
+
extraFilesWarning = `\nWARNING: NREKI also auto-patched collateral files: ${files}. Do NOT revert them.`;
|
|
1109
|
+
}
|
|
1110
|
+
const patchNotice = `\n\nIMPORTANT: NREKI applied these patches automatically.\n` +
|
|
1111
|
+
`Your code on disk already contains these fixes. ` +
|
|
1112
|
+
`Do not revert or overwrite them in your next edit.` +
|
|
1113
|
+
extraFilesWarning;
|
|
1114
|
+
// Group fixes by type for readable output
|
|
1115
|
+
const tsFixes = healing.appliedFixes.filter(f => !f.includes("LSP Auto-Heal"));
|
|
1116
|
+
const lspFixes = healing.appliedFixes.filter(f => f.includes("LSP Auto-Heal"));
|
|
1117
|
+
let fixSummary = "";
|
|
1118
|
+
if (tsFixes.length > 0) {
|
|
1119
|
+
fixSummary += `\n TypeScript (CodeFix API):\n${tsFixes.join("\n")}`;
|
|
1120
|
+
}
|
|
1121
|
+
if (lspFixes.length > 0) {
|
|
1122
|
+
fixSummary += `\n Go/Python (LSP codeAction):\n${lspFixes.join("\n")}`;
|
|
1123
|
+
}
|
|
1124
|
+
// TTRD post-contracts (healed path)
|
|
1125
|
+
const finalEditedFiles = new Set(explicitlyEditedFiles);
|
|
1126
|
+
for (const f of healing.newlyTouchedFiles)
|
|
1127
|
+
finalEditedFiles.add(f);
|
|
1128
|
+
const postContracts = await this.tsBackend.extractCanonicalTypes(finalEditedFiles);
|
|
1129
|
+
const regressions = this.tsBackend.computeTypeRegressions(preContracts, postContracts);
|
|
1130
|
+
this.restoreHologramVeil(temporarilyUnveiled);
|
|
1131
|
+
return {
|
|
1132
|
+
safe: true,
|
|
1133
|
+
exitCode: 0,
|
|
1134
|
+
latencyMs: latency,
|
|
1135
|
+
healedFiles: Array.from(healing.newlyTouchedFiles),
|
|
1136
|
+
errorText: `[NREKI AUTO-HEAL: ${healLatency}ms] ` +
|
|
1137
|
+
`Your code had structural errors. NREKI applied deterministic fixes in RAM:\n` +
|
|
1138
|
+
fixSummary +
|
|
1139
|
+
patchNotice,
|
|
1140
|
+
regressions: regressions.length > 0 ? regressions : undefined,
|
|
1141
|
+
postContracts: postContracts.size > 0
|
|
1142
|
+
? new Map([...postContracts].map(([file, syms]) => [file, new Map([...syms].map(([sym, contract]) => [sym, contract.typeStr]))]))
|
|
1143
|
+
: undefined,
|
|
1144
|
+
warnings: sidecarWarnings.length > 0 ? sidecarWarnings : undefined,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
// ─── END Auto-Healing ────────────────────────────────────
|
|
1148
|
+
// Healing failed. Use the ORIGINAL error matrix (do not recalculate).
|
|
1149
|
+
const structured = originalFatalErrors; // Already NrekiStructuredError[]
|
|
1150
|
+
// ACID rollback of the original edit
|
|
1151
|
+
for (const [posixPath, state] of rollbackState.entries()) {
|
|
1152
|
+
if (state.content !== undefined)
|
|
1153
|
+
this.vfs.set(posixPath, state.content);
|
|
1154
|
+
else
|
|
1155
|
+
this.vfs.delete(posixPath);
|
|
1156
|
+
if (state.time)
|
|
1157
|
+
this.vfsClock.set(posixPath, state.time);
|
|
1158
|
+
else
|
|
1159
|
+
this.vfsClock.delete(posixPath);
|
|
1160
|
+
if (state.wasInRoot)
|
|
1161
|
+
this.rootNames.add(posixPath);
|
|
1162
|
+
else
|
|
1163
|
+
this.rootNames.delete(posixPath);
|
|
1164
|
+
}
|
|
1165
|
+
// BOMBA 1 FIX: Compensatory Rollback — heal sidecar VFS
|
|
1166
|
+
// Without this, the LSP's internal VFS retains the rejected
|
|
1167
|
+
// edit and future validations run against a phantom state.
|
|
1168
|
+
this.rollbackSidecars(sidecarEdits, rollbackState);
|
|
1169
|
+
// PATCH-1: Remove this transaction's files from mutatedFiles
|
|
1170
|
+
// to prevent ghost deletion in the next commitToDisk().
|
|
1171
|
+
for (const file of transactionMutated)
|
|
1172
|
+
this.mutatedFiles.delete(file);
|
|
1173
|
+
// B6: Restore logicalTime on rollback
|
|
1174
|
+
this.logicalTime = savedLogicalTime;
|
|
1175
|
+
// A-02: Restore vfsDirectories
|
|
1176
|
+
this.vfsDirectories = savedDirectories;
|
|
1177
|
+
// P17 + P2 WARM-PATH: Advance clock instead of destroying program.
|
|
1178
|
+
this.logicalTime += 1000;
|
|
1179
|
+
for (const [posixPath] of rollbackState.entries()) {
|
|
1180
|
+
this.vfsClock.set(posixPath, new Date(this.logicalTime));
|
|
1181
|
+
}
|
|
1182
|
+
this.tsBackend.updateProgram();
|
|
1183
|
+
// DR/LS synced via tsBackend
|
|
1184
|
+
const latency = (performance.now() - t0).toFixed(2);
|
|
1185
|
+
latencyTracker.record("intercept", parseFloat(latency));
|
|
1186
|
+
this.restoreHologramVeil(temporarilyUnveiled);
|
|
776
1187
|
return {
|
|
777
|
-
safe:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
`Your code had structural errors. NREKI applied deterministic fixes in RAM:\n\n` +
|
|
783
|
-
healing.appliedFixes.join("\n") +
|
|
784
|
-
patchNotice,
|
|
785
|
-
regressions: regressions.length > 0 ? regressions : undefined,
|
|
786
|
-
postContracts: postContracts.size > 0
|
|
787
|
-
? new Map([...postContracts].map(([file, syms]) => [file, new Map([...syms].map(([sym, contract]) => [sym, contract.typeStr]))]))
|
|
788
|
-
: undefined,
|
|
789
|
-
warnings: sidecarWarnings.length > 0 ? sidecarWarnings : undefined,
|
|
1188
|
+
safe: false, exitCode: 2, latencyMs: latency, structured,
|
|
1189
|
+
errorText: `[Edit rejected - ${latency}ms] Atomic transaction aborted. ` +
|
|
1190
|
+
`${originalFatalErrors.length} violation(s) detected in RAM.\n` +
|
|
1191
|
+
structured.map((e) => ` → ${e.file} (${e.line},${e.column}): ${e.code} - ${e.message}`).join("\n") +
|
|
1192
|
+
`\nACTION: Disk untouched. Fix the code and retry.`,
|
|
790
1193
|
};
|
|
791
1194
|
}
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
1195
|
+
// TTRD post-contracts (clean path)
|
|
1196
|
+
const postContracts = await this.tsBackend.extractCanonicalTypes(explicitlyEditedFiles);
|
|
1197
|
+
const regressions = this.tsBackend.computeTypeRegressions(preContracts, postContracts);
|
|
1198
|
+
this.restoreHologramVeil(temporarilyUnveiled);
|
|
1199
|
+
return {
|
|
1200
|
+
safe: true,
|
|
1201
|
+
exitCode: 0,
|
|
1202
|
+
latencyMs: (() => { const l = (performance.now() - t0).toFixed(2); latencyTracker.record("intercept", parseFloat(l)); return l; })(),
|
|
1203
|
+
regressions: regressions.length > 0 ? regressions : undefined,
|
|
1204
|
+
postContracts: postContracts.size > 0
|
|
1205
|
+
? new Map([...postContracts].map(([file, syms]) => [file, new Map([...syms].map(([sym, contract]) => [sym, contract.typeStr]))]))
|
|
1206
|
+
: undefined,
|
|
1207
|
+
warnings: sidecarWarnings.length > 0 ? sidecarWarnings : undefined,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
catch (phaseError) {
|
|
1211
|
+
// A-01: Rollback partial VFS mutations from Phase 1 on any throw
|
|
796
1212
|
for (const [posixPath, state] of rollbackState.entries()) {
|
|
797
1213
|
if (state.content !== undefined)
|
|
798
1214
|
this.vfs.set(posixPath, state.content);
|
|
@@ -807,107 +1223,25 @@ export class NrekiKernel {
|
|
|
807
1223
|
else
|
|
808
1224
|
this.rootNames.delete(posixPath);
|
|
809
1225
|
}
|
|
810
|
-
// BOMBA 1 FIX: Compensatory Rollback
|
|
811
|
-
// Without this, the LSP's internal VFS retains the rejected
|
|
812
|
-
// edit and future validations run against a phantom state.
|
|
1226
|
+
// BOMBA 1 FIX: Compensatory Rollback on crash path
|
|
813
1227
|
this.rollbackSidecars(sidecarEdits, rollbackState);
|
|
814
|
-
//
|
|
815
|
-
|
|
1228
|
+
// PATCH-1: Remove this transaction's files from mutatedFiles (crash path)
|
|
1229
|
+
for (const file of transactionMutated)
|
|
1230
|
+
this.mutatedFiles.delete(file);
|
|
816
1231
|
// A-02: Restore vfsDirectories
|
|
817
1232
|
this.vfsDirectories = savedDirectories;
|
|
818
|
-
|
|
819
|
-
this.
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
// Restore hologram veil after rejection
|
|
827
|
-
if (this.mode === "hologram" && temporarilyUnveiled.size > 0) {
|
|
828
|
-
for (const file of temporarilyUnveiled) {
|
|
829
|
-
this.prunedTsLookup.add(file);
|
|
830
|
-
this.shadowDtsLookup.add(file.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
|
|
831
|
-
this.rootNames.delete(file);
|
|
832
|
-
this.vfsClock.set(file, new Date(this.logicalTime + 1));
|
|
833
|
-
// JIT: force re-classify on next access (content may have changed)
|
|
834
|
-
this.jitClassifiedCache.delete(file);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
this.currentEditTargets.clear();
|
|
838
|
-
return {
|
|
839
|
-
safe: false, exitCode: 2, latencyMs: latency, structured,
|
|
840
|
-
errorText: `[Edit rejected - ${latency}ms] Atomic transaction aborted. ` +
|
|
841
|
-
`${originalFatalErrors.length} violation(s) detected in RAM.\n` +
|
|
842
|
-
structured.map((e) => ` → ${e.file} (${e.line},${e.column}): ${e.code} - ${e.message}`).join("\n") +
|
|
843
|
-
`\nACTION: Disk untouched. Fix the code and retry.`,
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
// TTRD post-contracts (clean path)
|
|
847
|
-
const postContracts = await this.tsBackend.extractCanonicalTypes(explicitlyEditedFiles);
|
|
848
|
-
const regressions = this.tsBackend.computeTypeRegressions(preContracts, postContracts);
|
|
849
|
-
// Restore hologram veil after successful intercept
|
|
850
|
-
if (this.mode === "hologram" && temporarilyUnveiled.size > 0) {
|
|
851
|
-
for (const file of temporarilyUnveiled) {
|
|
852
|
-
this.prunedTsLookup.add(file);
|
|
853
|
-
this.shadowDtsLookup.add(file.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
|
|
854
|
-
this.rootNames.delete(file);
|
|
855
|
-
this.vfsClock.set(file, new Date(this.logicalTime + 1));
|
|
856
|
-
// JIT: force re-classify on next access (content may have changed)
|
|
857
|
-
this.jitClassifiedCache.delete(file);
|
|
858
|
-
}
|
|
1233
|
+
this.logicalTime = savedLogicalTime;
|
|
1234
|
+
this.restoreHologramVeil(temporarilyUnveiled);
|
|
1235
|
+
// FIX: Poison the compiler cache to force cold rebuild.
|
|
1236
|
+
// Without this, the TypeScript BuilderProgram retains state
|
|
1237
|
+
// from the failed transaction, causing phantom errors on
|
|
1238
|
+
// the next interceptAtomicBatch call.
|
|
1239
|
+
this.tsBackend.purgeCache(true);
|
|
1240
|
+
throw phaseError;
|
|
859
1241
|
}
|
|
860
|
-
this.currentEditTargets.clear();
|
|
861
|
-
return {
|
|
862
|
-
safe: true,
|
|
863
|
-
exitCode: 0,
|
|
864
|
-
latencyMs: (performance.now() - t0).toFixed(2),
|
|
865
|
-
regressions: regressions.length > 0 ? regressions : undefined,
|
|
866
|
-
postContracts: postContracts.size > 0
|
|
867
|
-
? new Map([...postContracts].map(([file, syms]) => [file, new Map([...syms].map(([sym, contract]) => [sym, contract.typeStr]))]))
|
|
868
|
-
: undefined,
|
|
869
|
-
warnings: sidecarWarnings.length > 0 ? sidecarWarnings : undefined,
|
|
870
|
-
};
|
|
871
1242
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
for (const [posixPath, state] of rollbackState.entries()) {
|
|
875
|
-
if (state.content !== undefined)
|
|
876
|
-
this.vfs.set(posixPath, state.content);
|
|
877
|
-
else
|
|
878
|
-
this.vfs.delete(posixPath);
|
|
879
|
-
if (state.time)
|
|
880
|
-
this.vfsClock.set(posixPath, state.time);
|
|
881
|
-
else
|
|
882
|
-
this.vfsClock.delete(posixPath);
|
|
883
|
-
if (state.wasInRoot)
|
|
884
|
-
this.rootNames.add(posixPath);
|
|
885
|
-
else
|
|
886
|
-
this.rootNames.delete(posixPath);
|
|
887
|
-
}
|
|
888
|
-
// BOMBA 1 FIX: Compensatory Rollback on crash path
|
|
889
|
-
this.rollbackSidecars(sidecarEdits, rollbackState);
|
|
890
|
-
// A-02: Restore vfsDirectories
|
|
891
|
-
this.vfsDirectories = savedDirectories;
|
|
892
|
-
this.logicalTime = savedLogicalTime;
|
|
893
|
-
// Restore hologram veil on failure
|
|
894
|
-
if (this.mode === "hologram" && temporarilyUnveiled.size > 0) {
|
|
895
|
-
for (const file of temporarilyUnveiled) {
|
|
896
|
-
this.prunedTsLookup.add(file);
|
|
897
|
-
this.shadowDtsLookup.add(file.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
|
|
898
|
-
this.rootNames.delete(file);
|
|
899
|
-
this.vfsClock.set(file, new Date(this.logicalTime + 1));
|
|
900
|
-
// JIT: force re-classify on next access (content may have changed)
|
|
901
|
-
this.jitClassifiedCache.delete(file);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
this.currentEditTargets.clear();
|
|
905
|
-
// FIX: Poison the compiler cache to force cold rebuild.
|
|
906
|
-
// Without this, the TypeScript BuilderProgram retains state
|
|
907
|
-
// from the failed transaction, causing phantom errors on
|
|
908
|
-
// the next interceptAtomicBatch call.
|
|
909
|
-
this.tsBackend.purgeCache(true);
|
|
910
|
-
throw phaseError;
|
|
1243
|
+
finally {
|
|
1244
|
+
clearTxId();
|
|
911
1245
|
}
|
|
912
1246
|
});
|
|
913
1247
|
}
|
|
@@ -922,9 +1256,12 @@ export class NrekiKernel {
|
|
|
922
1256
|
return this.mutex.withLock(async () => {
|
|
923
1257
|
const physicalUndoLog = [];
|
|
924
1258
|
const createdTmps = [];
|
|
1259
|
+
// Scope writes to files mutated in the current transaction only.
|
|
1260
|
+
// Prevents stale VFS entries from prior intercepts leaking into this commit.
|
|
1261
|
+
const filesToCommit = new Set(this.mutatedFiles);
|
|
925
1262
|
try {
|
|
926
1263
|
// PHASE 1: Physical backup
|
|
927
|
-
for (const posixPath of
|
|
1264
|
+
for (const posixPath of filesToCommit) {
|
|
928
1265
|
const osPath = path.normalize(posixPath);
|
|
929
1266
|
if (fs.existsSync(osPath)) {
|
|
930
1267
|
const txDir = path.join(this.projectRoot, ".nreki", "transactions");
|
|
@@ -938,8 +1275,24 @@ export class NrekiKernel {
|
|
|
938
1275
|
physicalUndoLog.push({ target: osPath, backup: null });
|
|
939
1276
|
}
|
|
940
1277
|
}
|
|
1278
|
+
// WAL: Write intent log before destructive Phase 2.
|
|
1279
|
+
// If the process crashes between Phase 1 (backup) and Phase 2 (write),
|
|
1280
|
+
// boot() will find this WAL and automatically restore from backups.
|
|
1281
|
+
const txDir = path.join(this.projectRoot, ".nreki", "transactions");
|
|
1282
|
+
const walPath = path.join(txDir, "wal.json");
|
|
1283
|
+
try {
|
|
1284
|
+
if (!fs.existsSync(txDir))
|
|
1285
|
+
fs.mkdirSync(txDir, { recursive: true });
|
|
1286
|
+
fs.writeFileSync(walPath, JSON.stringify({
|
|
1287
|
+
status: "pending",
|
|
1288
|
+
ts: new Date().toISOString(),
|
|
1289
|
+
files: physicalUndoLog.map(l => ({ target: l.target, backup: l.backup })),
|
|
1290
|
+
}), "utf-8");
|
|
1291
|
+
}
|
|
1292
|
+
catch { /* WAL write failure is non-fatal — proceed without crash safety */ }
|
|
941
1293
|
// PHASE 2: Destructive writes
|
|
942
|
-
for (const
|
|
1294
|
+
for (const posixPath of filesToCommit) {
|
|
1295
|
+
const content = this.vfs.get(posixPath) ?? null;
|
|
943
1296
|
const osPath = path.normalize(posixPath);
|
|
944
1297
|
if (content === null) {
|
|
945
1298
|
if (fs.existsSync(osPath))
|
|
@@ -1001,8 +1354,15 @@ export class NrekiKernel {
|
|
|
1001
1354
|
if (log.backup && fs.existsSync(log.backup))
|
|
1002
1355
|
fs.unlinkSync(log.backup);
|
|
1003
1356
|
}
|
|
1357
|
+
// WAL: Transaction complete — delete intent log
|
|
1358
|
+
try {
|
|
1359
|
+
if (fs.existsSync(walPath))
|
|
1360
|
+
fs.unlinkSync(walPath);
|
|
1361
|
+
}
|
|
1362
|
+
catch { /* best effort */ }
|
|
1004
1363
|
// Release stale AST versions from DocumentRegistry
|
|
1005
1364
|
this.tsBackend.releaseMutatedDocuments(this.mutatedFiles);
|
|
1365
|
+
this.mutatedFiles.clear();
|
|
1006
1366
|
logger.info("Atomic commit materialized. Disk synchronized.");
|
|
1007
1367
|
}
|
|
1008
1368
|
catch (error) {
|
|
@@ -1095,6 +1455,23 @@ export class NrekiKernel {
|
|
|
1095
1455
|
});
|
|
1096
1456
|
}
|
|
1097
1457
|
// ─── Utilities ─────────────────────────────────────────────────
|
|
1458
|
+
/**
|
|
1459
|
+
* Restore the hologram veil for temporarily unveiled files.
|
|
1460
|
+
* Re-adds files to the pruned/shadow lookups, removes them from rootNames,
|
|
1461
|
+
* and invalidates JIT classification cache. Clears currentEditTargets.
|
|
1462
|
+
*/
|
|
1463
|
+
restoreHologramVeil(temporarilyUnveiled) {
|
|
1464
|
+
if (this.mode === "hologram" && temporarilyUnveiled.size > 0) {
|
|
1465
|
+
for (const file of temporarilyUnveiled) {
|
|
1466
|
+
this.prunedTsLookup.add(file);
|
|
1467
|
+
this.shadowDtsLookup.add(file.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
|
|
1468
|
+
this.rootNames.delete(file);
|
|
1469
|
+
this.vfsClock.set(file, new Date(this.logicalTime + 1));
|
|
1470
|
+
this.jitClassifiedCache.delete(file);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
this.currentEditTargets.clear();
|
|
1474
|
+
}
|
|
1098
1475
|
/**
|
|
1099
1476
|
* Show which files depend on a symbol before the agent edits it.
|
|
1100
1477
|
* Uses LanguageService.findReferences(). Cost: ~20ms per call.
|
|
@@ -1366,9 +1743,11 @@ export class NrekiKernel {
|
|
|
1366
1743
|
rollbacks.push({ filePath: edit.filePath, content: originalContent });
|
|
1367
1744
|
}
|
|
1368
1745
|
if (rollbacks.length > 0) {
|
|
1369
|
-
// Fire-and-forget: don't block the error response
|
|
1746
|
+
// Fire-and-forget: don't block the error response.
|
|
1747
|
+
// If rollback fails, mark sidecar dead to force respawn on next use.
|
|
1748
|
+
// This prevents brain-split where sidecar VFS disagrees with kernel VFS.
|
|
1370
1749
|
sidecar.validateEdits(rollbacks).catch(() => {
|
|
1371
|
-
|
|
1750
|
+
sidecar.isDead = true;
|
|
1372
1751
|
});
|
|
1373
1752
|
}
|
|
1374
1753
|
}
|