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.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/ErrorHandler.d.ts +1 -1
  3. package/dist/cli/ErrorHandler.js +14 -14
  4. package/dist/cli/commands/bulk.d.ts +1 -1
  5. package/dist/cli/commands/bulk.d.ts.map +1 -1
  6. package/dist/cli/commands/bulk.js +51 -33
  7. package/dist/cli/commands/init.d.ts +1 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +26 -18
  10. package/dist/cli/commands/showReport.d.ts +1 -1
  11. package/dist/cli/commands/showReport.d.ts.map +1 -1
  12. package/dist/cli/commands/showReport.js +12 -10
  13. package/dist/cli/commands/status.d.ts +1 -1
  14. package/dist/cli/commands/status.d.ts.map +1 -1
  15. package/dist/cli/commands/status.js +8 -8
  16. package/dist/cli/commands/sync.d.ts +1 -1
  17. package/dist/cli/commands/sync.d.ts.map +1 -1
  18. package/dist/cli/commands/sync.js +38 -30
  19. package/dist/cli/index.js +9 -9
  20. package/dist/cli/ui/DnaTable.d.ts +1 -1
  21. package/dist/cli/ui/DnaTable.d.ts.map +1 -1
  22. package/dist/cli/ui/DnaTable.js +23 -16
  23. package/dist/cli/ui/RefactorWizard.d.ts +3 -3
  24. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
  25. package/dist/cli/ui/RefactorWizard.js +108 -82
  26. package/dist/config/ConfigLoader.d.ts +3 -2
  27. package/dist/config/ConfigLoader.d.ts.map +1 -1
  28. package/dist/config/ConfigLoader.js +23 -13
  29. package/dist/config/DryRunGuard.d.ts +14 -0
  30. package/dist/config/DryRunGuard.d.ts.map +1 -0
  31. package/dist/config/DryRunGuard.js +79 -0
  32. package/dist/config/SelaConfig.d.ts +26 -16
  33. package/dist/config/SelaConfig.d.ts.map +1 -1
  34. package/dist/config/SelaConfig.js +54 -22
  35. package/dist/engine/HealingRegistry.d.ts +1 -1
  36. package/dist/engine/HealingRegistry.js +3 -3
  37. package/dist/engine/SelaEngine.d.ts +13 -10
  38. package/dist/engine/SelaEngine.d.ts.map +1 -1
  39. package/dist/engine/SelaEngine.js +292 -101
  40. package/dist/errors/SelaError.d.ts +132 -0
  41. package/dist/errors/SelaError.d.ts.map +1 -0
  42. package/dist/errors/SelaError.js +154 -0
  43. package/dist/fixtures/expectProxy.d.ts +1 -1
  44. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  45. package/dist/fixtures/expectProxy.js +17 -10
  46. package/dist/fixtures/index.d.ts +7 -1
  47. package/dist/fixtures/index.d.ts.map +1 -1
  48. package/dist/fixtures/index.js +26 -11
  49. package/dist/fixtures/proxyTag.d.ts +12 -0
  50. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  51. package/dist/fixtures/proxyTag.js +45 -0
  52. package/dist/index.d.ts +8 -6
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -4
  55. package/dist/reporter/SelaReporter.d.ts +143 -0
  56. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  57. package/dist/reporter/SelaReporter.js +428 -0
  58. package/dist/services/ASTSourceUpdater.d.ts +20 -1
  59. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  60. package/dist/services/ASTSourceUpdater.js +257 -89
  61. package/dist/services/ArgumentTypeAnalyzer.js +7 -7
  62. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
  63. package/dist/services/BlastRadiusAnalyzer.js +5 -2
  64. package/dist/services/ChainValidator.js +22 -22
  65. package/dist/services/CrossFileHealer.js +7 -7
  66. package/dist/services/DefinitionTracer.d.ts +1 -1
  67. package/dist/services/DefinitionTracer.d.ts.map +1 -1
  68. package/dist/services/DefinitionTracer.js +28 -13
  69. package/dist/services/DnaEditorService.d.ts +2 -2
  70. package/dist/services/DnaEditorService.d.ts.map +1 -1
  71. package/dist/services/DnaEditorService.js +81 -62
  72. package/dist/services/DnaIndexService.d.ts +1 -1
  73. package/dist/services/DnaIndexService.d.ts.map +1 -1
  74. package/dist/services/DnaIndexService.js +14 -14
  75. package/dist/services/HealReportService.d.ts +43 -1
  76. package/dist/services/HealReportService.d.ts.map +1 -1
  77. package/dist/services/HealReportService.js +108 -25
  78. package/dist/services/HealingAdvisory.js +1 -1
  79. package/dist/services/HealingCacheService.d.ts +135 -0
  80. package/dist/services/HealingCacheService.d.ts.map +1 -0
  81. package/dist/services/HealingCacheService.js +460 -0
  82. package/dist/services/HealthReportService.d.ts +1 -1
  83. package/dist/services/HealthReportService.d.ts.map +1 -1
  84. package/dist/services/HealthReportService.js +25 -25
  85. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  86. package/dist/services/InitializerUpdater.js +20 -1
  87. package/dist/services/IntentAuditor.d.ts +18 -1
  88. package/dist/services/IntentAuditor.d.ts.map +1 -1
  89. package/dist/services/IntentAuditor.js +50 -26
  90. package/dist/services/InteractiveReview.d.ts +15 -0
  91. package/dist/services/InteractiveReview.d.ts.map +1 -0
  92. package/dist/services/InteractiveReview.js +125 -0
  93. package/dist/services/LLMService.d.ts +18 -2
  94. package/dist/services/LLMService.d.ts.map +1 -1
  95. package/dist/services/LLMService.js +33 -23
  96. package/dist/services/PRAutomationService.d.ts +14 -1
  97. package/dist/services/PRAutomationService.d.ts.map +1 -1
  98. package/dist/services/PRAutomationService.js +287 -92
  99. package/dist/services/PendingPromptLedger.d.ts +44 -0
  100. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  101. package/dist/services/PendingPromptLedger.js +180 -0
  102. package/dist/services/ReportGenerator.d.ts +70 -0
  103. package/dist/services/ReportGenerator.d.ts.map +1 -0
  104. package/dist/services/ReportGenerator.js +191 -0
  105. package/dist/services/SafetyGuard.d.ts +30 -7
  106. package/dist/services/SafetyGuard.d.ts.map +1 -1
  107. package/dist/services/SafetyGuard.js +85 -116
  108. package/dist/services/SnapshotService.js +5 -5
  109. package/dist/services/SourceLinkService.d.ts +2 -2
  110. package/dist/services/SourceLinkService.d.ts.map +1 -1
  111. package/dist/services/SourceLinkService.js +31 -22
  112. package/dist/services/SourceUpdater.d.ts +1 -1
  113. package/dist/services/SourceUpdater.d.ts.map +1 -1
  114. package/dist/services/SourceUpdater.js +66 -34
  115. package/dist/services/TemplateDiffService.d.ts.map +1 -1
  116. package/dist/services/TemplateDiffService.js +18 -15
  117. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  118. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  119. package/dist/services/WorkspaceSnapshotService.js +200 -0
  120. package/dist/types/index.d.ts +1 -1
  121. package/dist/utils/IsolatedDiff.d.ts +45 -0
  122. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  123. package/dist/utils/IsolatedDiff.js +382 -0
  124. 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();
@@ -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}]" stable fingerprint for drift comparison */
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
+ }