sela-core 1.0.6 → 1.0.8
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/README.md +1 -1
- package/dist/cli/ErrorHandler.d.ts +1 -1
- package/dist/cli/ErrorHandler.js +14 -14
- package/dist/cli/commands/bulk.d.ts +1 -1
- package/dist/cli/commands/bulk.d.ts.map +1 -1
- package/dist/cli/commands/bulk.js +51 -33
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +26 -18
- package/dist/cli/commands/showReport.d.ts +1 -1
- package/dist/cli/commands/showReport.d.ts.map +1 -1
- package/dist/cli/commands/showReport.js +12 -10
- package/dist/cli/commands/status.d.ts +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +8 -8
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +38 -30
- package/dist/cli/index.js +9 -9
- package/dist/cli/ui/DnaTable.d.ts +1 -1
- package/dist/cli/ui/DnaTable.d.ts.map +1 -1
- package/dist/cli/ui/DnaTable.js +23 -16
- package/dist/cli/ui/RefactorWizard.d.ts +3 -3
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
- package/dist/cli/ui/RefactorWizard.js +108 -82
- package/dist/config/ConfigLoader.d.ts +3 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +23 -13
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +79 -0
- package/dist/config/SelaConfig.d.ts +26 -16
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +54 -22
- package/dist/engine/HealingRegistry.d.ts +1 -1
- package/dist/engine/HealingRegistry.js +3 -3
- package/dist/engine/SelaEngine.d.ts +13 -10
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +292 -101
- package/dist/errors/SelaError.d.ts +132 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +154 -0
- package/dist/fixtures/expectProxy.d.ts +1 -1
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +17 -10
- package/dist/fixtures/index.d.ts +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +26 -11
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/reporter/SelaReporter.d.ts +143 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +428 -0
- package/dist/services/ASTSourceUpdater.d.ts +20 -1
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +257 -89
- package/dist/services/ArgumentTypeAnalyzer.js +7 -7
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
- package/dist/services/BlastRadiusAnalyzer.js +5 -2
- package/dist/services/ChainValidator.js +22 -22
- package/dist/services/CrossFileHealer.js +7 -7
- package/dist/services/DefinitionTracer.d.ts +1 -1
- package/dist/services/DefinitionTracer.d.ts.map +1 -1
- package/dist/services/DefinitionTracer.js +28 -13
- package/dist/services/DnaEditorService.d.ts +2 -2
- package/dist/services/DnaEditorService.d.ts.map +1 -1
- package/dist/services/DnaEditorService.js +81 -62
- package/dist/services/DnaIndexService.d.ts +1 -1
- package/dist/services/DnaIndexService.d.ts.map +1 -1
- package/dist/services/DnaIndexService.js +14 -14
- package/dist/services/HealReportService.d.ts +43 -1
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +108 -25
- package/dist/services/HealingAdvisory.js +1 -1
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +460 -0
- package/dist/services/HealthReportService.d.ts +1 -1
- package/dist/services/HealthReportService.d.ts.map +1 -1
- package/dist/services/HealthReportService.js +25 -25
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +20 -1
- package/dist/services/IntentAuditor.d.ts +18 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +50 -26
- package/dist/services/InteractiveReview.d.ts +15 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +125 -0
- package/dist/services/LLMService.d.ts +18 -2
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +33 -23
- package/dist/services/PRAutomationService.d.ts +14 -1
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +287 -92
- package/dist/services/PendingPromptLedger.d.ts +44 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +180 -0
- package/dist/services/ReportGenerator.d.ts +70 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +191 -0
- package/dist/services/SafetyGuard.d.ts +30 -7
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +85 -116
- package/dist/services/SnapshotService.js +5 -5
- package/dist/services/SourceLinkService.d.ts +2 -2
- package/dist/services/SourceLinkService.d.ts.map +1 -1
- package/dist/services/SourceLinkService.js +31 -22
- package/dist/services/SourceUpdater.d.ts +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +66 -34
- package/dist/services/TemplateDiffService.d.ts.map +1 -1
- package/dist/services/TemplateDiffService.js +18 -15
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +200 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +382 -0
- package/package.json +71 -67
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/WorkspaceSnapshotService.ts
|
|
3
|
+
//
|
|
4
|
+
// Clean-Room Architecture - file 1 of 2.
|
|
5
|
+
//
|
|
6
|
+
// Tracks the EXACT content of every file Sela mutates during a session, so:
|
|
7
|
+
// • The HTML report diff can be generated from an isolated in-memory diff
|
|
8
|
+
// (developer's dirty pre-existing edits NEVER bleed in).
|
|
9
|
+
// • PRAutomationService can restore the developer's pre-Sela workspace
|
|
10
|
+
// byte-for-byte after a PR is opened on an isolated branch.
|
|
11
|
+
//
|
|
12
|
+
// Lifecycle:
|
|
13
|
+
// preWrite(path) - first call per file records `contentBefore` (disk
|
|
14
|
+
// snapshot right before Sela touches it) + HEAD blob.
|
|
15
|
+
// postWrite(path) - refreshes `contentAfter` (latest disk state).
|
|
16
|
+
// restoreOriginal()- writes contentBefore back to every tracked file.
|
|
17
|
+
// restoreMutated() - writes contentAfter back to every tracked file.
|
|
18
|
+
// clear() - empties the ledger.
|
|
19
|
+
//
|
|
20
|
+
// Singleton `sharedWorkspaceSnapshot` is the canonical instance the
|
|
21
|
+
// SourceUpdater / ASTSourceUpdater write hooks talk to.
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.sharedWorkspaceSnapshot = exports.WorkspaceSnapshotService = void 0;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
const child_process_1 = require("child_process");
|
|
60
|
+
class WorkspaceSnapshotService {
|
|
61
|
+
snapshots = new Map();
|
|
62
|
+
cwd;
|
|
63
|
+
constructor(cwd = process.cwd()) {
|
|
64
|
+
this.cwd = cwd;
|
|
65
|
+
}
|
|
66
|
+
/** Override the cwd used for relative-path + HEAD lookups (tests). */
|
|
67
|
+
setCwd(cwd) {
|
|
68
|
+
this.cwd = cwd;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Record the pre-mutation state of `filePath` if not already tracked.
|
|
72
|
+
* Idempotent - only the FIRST call per file matters.
|
|
73
|
+
*/
|
|
74
|
+
preWrite(filePath) {
|
|
75
|
+
const abs = this.normalise(filePath);
|
|
76
|
+
if (this.snapshots.has(abs))
|
|
77
|
+
return;
|
|
78
|
+
const exists = fs.existsSync(abs);
|
|
79
|
+
const contentBefore = exists ? fs.readFileSync(abs, "utf8") : "";
|
|
80
|
+
const headContent = this.readHeadContent(abs);
|
|
81
|
+
const hadUncommittedChanges = headContent !== null && headContent !== contentBefore;
|
|
82
|
+
this.snapshots.set(abs, {
|
|
83
|
+
absolutePath: abs,
|
|
84
|
+
relativePath: this.toRelative(abs),
|
|
85
|
+
contentBefore,
|
|
86
|
+
contentAfter: null,
|
|
87
|
+
headContent,
|
|
88
|
+
hadUncommittedChanges,
|
|
89
|
+
createdBySela: !exists,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Refresh the post-mutation state. Safe to call multiple times - each
|
|
94
|
+
* call overwrites `contentAfter` with the latest disk state.
|
|
95
|
+
*/
|
|
96
|
+
postWrite(filePath) {
|
|
97
|
+
const abs = this.normalise(filePath);
|
|
98
|
+
const snap = this.snapshots.get(abs);
|
|
99
|
+
if (!snap)
|
|
100
|
+
return;
|
|
101
|
+
snap.contentAfter = fs.existsSync(abs) ? fs.readFileSync(abs, "utf8") : "";
|
|
102
|
+
}
|
|
103
|
+
/** Lookup by absolute or relative path. Returns `null` when untracked. */
|
|
104
|
+
getSnapshot(filePath) {
|
|
105
|
+
return this.snapshots.get(this.normalise(filePath)) ?? null;
|
|
106
|
+
}
|
|
107
|
+
/** Every tracked snapshot. Insertion order. */
|
|
108
|
+
getAllSnapshots() {
|
|
109
|
+
return Array.from(this.snapshots.values());
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Did Sela actually mutate this file? True when contentAfter exists and
|
|
113
|
+
* differs from contentBefore.
|
|
114
|
+
*/
|
|
115
|
+
isMutated(filePath) {
|
|
116
|
+
const snap = this.getSnapshot(filePath);
|
|
117
|
+
if (!snap || snap.contentAfter === null)
|
|
118
|
+
return false;
|
|
119
|
+
return snap.contentAfter !== snap.contentBefore;
|
|
120
|
+
}
|
|
121
|
+
/** All snapshots with a real mutation recorded. */
|
|
122
|
+
getMutatedSnapshots() {
|
|
123
|
+
return this.getAllSnapshots().filter((s) => s.contentAfter !== null && s.contentAfter !== s.contentBefore);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Rewind every tracked file on disk to `contentBefore` - restoring the
|
|
127
|
+
* developer's pre-Sela workspace state byte-for-byte.
|
|
128
|
+
*/
|
|
129
|
+
restoreOriginal() {
|
|
130
|
+
for (const snap of this.snapshots.values()) {
|
|
131
|
+
this.rewindSnapshot(snap);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Rewind a SINGLE tracked file on disk to `contentBefore`. Used by the
|
|
136
|
+
* Local DX interactive review flow when the developer rejects a fix.
|
|
137
|
+
* Returns true when the rewind happened, false when the file is untracked.
|
|
138
|
+
*/
|
|
139
|
+
restoreOriginalByPath(filePath) {
|
|
140
|
+
const snap = this.getSnapshot(filePath);
|
|
141
|
+
if (!snap)
|
|
142
|
+
return false;
|
|
143
|
+
this.rewindSnapshot(snap);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
rewindSnapshot(snap) {
|
|
147
|
+
if (snap.createdBySela) {
|
|
148
|
+
if (fs.existsSync(snap.absolutePath)) {
|
|
149
|
+
fs.unlinkSync(snap.absolutePath);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
fs.writeFileSync(snap.absolutePath, snap.contentBefore, "utf8");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Forward every tracked file on disk to `contentAfter` (no-op for files
|
|
157
|
+
* that were never written). Used by PRAutomationService to reapply
|
|
158
|
+
* Sela's mutations after a `git stash pop` round-trip.
|
|
159
|
+
*/
|
|
160
|
+
restoreMutated() {
|
|
161
|
+
for (const snap of this.snapshots.values()) {
|
|
162
|
+
if (snap.contentAfter === null)
|
|
163
|
+
continue;
|
|
164
|
+
fs.writeFileSync(snap.absolutePath, snap.contentAfter, "utf8");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
clear() {
|
|
168
|
+
this.snapshots.clear();
|
|
169
|
+
}
|
|
170
|
+
// ────────────────────────────────────────────────────────────────
|
|
171
|
+
// INTERNAL
|
|
172
|
+
// ────────────────────────────────────────────────────────────────
|
|
173
|
+
normalise(raw) {
|
|
174
|
+
if (!raw)
|
|
175
|
+
return raw;
|
|
176
|
+
const abs = path.isAbsolute(raw) ? raw : path.resolve(this.cwd, raw);
|
|
177
|
+
return path.normalize(abs);
|
|
178
|
+
}
|
|
179
|
+
toRelative(abs) {
|
|
180
|
+
const rel = path.relative(this.cwd, abs);
|
|
181
|
+
return rel.split(path.sep).join("/");
|
|
182
|
+
}
|
|
183
|
+
readHeadContent(abs) {
|
|
184
|
+
try {
|
|
185
|
+
const rel = this.toRelative(abs);
|
|
186
|
+
const out = (0, child_process_1.execSync)(`git show HEAD:"${rel}"`, {
|
|
187
|
+
cwd: this.cwd,
|
|
188
|
+
encoding: "utf8",
|
|
189
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
190
|
+
windowsHide: true,
|
|
191
|
+
});
|
|
192
|
+
return out.toString();
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.WorkspaceSnapshotService = WorkspaceSnapshotService;
|
|
200
|
+
exports.sharedWorkspaceSnapshot = new WorkspaceSnapshotService();
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface AncestorNode {
|
|
|
2
2
|
tag: string;
|
|
3
3
|
id: string | null;
|
|
4
4
|
classes: string[];
|
|
5
|
-
/** "{tag}#{id}[{classes_sorted_asc}]"
|
|
5
|
+
/** "{tag}#{id}[{classes_sorted_asc}]" - stable fingerprint for drift comparison */
|
|
6
6
|
fingerprint: string;
|
|
7
7
|
}
|
|
8
8
|
export type GhostCondition = "G1_DISPLAY_NONE" | "G2_VISIBILITY_HIDDEN" | "G3_OPACITY_ZERO" | "G4_POINTER_EVENTS_NONE" | "G5_ZERO_AREA" | "G6_CONTENT_VISIBILITY_HIDDEN";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface IsolatedDiffOptions {
|
|
2
|
+
/** Lines of unchanged context around each hunk. Default 3 (git default). */
|
|
3
|
+
context?: number;
|
|
4
|
+
/**
|
|
5
|
+
* Path to render in the `--- a/<path>` / `+++ b/<path>` preamble. When
|
|
6
|
+
* omitted the preamble is dropped entirely (hunks only).
|
|
7
|
+
*/
|
|
8
|
+
filePath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generate a `git`-style unified diff between two in-memory strings.
|
|
12
|
+
* Returns `""` when the two buffers are identical.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateUnifiedDiff(before: string, after: string, opts?: IsolatedDiffOptions): string;
|
|
15
|
+
export interface ChangeBlock {
|
|
16
|
+
/** Old text (newlines preserved). Empty string for pure additions. */
|
|
17
|
+
oldBlock: string;
|
|
18
|
+
/** New text (newlines preserved). Empty string for pure deletions. */
|
|
19
|
+
newBlock: string;
|
|
20
|
+
/** 1-based line in `before` where the block starts (0 if pure add). */
|
|
21
|
+
oldLineStart: number;
|
|
22
|
+
/** 1-based line in `after` where the block starts (0 if pure delete). */
|
|
23
|
+
newLineStart: number;
|
|
24
|
+
/**
|
|
25
|
+
* For pure additions: a non-empty line of unchanged anchor text immediately
|
|
26
|
+
* PRECEDING the new block in the `before` buffer. The replayer uses this
|
|
27
|
+
* to locate the insertion point. `null` when adding at the very top.
|
|
28
|
+
*/
|
|
29
|
+
anchorBefore: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function extractChangeBlocks(before: string, after: string): ChangeBlock[];
|
|
32
|
+
export interface ApplyBlocksResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
result?: string;
|
|
35
|
+
/** Human-readable failure reason - surfaced in the PR-automation log. */
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Replay a list of change blocks against a fresh base buffer (typically
|
|
40
|
+
* the file's `HEAD` blob). Each block is applied via UNIQUE substring
|
|
41
|
+
* replacement - when the old block is missing or ambiguous in the base,
|
|
42
|
+
* we fail loudly rather than silently mutating the wrong spot.
|
|
43
|
+
*/
|
|
44
|
+
export declare function applyChangeBlocks(base: string, blocks: ChangeBlock[]): ApplyBlocksResult;
|
|
45
|
+
//# sourceMappingURL=IsolatedDiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IsolatedDiff.d.ts","sourceRoot":"","sources":["../../src/utils/IsolatedDiff.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,mBAAwB,GAC7B,MAAM,CAsBR;AA6PD,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,WAAW,EAAE,CAoDf;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EAAE,GACpB,iBAAiB,CAuDnB"}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/utils/IsolatedDiff.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure-JS unified-diff generator. No external `diff` package, no shell-out to
|
|
5
|
+
// `git diff` - the result is guaranteed to be a function ONLY of the two
|
|
6
|
+
// in-memory buffers passed in, never of any global working-tree state.
|
|
7
|
+
//
|
|
8
|
+
// This is the foundation of the Clean-Room Architecture: the HTML report
|
|
9
|
+
// renders diffs computed here, so a developer's pre-existing uncommitted
|
|
10
|
+
// edits CANNOT leak into Sela's report.
|
|
11
|
+
//
|
|
12
|
+
// Algorithm: classic LCS over lines, then a unified-diff emitter with
|
|
13
|
+
// configurable context. Memory is O(N*M) which is fine for the spec files
|
|
14
|
+
// Sela mutates (typically a few thousand lines max).
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.generateUnifiedDiff = generateUnifiedDiff;
|
|
17
|
+
exports.extractChangeBlocks = extractChangeBlocks;
|
|
18
|
+
exports.applyChangeBlocks = applyChangeBlocks;
|
|
19
|
+
/**
|
|
20
|
+
* Generate a `git`-style unified diff between two in-memory strings.
|
|
21
|
+
* Returns `""` when the two buffers are identical.
|
|
22
|
+
*/
|
|
23
|
+
function generateUnifiedDiff(before, after, opts = {}) {
|
|
24
|
+
if (before === after)
|
|
25
|
+
return "";
|
|
26
|
+
const context = Math.max(0, opts.context ?? 3);
|
|
27
|
+
const a = splitKeepEol(before);
|
|
28
|
+
const b = splitKeepEol(after);
|
|
29
|
+
const ops = diffLines(a, b);
|
|
30
|
+
if (ops.length === 0)
|
|
31
|
+
return "";
|
|
32
|
+
const hunks = groupHunks(ops, { a, b, context });
|
|
33
|
+
if (hunks.length === 0)
|
|
34
|
+
return "";
|
|
35
|
+
const out = [];
|
|
36
|
+
if (opts.filePath) {
|
|
37
|
+
const p = opts.filePath.split(/[\\/]/).join("/");
|
|
38
|
+
out.push(`diff --git a/${p} b/${p}`);
|
|
39
|
+
out.push(`--- a/${p}`);
|
|
40
|
+
out.push(`+++ b/${p}`);
|
|
41
|
+
}
|
|
42
|
+
for (const hunk of hunks) {
|
|
43
|
+
out.push(formatHunkHeader(hunk));
|
|
44
|
+
for (const row of hunk.rows)
|
|
45
|
+
out.push(row);
|
|
46
|
+
}
|
|
47
|
+
return out.join("\n") + "\n";
|
|
48
|
+
}
|
|
49
|
+
// ────────────────────────────────────────────────────────────────────
|
|
50
|
+
// LINE SPLITTER - preserves trailing newline / CRLF so we can emit the
|
|
51
|
+
// canonical `` marker when needed.
|
|
52
|
+
// ────────────────────────────────────────────────────────────────────
|
|
53
|
+
function splitKeepEol(text) {
|
|
54
|
+
if (text.length === 0)
|
|
55
|
+
return [];
|
|
56
|
+
const out = [];
|
|
57
|
+
let start = 0;
|
|
58
|
+
for (let i = 0; i < text.length; i++) {
|
|
59
|
+
if (text.charCodeAt(i) === 10 /* \n */) {
|
|
60
|
+
out.push(text.slice(start, i + 1));
|
|
61
|
+
start = i + 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (start < text.length)
|
|
65
|
+
out.push(text.slice(start));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function stripEol(line) {
|
|
69
|
+
if (line.endsWith("\r\n"))
|
|
70
|
+
return line.slice(0, -2);
|
|
71
|
+
if (line.endsWith("\n"))
|
|
72
|
+
return line.slice(0, -1);
|
|
73
|
+
return line;
|
|
74
|
+
}
|
|
75
|
+
function hasEol(line) {
|
|
76
|
+
return line.endsWith("\n");
|
|
77
|
+
}
|
|
78
|
+
function diffLines(a, b) {
|
|
79
|
+
const m = a.length;
|
|
80
|
+
const n = b.length;
|
|
81
|
+
const w = n + 1;
|
|
82
|
+
const dp = new Uint32Array((m + 1) * w);
|
|
83
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
84
|
+
const ai = a[i];
|
|
85
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
86
|
+
if (ai === b[j]) {
|
|
87
|
+
dp[i * w + j] = dp[(i + 1) * w + (j + 1)] + 1;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const down = dp[(i + 1) * w + j];
|
|
91
|
+
const right = dp[i * w + (j + 1)];
|
|
92
|
+
dp[i * w + j] = down >= right ? down : right;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const ops = [];
|
|
97
|
+
let i = 0;
|
|
98
|
+
let j = 0;
|
|
99
|
+
while (i < m && j < n) {
|
|
100
|
+
if (a[i] === b[j]) {
|
|
101
|
+
ops.push({ kind: "eq", a: i, b: j });
|
|
102
|
+
i++;
|
|
103
|
+
j++;
|
|
104
|
+
}
|
|
105
|
+
else if (dp[(i + 1) * w + j] >= dp[i * w + (j + 1)]) {
|
|
106
|
+
ops.push({ kind: "del", a: i });
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
ops.push({ kind: "add", b: j });
|
|
111
|
+
j++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
while (i < m) {
|
|
115
|
+
ops.push({ kind: "del", a: i });
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
while (j < n) {
|
|
119
|
+
ops.push({ kind: "add", b: j });
|
|
120
|
+
j++;
|
|
121
|
+
}
|
|
122
|
+
return ops;
|
|
123
|
+
}
|
|
124
|
+
function groupHunks(ops, ctx) {
|
|
125
|
+
const hunks = [];
|
|
126
|
+
const ctxN = ctx.context;
|
|
127
|
+
let i = 0;
|
|
128
|
+
while (i < ops.length) {
|
|
129
|
+
// Locate the next non-equal op.
|
|
130
|
+
let firstChange = -1;
|
|
131
|
+
for (let k = i; k < ops.length; k++) {
|
|
132
|
+
if (ops[k].kind !== "eq") {
|
|
133
|
+
firstChange = k;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (firstChange === -1)
|
|
138
|
+
break;
|
|
139
|
+
// Leading-context start, bounded by previous hunk end.
|
|
140
|
+
let hunkStart = firstChange - ctxN;
|
|
141
|
+
if (hunkStart < i)
|
|
142
|
+
hunkStart = i;
|
|
143
|
+
if (hunkStart < 0)
|
|
144
|
+
hunkStart = 0;
|
|
145
|
+
let hunkEnd = firstChange;
|
|
146
|
+
// Extend by merging change groups separated by <= 2*ctxN equality lines.
|
|
147
|
+
// Avoid merging across the very end of `ops`.
|
|
148
|
+
while (true) {
|
|
149
|
+
let cursor = hunkEnd + 1;
|
|
150
|
+
let runEqs = 0;
|
|
151
|
+
let nextChange = -1;
|
|
152
|
+
while (cursor < ops.length) {
|
|
153
|
+
if (ops[cursor].kind === "eq") {
|
|
154
|
+
runEqs++;
|
|
155
|
+
cursor++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
nextChange = cursor;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (nextChange !== -1 && runEqs <= 2 * ctxN) {
|
|
162
|
+
hunkEnd = nextChange;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Trailing context.
|
|
169
|
+
let trailing = 0;
|
|
170
|
+
while (hunkEnd + 1 < ops.length &&
|
|
171
|
+
ops[hunkEnd + 1].kind === "eq" &&
|
|
172
|
+
trailing < ctxN) {
|
|
173
|
+
hunkEnd++;
|
|
174
|
+
trailing++;
|
|
175
|
+
}
|
|
176
|
+
// Materialise rows + compute header coordinates.
|
|
177
|
+
const rows = [];
|
|
178
|
+
let oldStartIdx = -1;
|
|
179
|
+
let newStartIdx = -1;
|
|
180
|
+
let oldLen = 0;
|
|
181
|
+
let newLen = 0;
|
|
182
|
+
for (let k = hunkStart; k <= hunkEnd; k++) {
|
|
183
|
+
const op = ops[k];
|
|
184
|
+
if (op.kind === "eq") {
|
|
185
|
+
if (oldStartIdx === -1)
|
|
186
|
+
oldStartIdx = op.a;
|
|
187
|
+
if (newStartIdx === -1)
|
|
188
|
+
newStartIdx = op.b;
|
|
189
|
+
const line = ctx.a[op.a];
|
|
190
|
+
rows.push(" " + stripEol(line));
|
|
191
|
+
oldLen++;
|
|
192
|
+
newLen++;
|
|
193
|
+
if (!hasEol(line))
|
|
194
|
+
rows.push("\");
|
|
195
|
+
}
|
|
196
|
+
else if (op.kind === "del") {
|
|
197
|
+
if (oldStartIdx === -1)
|
|
198
|
+
oldStartIdx = op.a;
|
|
199
|
+
if (newStartIdx === -1) {
|
|
200
|
+
// Anchor on the corresponding `b` position. For pure-deletions at
|
|
201
|
+
// the top of file `op.a === 0` ⇒ newStart should be 0 (special
|
|
202
|
+
// header form `+0,0`). For mid-file deletions use the running b
|
|
203
|
+
// cursor - derive it from prior ops.
|
|
204
|
+
newStartIdx = computeBAnchor(ops, k);
|
|
205
|
+
}
|
|
206
|
+
const line = ctx.a[op.a];
|
|
207
|
+
rows.push("-" + stripEol(line));
|
|
208
|
+
oldLen++;
|
|
209
|
+
if (!hasEol(line))
|
|
210
|
+
rows.push("\");
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
if (newStartIdx === -1)
|
|
214
|
+
newStartIdx = op.b;
|
|
215
|
+
if (oldStartIdx === -1)
|
|
216
|
+
oldStartIdx = computeAAnchor(ops, k);
|
|
217
|
+
const line = ctx.b[op.b];
|
|
218
|
+
rows.push("+" + stripEol(line));
|
|
219
|
+
newLen++;
|
|
220
|
+
if (!hasEol(line))
|
|
221
|
+
rows.push("\");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
hunks.push({
|
|
225
|
+
oldStart: oldLen === 0 ? oldStartIdx : oldStartIdx + 1,
|
|
226
|
+
oldLen,
|
|
227
|
+
newStart: newLen === 0 ? newStartIdx : newStartIdx + 1,
|
|
228
|
+
newLen,
|
|
229
|
+
rows,
|
|
230
|
+
});
|
|
231
|
+
i = hunkEnd + 1;
|
|
232
|
+
}
|
|
233
|
+
return hunks;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* For a pure-add op at position `k`, walk backwards to find the prior
|
|
237
|
+
* `a` index (so we know which line of the old file the hunk starts at).
|
|
238
|
+
*/
|
|
239
|
+
function computeAAnchor(ops, k) {
|
|
240
|
+
for (let p = k - 1; p >= 0; p--) {
|
|
241
|
+
const op = ops[p];
|
|
242
|
+
if (op.kind === "eq")
|
|
243
|
+
return op.a + 1;
|
|
244
|
+
if (op.kind === "del")
|
|
245
|
+
return op.a + 1;
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
function computeBAnchor(ops, k) {
|
|
250
|
+
for (let p = k - 1; p >= 0; p--) {
|
|
251
|
+
const op = ops[p];
|
|
252
|
+
if (op.kind === "eq")
|
|
253
|
+
return op.b + 1;
|
|
254
|
+
if (op.kind === "add")
|
|
255
|
+
return op.b + 1;
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
function formatHunkHeader(hunk) {
|
|
260
|
+
const oldPart = hunk.oldLen === 1 ? `${hunk.oldStart}` : `${hunk.oldStart},${hunk.oldLen}`;
|
|
261
|
+
const newPart = hunk.newLen === 1 ? `${hunk.newStart}` : `${hunk.newStart},${hunk.newLen}`;
|
|
262
|
+
return `@@ -${oldPart} +${newPart} @@`;
|
|
263
|
+
}
|
|
264
|
+
function extractChangeBlocks(before, after) {
|
|
265
|
+
if (before === after)
|
|
266
|
+
return [];
|
|
267
|
+
const a = splitKeepEol(before);
|
|
268
|
+
const b = splitKeepEol(after);
|
|
269
|
+
const ops = diffLines(a, b);
|
|
270
|
+
const blocks = [];
|
|
271
|
+
let i = 0;
|
|
272
|
+
while (i < ops.length) {
|
|
273
|
+
if (ops[i].kind === "eq") {
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const start = i;
|
|
278
|
+
while (i < ops.length && ops[i].kind !== "eq")
|
|
279
|
+
i++;
|
|
280
|
+
const oldLines = [];
|
|
281
|
+
const newLines = [];
|
|
282
|
+
let oldStart = -1;
|
|
283
|
+
let newStart = -1;
|
|
284
|
+
for (let k = start; k < i; k++) {
|
|
285
|
+
const op = ops[k];
|
|
286
|
+
if (op.kind === "del") {
|
|
287
|
+
if (oldStart === -1)
|
|
288
|
+
oldStart = op.a;
|
|
289
|
+
oldLines.push(a[op.a]);
|
|
290
|
+
}
|
|
291
|
+
else if (op.kind === "add") {
|
|
292
|
+
if (newStart === -1)
|
|
293
|
+
newStart = op.b;
|
|
294
|
+
newLines.push(b[op.b]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
let anchorBefore = null;
|
|
298
|
+
if (oldLines.length === 0) {
|
|
299
|
+
// Pure addition - find preceding unchanged anchor line.
|
|
300
|
+
for (let k = start - 1; k >= 0; k--) {
|
|
301
|
+
const op = ops[k];
|
|
302
|
+
if (op.kind === "eq") {
|
|
303
|
+
anchorBefore = a[op.a];
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
blocks.push({
|
|
309
|
+
oldBlock: oldLines.join(""),
|
|
310
|
+
newBlock: newLines.join(""),
|
|
311
|
+
oldLineStart: oldStart >= 0 ? oldStart + 1 : 0,
|
|
312
|
+
newLineStart: newStart >= 0 ? newStart + 1 : 0,
|
|
313
|
+
anchorBefore,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return blocks;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Replay a list of change blocks against a fresh base buffer (typically
|
|
320
|
+
* the file's `HEAD` blob). Each block is applied via UNIQUE substring
|
|
321
|
+
* replacement - when the old block is missing or ambiguous in the base,
|
|
322
|
+
* we fail loudly rather than silently mutating the wrong spot.
|
|
323
|
+
*/
|
|
324
|
+
function applyChangeBlocks(base, blocks) {
|
|
325
|
+
let working = base;
|
|
326
|
+
for (const block of blocks) {
|
|
327
|
+
if (block.oldBlock.length === 0) {
|
|
328
|
+
// Pure addition.
|
|
329
|
+
if (block.anchorBefore === null) {
|
|
330
|
+
if (working.length === 0) {
|
|
331
|
+
// Empty file - prepend.
|
|
332
|
+
working = block.newBlock + working;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Prepend at top of file.
|
|
336
|
+
working = block.newBlock + working;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const idx = working.indexOf(block.anchorBefore);
|
|
340
|
+
if (idx === -1) {
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
reason: `pure-add anchor not found in clean base: "${snippet(block.anchorBefore)}"`,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const lastIdx = working.lastIndexOf(block.anchorBefore);
|
|
347
|
+
if (idx !== lastIdx) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
reason: `pure-add anchor is not unique in clean base: "${snippet(block.anchorBefore)}"`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const insertAt = idx + block.anchorBefore.length;
|
|
354
|
+
working =
|
|
355
|
+
working.slice(0, insertAt) + block.newBlock + working.slice(insertAt);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const idx = working.indexOf(block.oldBlock);
|
|
359
|
+
if (idx === -1) {
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
reason: `block not found in clean base - developer's dirty state likely overlaps Sela's mutation site: "${snippet(block.oldBlock)}"`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const lastIdx = working.lastIndexOf(block.oldBlock);
|
|
366
|
+
if (idx !== lastIdx) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
reason: `block matches multiple locations in clean base - refusing to guess: "${snippet(block.oldBlock)}"`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
working =
|
|
373
|
+
working.slice(0, idx) +
|
|
374
|
+
block.newBlock +
|
|
375
|
+
working.slice(idx + block.oldBlock.length);
|
|
376
|
+
}
|
|
377
|
+
return { ok: true, result: working };
|
|
378
|
+
}
|
|
379
|
+
function snippet(s) {
|
|
380
|
+
const flat = s.replace(/\r?\n/g, "\\n");
|
|
381
|
+
return flat.length > 80 ? flat.slice(0, 77) + "..." : flat;
|
|
382
|
+
}
|