@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +805 -774
  2. package/README.md +308 -442
  3. package/dist/ast-sandbox.d.ts.map +1 -1
  4. package/dist/ast-sandbox.js +17 -1
  5. package/dist/ast-sandbox.js.map +1 -1
  6. package/dist/audit.d.ts.map +1 -1
  7. package/dist/audit.js +10 -4
  8. package/dist/audit.js.map +1 -1
  9. package/dist/chronos-memory.d.ts.map +1 -1
  10. package/dist/chronos-memory.js +10 -2
  11. package/dist/chronos-memory.js.map +1 -1
  12. package/dist/compressor.d.ts.map +1 -1
  13. package/dist/compressor.js +13 -1
  14. package/dist/compressor.js.map +1 -1
  15. package/dist/database.d.ts +12 -1
  16. package/dist/database.d.ts.map +1 -1
  17. package/dist/database.js +81 -29
  18. package/dist/database.js.map +1 -1
  19. package/dist/embedder.d.ts.map +1 -1
  20. package/dist/embedder.js +7 -2
  21. package/dist/embedder.js.map +1 -1
  22. package/dist/handlers/code.d.ts.map +1 -1
  23. package/dist/handlers/code.js +198 -243
  24. package/dist/handlers/code.js.map +1 -1
  25. package/dist/handlers/guard.d.ts.map +1 -1
  26. package/dist/handlers/guard.js +10 -1
  27. package/dist/handlers/guard.js.map +1 -1
  28. package/dist/hologram/shadow-generator.d.ts.map +1 -1
  29. package/dist/hologram/shadow-generator.js +20 -1
  30. package/dist/hologram/shadow-generator.js.map +1 -1
  31. package/dist/kernel/backends/lsp-sidecar-base.d.ts +49 -5
  32. package/dist/kernel/backends/lsp-sidecar-base.d.ts.map +1 -1
  33. package/dist/kernel/backends/lsp-sidecar-base.js +209 -69
  34. package/dist/kernel/backends/lsp-sidecar-base.js.map +1 -1
  35. package/dist/kernel/backends/ts-compiler-wrapper.d.ts +1 -1
  36. package/dist/kernel/backends/ts-compiler-wrapper.d.ts.map +1 -1
  37. package/dist/kernel/backends/ts-compiler-wrapper.js +7 -4
  38. package/dist/kernel/backends/ts-compiler-wrapper.js.map +1 -1
  39. package/dist/kernel/backends/ts-corsa-sidecar.d.ts +26 -0
  40. package/dist/kernel/backends/ts-corsa-sidecar.d.ts.map +1 -0
  41. package/dist/kernel/backends/ts-corsa-sidecar.js +30 -0
  42. package/dist/kernel/backends/ts-corsa-sidecar.js.map +1 -0
  43. package/dist/kernel/nreki-kernel.d.ts +37 -0
  44. package/dist/kernel/nreki-kernel.d.ts.map +1 -1
  45. package/dist/kernel/nreki-kernel.js +669 -290
  46. package/dist/kernel/nreki-kernel.js.map +1 -1
  47. package/dist/kernel/spectral-topology.d.ts.map +1 -1
  48. package/dist/kernel/spectral-topology.js +32 -16
  49. package/dist/kernel/spectral-topology.js.map +1 -1
  50. package/dist/middleware/circuit-breaker.d.ts.map +1 -1
  51. package/dist/middleware/circuit-breaker.js +18 -2
  52. package/dist/middleware/circuit-breaker.js.map +1 -1
  53. package/dist/middleware/file-lock.d.ts.map +1 -1
  54. package/dist/middleware/file-lock.js +8 -3
  55. package/dist/middleware/file-lock.js.map +1 -1
  56. package/dist/monitor.d.ts.map +1 -1
  57. package/dist/monitor.js +1 -0
  58. package/dist/monitor.js.map +1 -1
  59. package/dist/parser.d.ts.map +1 -1
  60. package/dist/parser.js +19 -2
  61. package/dist/parser.js.map +1 -1
  62. package/dist/pin-memory.d.ts +2 -2
  63. package/dist/pin-memory.d.ts.map +1 -1
  64. package/dist/pin-memory.js.map +1 -1
  65. package/dist/repo-map.d.ts.map +1 -1
  66. package/dist/repo-map.js +26 -0
  67. package/dist/repo-map.js.map +1 -1
  68. package/dist/router.d.ts.map +1 -1
  69. package/dist/router.js +58 -18
  70. package/dist/router.js.map +1 -1
  71. package/dist/undo.js +1 -1
  72. package/dist/undo.js.map +1 -1
  73. package/dist/utils/imports.d.ts.map +1 -1
  74. package/dist/utils/imports.js +8 -4
  75. package/dist/utils/imports.js.map +1 -1
  76. package/dist/utils/latency-tracker.d.ts +22 -0
  77. package/dist/utils/latency-tracker.d.ts.map +1 -0
  78. package/dist/utils/latency-tracker.js +49 -0
  79. package/dist/utils/latency-tracker.js.map +1 -0
  80. package/dist/utils/logger.d.ts +5 -2
  81. package/dist/utils/logger.d.ts.map +1 -1
  82. package/dist/utils/logger.js +29 -6
  83. package/dist/utils/logger.js.map +1 -1
  84. package/dist/utils/path-jail.d.ts.map +1 -1
  85. package/dist/utils/path-jail.js +3 -0
  86. package/dist/utils/path-jail.js.map +1 -1
  87. 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 Promise.resolve().then(fn);
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 ? ts.sys.getModifiedTime(fileName) : new Date();
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: Clean orphaned transaction backups from previous crashes
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 t0 = performance.now();
585
- const rollbackState = new Map();
586
- // B6: Save logicalTime for rollback
587
- const savedLogicalTime = this.logicalTime;
588
- this.logicalTime += 1000;
589
- const explicitlyEditedFiles = new Set();
590
- // A-02: Snapshot vfsDirectories for rollback
591
- const savedDirectories = new Set(this.vfsDirectories);
592
- // HOLOGRAM: set currentEditTargets so VFS hooks show them as real .ts
593
- if (this.mode === "hologram") {
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
- this.currentEditTargets.add(posixPath);
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
- // TTRD PRE-SCAN: Identify files that will be edited (before VFS mutation)
606
- for (const edit of edits) {
607
- if (edit.proposedContent !== null) {
608
- const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
609
- explicitlyEditedFiles.add(posixPath);
610
- }
611
- }
612
- // TOPOLOGICAL INJECTION: unveil dependents in hologram mode
613
- const filesToEvaluate = new Set(explicitlyEditedFiles);
614
- const temporarilyUnveiled = new Set();
615
- if (this.mode === "hologram") {
616
- for (const dep of dependents) {
617
- const posixDep = this.toPosix(path.resolve(this.projectRoot, dep));
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
- // JIT baseline: recapture baseline scoped to files we will evaluate
630
- if (this.mode === "hologram" || this.mode === "file") {
631
- this.tsBackend.updateProgram();
632
- // DR/LS synced via tsBackend
633
- this.tsBackend.captureBaseline(filesToEvaluate, this.mode);
634
- if (this.bootErrorCount === -1) {
635
- this.bootErrorCount = this.getBaselineErrorCount();
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
- // TTRD: Extract pre-mutation type contracts (before VFS injection)
639
- const preContracts = await this.tsBackend.extractCanonicalTypes(explicitlyEditedFiles);
640
- // A-01: Wrap Phase 1-4 so partial VFS mutations are rolled back on throw
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
- const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
647
- const rootPosix = this.toPosix(path.resolve(this.projectRoot));
648
- // A1: Path Jail - block traversal attempts at kernel level
649
- if (!posixPath.startsWith(rootPosix + "/") && posixPath !== rootPosix) {
650
- throw new Error(`[NREKI] Security rejection: Path traversal blocked. ` +
651
- `"${edit.targetFile}" resolves outside project root.`);
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
- else {
671
- explicitlyEditedFiles.add(posixPath);
672
- if (!currentlyInRoots && this.isTypeScriptFile(posixPath)) {
673
- this.rootNames.add(posixPath);
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
- // E1: Build directory hierarchy for O(1) lookup
676
- let dir = path.posix.dirname(posixPath);
677
- while (dir.length >= rootPosix.length && dir !== rootPosix && dir !== ".") {
678
- this.vfsDirectories.add(dir);
679
- dir = path.posix.dirname(dir);
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
- // PHASE 2: Rebuild incremental program
684
- this.tsBackend.updateProgram();
685
- // DR/LS synced via tsBackend
686
- // BUG 3 FIXED: Compute AI errors exactly ONCE.
687
- // getFatalErrors consumes the stateful getSemanticDiagnosticsOfNextAffectedFile iterator.
688
- // Cannot call it again without updateProgram() in between.
689
- const originalFatalErrors = await this.tsBackend.getDiagnostics(filesToEvaluate, explicitlyEditedFiles, this.mode);
690
- // ─── Phase 2.5: LSP Sidecar Validation (Go, Python) ─────────
691
- const sidecarWarnings = [];
692
- // Collect non-TS edits grouped by sidecar
693
- sidecarEdits.clear();
694
- for (const edit of edits) {
695
- const posixPath = this.toPosix(path.resolve(this.projectRoot, edit.targetFile));
696
- const ext = path.extname(posixPath).toLowerCase();
697
- const sidecar = this.lspSidecars.get(ext);
698
- if (!sidecar)
699
- continue;
700
- const list = sidecarEdits.get(sidecar) || [];
701
- list.push({ filePath: posixPath, content: edit.proposedContent });
702
- sidecarEdits.set(sidecar, list);
703
- }
704
- // Boot + validate each sidecar
705
- for (const [sidecar, scEdits] of sidecarEdits) {
706
- if (!sidecar.isHealthy()) {
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.boot();
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: '${sidecar.command[0]}' not found or crashed. ` +
712
- `Layer 2 Semantic Shield OFFLINE for ${sidecar.languageId} files. ` +
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
- try {
718
- const sidecarErrors = await sidecar.validateEdits(scEdits);
719
- // ACID: Merge sidecar errors. If ANY backend rejects, NOTHING touches disk.
720
- originalFatalErrors.push(...sidecarErrors);
721
- }
722
- catch {
723
- sidecarWarnings.push(`[⚠️ NREKI WARNING: ${sidecar.languageId} validation timed out. ` +
724
- `Code passed without semantic check.]`);
725
- }
726
- }
727
- // ─── End Phase 2.5 ───────────────────────────────────────────
728
- // PHASE 4: Verdict
729
- if (originalFatalErrors.length > 0) {
730
- // ─── NREKI L3.3: Iterative Auto-Healing ─────────────────
731
- // Only attempt auto-healing on TypeScript errors.
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
- if (healing.healed) {
745
- const latency = (performance.now() - t0).toFixed(2);
746
- const healLatency = (performance.now() - tHealStart).toFixed(2);
747
- // Patch notice: tell the agent what NOT to overwrite in its next edit
748
- let extraFilesWarning = "";
749
- if (healing.newlyTouchedFiles.size > 0) {
750
- const files = Array.from(healing.newlyTouchedFiles)
751
- .map(f => path.relative(this.projectRoot, f)).join(", ");
752
- extraFilesWarning = `\nWARNING: NREKI also auto-patched collateral files: ${files}. Do NOT revert them.`;
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
- const patchNotice = `\n\nIMPORTANT: NREKI applied these patches automatically.\n` +
755
- `Your code on disk already contains these fixes. ` +
756
- `Do not revert or overwrite them in your next edit.` +
757
- extraFilesWarning;
758
- // TTRD post-contracts (healed path)
759
- const finalEditedFiles = new Set(explicitlyEditedFiles);
760
- for (const f of healing.newlyTouchedFiles)
761
- finalEditedFiles.add(f);
762
- const postContracts = await this.tsBackend.extractCanonicalTypes(finalEditedFiles);
763
- const regressions = this.tsBackend.computeTypeRegressions(preContracts, postContracts);
764
- // Restore hologram veil after healed success
765
- if (this.mode === "hologram" && temporarilyUnveiled.size > 0) {
766
- for (const file of temporarilyUnveiled) {
767
- this.prunedTsLookup.add(file);
768
- this.shadowDtsLookup.add(file.replace(/\.([mc]?)tsx?$/, ".d.$1ts"));
769
- this.rootNames.delete(file);
770
- this.vfsClock.set(file, new Date(this.logicalTime + 1));
771
- // JIT: force re-classify on next access (content may have changed)
772
- this.jitClassifiedCache.delete(file);
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
- this.currentEditTargets.clear();
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: true,
778
- exitCode: 0,
779
- latencyMs: latency,
780
- healedFiles: Array.from(healing.newlyTouchedFiles),
781
- errorText: `[NREKI AUTO-HEAL: ${healLatency}ms] ` +
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
- // ─── END Auto-Healing ────────────────────────────────────
793
- // Healing failed. Use the ORIGINAL error matrix (do not recalculate).
794
- const structured = originalFatalErrors; // Already NrekiStructuredError[]
795
- // ACID rollback of the original edit
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 heal sidecar VFS
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
- // B6: Restore logicalTime on rollback
815
- this.logicalTime = savedLogicalTime;
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
- // P17 + P2 WARM-PATH: Advance clock instead of destroying program.
819
- this.logicalTime += 1000;
820
- for (const [posixPath] of rollbackState.entries()) {
821
- this.vfsClock.set(posixPath, new Date(this.logicalTime));
822
- }
823
- this.tsBackend.updateProgram();
824
- // DR/LS synced via tsBackend
825
- const latency = (performance.now() - t0).toFixed(2);
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
- catch (phaseError) {
873
- // A-01: Rollback partial VFS mutations from Phase 1 on any throw
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 this.vfs.keys()) {
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 [posixPath, content] of this.vfs.entries()) {
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
- // Best effort — sidecar may be dead
1750
+ sidecar.isDead = true;
1372
1751
  });
1373
1752
  }
1374
1753
  }