@cueloop/schema 0.1.0-alpha.102 → 0.1.0-alpha.103

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/schema",
3
- "version": "0.1.0-alpha.102",
3
+ "version": "0.1.0-alpha.103",
4
4
  "description": "The Thread primitive: types, markdown block model, quote-primary anchors, feedback serialization",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -7,9 +7,11 @@ export function agentCommentRoot(state: ThreadAgentState, id: string): AgentComm
7
7
  const comment = state.comments.find((entry) => entry.id === id);
8
8
 
9
9
  if (comment) return comment;
10
+
10
11
  const submission = state.submissions?.find((entry) => entry.id === id && entry.commentId === id);
11
12
 
12
13
  if (!submission) return undefined;
14
+
13
15
  const blocks = parseBlocks(submission.prompt);
14
16
  const anchor = makeAnchor(blocks, 0, 0, Math.min(blocks[0]?.text.length ?? 0, 32768));
15
17
 
@@ -44,6 +44,7 @@ export interface AgentHarnessOptions {
44
44
  export interface AgentHarnessAdapter {
45
45
  id: string;
46
46
  label: string;
47
+ recovery?: "durable";
47
48
  connect(options: AgentHarnessOptions): AgentHarnessConnection;
48
49
  remove?(sessionId: string): void | Promise<void>;
49
50
  }
package/src/anchor.ts CHANGED
@@ -94,8 +94,11 @@ function contextScore(
94
94
  let score = 0;
95
95
 
96
96
  if (prefix === anchor.prefix) score += 2;
97
+
97
98
  if (suffix === anchor.suffix) score += 2;
99
+
98
100
  if (anchor.blockIndex === blockIndex) score += 1;
101
+
99
102
  if (anchor.blockIndex === blockIndex && anchor.start === start) score += 1;
100
103
 
101
104
  return score;
@@ -138,11 +141,13 @@ const FUZZY_CONTEXT_TIE_BREAK = 0.01;
138
141
  /** Block indices to scan, spiralling out from the anchor's hinted block: hint, hint+1, hint-1, ... */
139
142
  function blockScanOrder(hint: number | undefined, count: number): number[] {
140
143
  if (count <= 0) return [];
144
+
141
145
  const first = Number.isInteger(hint) ? Math.min(Math.max(hint!, 0), count - 1) : 0;
142
146
  const order = [first];
143
147
 
144
148
  for (const step of Array.from({ length: count - 1 }, (_unused, index) => index + 1)) {
145
149
  if (first + step < count) order.push(first + step);
150
+
146
151
  if (first - step >= 0) order.push(first - step);
147
152
  }
148
153
 
@@ -175,6 +180,7 @@ function findFuzzyCandidates(
175
180
 
176
181
  candidates.push({ blockIndex, start: match.start, end: match.end, score });
177
182
  }
183
+
178
184
  if (budget.remaining <= 0) break;
179
185
  }
180
186
 
@@ -184,6 +190,7 @@ function findFuzzyCandidates(
184
190
  /** Highest-scoring candidate, or null when there are none. */
185
191
  function pickBest(candidates: Candidate[]): Candidate | null {
186
192
  if (candidates.length === 0) return null;
193
+
187
194
  candidates.sort((left, right) => right.score - left.score);
188
195
 
189
196
  return candidates[0]!;
@@ -214,6 +221,7 @@ function joinBlocks(blocks: Block[]): JoinedDocument {
214
221
 
215
222
  blocks.forEach((block, blockIndex) => {
216
223
  if (blockIndex > 0) text += BLOCK_SEPARATOR;
224
+
217
225
  starts.push(text.length);
218
226
  text += block.text;
219
227
  });
@@ -241,11 +249,13 @@ function locateInBlocks(
241
249
  let blockIndex = starts.findLastIndex((start) => start <= offset);
242
250
 
243
251
  if (blockIndex < 0) blockIndex = 0;
252
+
244
253
  const blockEnd = starts[blockIndex]! + blocks[blockIndex]!.text.length;
245
254
 
246
255
  if (edge === "start" && offset >= blockEnd && blockIndex < blocks.length - 1) {
247
256
  return { blockIndex: blockIndex + 1, offset: 0 };
248
257
  }
258
+
249
259
  if (edge === "end" && offset === starts[blockIndex] && blockIndex > 0) {
250
260
  return {
251
261
  blockIndex: blockIndex - 1,
@@ -272,7 +282,9 @@ function documentScore(
272
282
  let score = 0;
273
283
 
274
284
  if (prefix === anchor.prefix) score += 2;
285
+
275
286
  if (suffix === anchor.suffix) score += 2;
287
+
276
288
  if (anchor.blockIndex === startBlockIndex) score += 1;
277
289
 
278
290
  return score;
@@ -369,6 +381,7 @@ function resolveSpanningAnchor(anchor: Anchor, blocks: Block[]): ResolvedAnchor
369
381
  /** Resolve an anchor against the current blocks, or null when orphaned. */
370
382
  export function resolveAnchor(anchor: Anchor, blocks: Block[]): ResolvedAnchor | null {
371
383
  if (anchor.quote === "") return null;
384
+
372
385
  if (anchor.quote.includes(BLOCK_SEPARATOR)) return resolveSpanningAnchor(anchor, blocks);
373
386
 
374
387
  const exact = pickBest(findExactCandidates(anchor, blocks, anchor.quote));
package/src/diff.test.ts CHANGED
@@ -54,6 +54,7 @@ describe("unifiedDiff", () => {
54
54
 
55
55
  for (const line of diffLines) {
56
56
  if (line.kind === "hunk") continue;
57
+
57
58
  if (line.kind === "ctx" || line.kind === "add") rebuilt.push(line.text.slice(1));
58
59
  }
59
60
  expect(rebuilt.join("\n")).toBe(newText);
package/src/diff.ts CHANGED
@@ -70,6 +70,7 @@ export function unifiedDiff(oldText: string, newText: string, context = 3): Unif
70
70
  const ops = lcsDiff(oldText.split("\n"), newText.split("\n"));
71
71
 
72
72
  if (!ops.some((op) => op.kind !== "ctx")) return null;
73
+
73
74
  let oldLineNumber = 1;
74
75
  let newLineNumber = 1;
75
76
  const rows = ops.map((op) => {
@@ -81,6 +82,7 @@ export function unifiedDiff(oldText: string, newText: string, context = 3): Unif
81
82
  };
82
83
 
83
84
  if (op.kind !== "add") oldLineNumber++;
85
+
84
86
  if (op.kind !== "del") newLineNumber++;
85
87
 
86
88
  return row;
@@ -107,10 +109,12 @@ export function unifiedDiff(oldText: string, newText: string, context = 3): Unif
107
109
 
108
110
  return;
109
111
  }
112
+
110
113
  if (!openHunk) {
111
114
  openHunk = { rows: [] };
112
115
  hunks.push(openHunk);
113
116
  }
117
+
114
118
  openHunk.rows.push(row);
115
119
  });
116
120
  const lines: UnifiedLine[] = [];
package/src/feedback.ts CHANGED
@@ -75,6 +75,7 @@ export function renderFeedback(input: FeedbackInput): string {
75
75
 
76
76
  lines.push("# Review: " + input.outcome.replaceAll("_", " "));
77
77
  lines.push("");
78
+
78
79
  if (input.summary.trim()) {
79
80
  lines.push(input.summary.trim());
80
81
  lines.push("");
@@ -88,6 +89,7 @@ export function renderFeedback(input: FeedbackInput): string {
88
89
  hasEdits = true;
89
90
  lines.push("## Curated changes");
90
91
  lines.push("");
92
+
91
93
  if (input.workingCopy.trim()) {
92
94
  lines.push("The reviewer accepted a subset of your proposed changes. Apply this");
93
95
  lines.push("exact unified diff; it replaces what you submitted.");
@@ -98,6 +100,7 @@ export function renderFeedback(input: FeedbackInput): string {
98
100
  } else {
99
101
  lines.push("The reviewer rejected all of your proposed changes.");
100
102
  }
103
+
101
104
  lines.push("");
102
105
  } else {
103
106
  const diff =
@@ -132,6 +135,7 @@ export function renderFeedback(input: FeedbackInput): string {
132
135
  input.actionBodies,
133
136
  ),
134
137
  );
138
+
135
139
  if (input.sessionId) {
136
140
  lines.push("## Reporting what you addressed");
137
141
  lines.push("");
@@ -178,6 +182,7 @@ function annotationSectionLines(
178
182
  const target = annotationTarget(annotation);
179
183
 
180
184
  if (target.kind !== "file") continue;
185
+
181
186
  const group = fileGroups.get(target.path) ?? [];
182
187
 
183
188
  group.push(annotation);
@@ -200,6 +205,7 @@ function annotationSectionLines(
200
205
  lines.push(`- ${expandActionRefs(reply.body, actionBodies).replace(/\n/g, "\n ")}`);
201
206
  lines.push("");
202
207
  }
208
+
203
209
  lines.push(`annotation id: \`${annotation.id}\``);
204
210
  lines.push("");
205
211
  };
@@ -227,6 +233,7 @@ function annotationSectionLines(
227
233
  pushNote(annotation, index, location, orphan);
228
234
  });
229
235
  }
236
+
230
237
  for (const [filePath, notes] of fileGroups) {
231
238
  lines.push(`## ${filePath} (${notes.length})`);
232
239
  lines.push("");
package/src/fuzzy.ts CHANGED
@@ -20,7 +20,9 @@ export interface FuzzyMatch {
20
20
  */
21
21
  export function levenshteinDistance(left: string, right: string): number {
22
22
  if (left === right) return 0;
23
+
23
24
  if (left.length === 0) return right.length;
25
+
24
26
  if (right.length === 0) return left.length;
25
27
 
26
28
  let previousRow: number[] = Array.from({ length: right.length + 1 }, (_unused, column) => column);
@@ -114,11 +116,14 @@ export function fuzzyFindBestMatch(
114
116
  ) {
115
117
  // each window costs a needle x window edit-distance pass; spend that from the shared budget
116
118
  budget.remaining -= needle.length * windowLength;
119
+
117
120
  if (budget.remaining <= 0) return bestMatch;
121
+
118
122
  const window = haystack.slice(start, start + windowLength);
119
123
  const similarity = similarityRatio(needle, window);
120
124
 
121
125
  if (similarity < minimumSimilarity) continue;
126
+
122
127
  if (bestMatch === null || similarity > bestMatch.similarity) {
123
128
  bestMatch = { start, end: start + windowLength, similarity };
124
129
  }
package/src/herdr.ts CHANGED
@@ -22,9 +22,11 @@ export type HerdrEnv = Record<string, string | undefined>;
22
22
  */
23
23
  export function detectHerdr(env: HerdrEnv = process.env): HerdrContext | null {
24
24
  if (env.HERDR_ENV !== "1") return null;
25
+
25
26
  const paneId = env.HERDR_PANE_ID;
26
27
 
27
28
  if (!paneId) return null;
29
+
28
30
  // herdr 0.8.0 exposes no HERDR_BIN_PATH; the CLI is `herdr` on PATH and finds
29
31
  // the app over HERDR_SOCKET_PATH from the inherited env. Tests still override
30
32
  // HERDR_BIN_PATH to point the spawn at a stub script.
@@ -48,6 +50,7 @@ export function returnPaneFor(
48
50
  env: HerdrEnv = process.env,
49
51
  ): string | undefined {
50
52
  if (!insideHerdr(env)) return undefined;
53
+
51
54
  const pane = env.CUELOOP_RETURN_PANE ?? sessionHerdrPane;
52
55
 
53
56
  return pane && pane !== env.HERDR_PANE_ID ? pane : undefined;
package/src/history.ts CHANGED
@@ -105,6 +105,7 @@ export function pathOf(history: SessionHistory, fromId: string = tipOf(history))
105
105
  ) {
106
106
  if (seen.has(entry.id))
107
107
  throw new HistoryError("cycle", `entry "${entry.id}" is its own ancestor`);
108
+
108
109
  seen.add(entry.id);
109
110
  path.unshift(entry);
110
111
  }
@@ -123,6 +124,7 @@ export function validateHistory(history: SessionHistory): string | null {
123
124
 
124
125
  for (const entry of history.entries) {
125
126
  if (ids.has(entry.id)) return `duplicate entry id "${entry.id}"`;
127
+
126
128
  ids.add(entry.id);
127
129
  }
128
130
  for (const entry of history.entries) {
@@ -130,11 +132,15 @@ export function validateHistory(history: SessionHistory): string | null {
130
132
  return `entry "${entry.id}" points at missing parent "${entry.parentId}"`;
131
133
  }
132
134
  }
135
+
133
136
  if (history.tips[MAIN_BRANCH] === undefined) return `no "${MAIN_BRANCH}" branch`;
137
+
134
138
  if (history.tips[history.branch] === undefined)
135
139
  return `current branch "${history.branch}" has no tip`;
140
+
136
141
  for (const [branch, tip] of Object.entries(history.tips)) {
137
142
  if (!ids.has(tip)) return `branch "${branch}" points at missing entry "${tip}"`;
143
+
138
144
  try {
139
145
  if (!pathOf(history, tip).some((entry) => entry.type === "revision")) {
140
146
  return `branch "${branch}" has no revision on its path`;
@@ -170,14 +176,18 @@ export function derivePath(history: SessionHistory, fromId?: string): DerivedPat
170
176
  const head = revisions.at(-1);
171
177
 
172
178
  if (!head) throw new HistoryError("not-a-revision", "a path without a revision has no head");
179
+
173
180
  const open = new Set<string>();
174
181
  const messages: Message[] = [];
175
182
  const summaries: Array<SessionEntry & { type: "branch-summary" }> = [];
176
183
 
177
184
  for (const entry of path) {
178
185
  if (entry.type === "comment") open.add(entry.annotationId);
186
+
179
187
  if (entry.type === "comment-removed") open.delete(entry.annotationId);
188
+
180
189
  if (entry.type === "message") messages.push(entry.message);
190
+
181
191
  if (entry.type === "branch-summary") summaries.push(entry);
182
192
  }
183
193
 
@@ -265,9 +275,11 @@ export function navigateTo(
265
275
  if (!entryOf(history, targetId)) {
266
276
  throw new HistoryError("unknown-entry", `no entry "${targetId}"`);
267
277
  }
278
+
268
279
  const tip = tipOf(history);
269
280
 
270
281
  if (targetId === tip) throw new HistoryError("already-there", "already at that entry");
282
+
271
283
  const path = pathOf(history);
272
284
  const index = path.findIndex((entry) => entry.id === targetId);
273
285
 
@@ -277,6 +289,7 @@ export function navigateTo(
277
289
  `"${targetId}" is not on branch "${history.branch}" - switch branch first`,
278
290
  );
279
291
  }
292
+
280
293
  const abandoned = path.slice(index + 1).map((entry) => entry.id);
281
294
  const moved: SessionHistory = {
282
295
  ...history,
@@ -306,6 +319,7 @@ export function forkHistory(history: SessionHistory): SessionHistory {
306
319
  for (const entry of pathOf(history)) {
307
320
  if (entry.type === "message" || (entry.type === "revision" && entry.by === "reviewer"))
308
321
  continue;
322
+
309
323
  kept.push({ ...entry, parentId: kept.at(-1)?.id ?? null });
310
324
  const label = history.labels[entry.id];
311
325
 
@@ -397,6 +411,7 @@ export function historyFromLinear(
397
411
  nextComment++;
398
412
  }
399
413
  });
414
+
400
415
  if (session.message) {
401
416
  push({
402
417
  id: `${session.id}_message`,
@@ -405,6 +420,7 @@ export function historyFromLinear(
405
420
  createdAt: session.message.sentAt,
406
421
  });
407
422
  }
423
+
408
424
  const tip = entries.at(-1);
409
425
 
410
426
  if (!tip) throw new HistoryError("not-a-revision", `session ${session.id} has no revision`);
package/src/inline.ts CHANGED
@@ -143,11 +143,14 @@ function matchLink(
143
143
  end: number;
144
144
  } | null {
145
145
  if (text[index] !== "[") return null;
146
+
146
147
  const labelStart = index + 1;
147
148
  const labelEnd = text.indexOf("]", labelStart);
148
149
 
149
150
  if (labelEnd < 0 || labelEnd === labelStart) return null;
151
+
150
152
  if (text[labelEnd + 1] !== "(") return null;
153
+
151
154
  const hrefStart = labelEnd + 2;
152
155
  // the destination may carry balanced parentheses (wiki URLs); the span ends
153
156
  // at the first unmatched closer
@@ -162,6 +165,7 @@ function matchLink(
162
165
  depth--;
163
166
  continue;
164
167
  }
168
+
165
169
  if (scan === hrefStart) return null;
166
170
 
167
171
  return { labelStart, labelEnd, hrefStart, hrefEnd: scan, end: scan + 1 };
@@ -177,6 +181,7 @@ function matchCode(
177
181
  index: number,
178
182
  ): { contentStart: number; contentEnd: number; end: number } | null {
179
183
  if (text[index] !== "`") return null;
184
+
180
185
  let fence = 0;
181
186
 
182
187
  while (text[index + fence] === "`") fence++;
@@ -194,7 +199,9 @@ function matchEmphasis(text: string, index: number): Span | null {
194
199
  const two = text.slice(index, index + 2);
195
200
 
196
201
  if (two === "**") return closingSpan(text, index, 2, "strong");
202
+
197
203
  if (two === "~~") return closingSpan(text, index, 2, "strike");
204
+
198
205
  if (text[index] === "*") return closingSpan(text, index, 1, "em");
199
206
 
200
207
  return null;
@@ -224,9 +231,11 @@ function findLoneStar(text: string, from: number): number {
224
231
  while (index < text.length) {
225
232
  if (text[index] === "*") {
226
233
  if (text[index + 1] !== "*") return index;
234
+
227
235
  while (text[index] === "*") index++; // skip the whole `**` run
228
236
  continue;
229
237
  }
238
+
230
239
  index++;
231
240
  }
232
241
 
package/src/markdown.ts CHANGED
@@ -47,6 +47,7 @@ export function stripLeadingBlockMarker(line: string): string {
47
47
  /** A GFM table delimiter row: one or more dash cells split by pipes, each optionally `:`-aligned. */
48
48
  function isTableDelimiterRow(line: string): boolean {
49
49
  if (!line.includes("|")) return false;
50
+
50
51
  const cells = line
51
52
  .trim()
52
53
  .replace(/^\||\|$/g, "")
@@ -81,9 +82,11 @@ function isMarkerLine(line: string): boolean {
81
82
  */
82
83
  function frontmatterBlock(lines: string[]): Block | null {
83
84
  if (lines[0]?.trim() !== "---") return null;
85
+
84
86
  let close = 1;
85
87
 
86
88
  while (close < lines.length && lines[close]!.trim() !== "---") close++;
89
+
87
90
  if (close >= lines.length) return null;
88
91
 
89
92
  return {
@@ -110,12 +113,17 @@ function singleLineBlock(line: string, lineIndex: number): Block | null {
110
113
  });
111
114
 
112
115
  if (line.startsWith("### ")) return marked("h3");
116
+
113
117
  if (line.startsWith("## ")) return marked("h2");
118
+
114
119
  if (line.startsWith("# ")) return marked("h1");
120
+
115
121
  if (/^(---|\*\*\*|___)\s*$/.test(line)) {
116
122
  return { kind: "hr", text: "", lineStart: lineIndex, lineEnd: lineIndex };
117
123
  }
124
+
118
125
  if (line.startsWith("- ")) return marked("li");
126
+
119
127
  if (/^\d+\. /.test(line)) return marked("oli");
120
128
 
121
129
  return null;
@@ -203,10 +211,13 @@ function nextBlock(lines: string[], lineIndex: number): BlockScan {
203
211
  const line = lines[lineIndex]!;
204
212
 
205
213
  if (line.startsWith("```")) return codeBlock(lines, lineIndex);
214
+
206
215
  const single = singleLineBlock(line, lineIndex);
207
216
 
208
217
  if (single) return { block: single, next: lineIndex + 1 };
218
+
209
219
  if (line.startsWith("> ")) return quoteBlock(lines, lineIndex);
220
+
210
221
  if (isTableStart(lines, lineIndex)) return tableBlock(lines, lineIndex);
211
222
 
212
223
  return paragraphBlock(lines, lineIndex);
@@ -243,11 +254,13 @@ function blockSourceSegments(
243
254
  } else {
244
255
  contentLines = rawLines.map((_, rawLine) => ({ rawLine, prefix: 0 }));
245
256
  }
257
+
246
258
  const content = contentLines
247
259
  .map(({ rawLine, prefix }) => rawLines[rawLine]!.slice(prefix))
248
260
  .join("\n");
249
261
 
250
262
  if (content !== block.text) return undefined;
263
+
251
264
  const rawLineStarts: number[] = [];
252
265
  let rawOffset = 0;
253
266
 
@@ -317,11 +330,13 @@ export function parseBlocks(markdown: string): Block[] {
317
330
  blocks.push(frontmatter);
318
331
  lineIndex = frontmatter.lineEnd + 1;
319
332
  }
333
+
320
334
  while (lineIndex < lines.length) {
321
335
  if (lines[lineIndex]!.trim() === "") {
322
336
  lineIndex++;
323
337
  continue;
324
338
  }
339
+
325
340
  const scan = nextBlock(lines, lineIndex);
326
341
 
327
342
  blocks.push(scan.block);
@@ -35,11 +35,13 @@ export function registerParticipant(
35
35
  const trimmed = name?.trim();
36
36
 
37
37
  if (existing && !trimmed && !source) return session;
38
+
38
39
  const resolvedName = trimmed ?? existing?.name;
39
40
  const resolvedHandle = source?.handle ?? existing?.handle;
40
41
  const next: Identity = { id: author, provider: source?.provider ?? existing?.provider ?? "ssh" };
41
42
 
42
43
  if (resolvedName) next.name = resolvedName;
44
+
43
45
  if (resolvedHandle) next.handle = resolvedHandle;
44
46
 
45
47
  return {
package/src/path-view.ts CHANGED
@@ -52,10 +52,13 @@ export function viewOfPath(history: SessionHistory, known: Annotation[]): PathVi
52
52
  export function applyPathView(session: Thread, view: PathView): void {
53
53
  session.artifact = { ...session.artifact, content: view.content };
54
54
  session.annotations = view.annotations;
55
+
55
56
  if (view.textCuts?.length) session.textCuts = view.textCuts;
56
57
  else delete session.textCuts;
58
+
57
59
  if (view.workingCopy === undefined) delete session.workingCopy;
58
60
  else session.workingCopy = view.workingCopy;
61
+
59
62
  if (view.shelvedAnnotations.length === 0) delete session.shelvedAnnotations;
60
63
  else session.shelvedAnnotations = view.shelvedAnnotations;
61
64
  }
@@ -74,6 +77,7 @@ export function viewFollowing(session: Thread, branch?: string): Thread {
74
77
  const shared: Thread = { ...session };
75
78
 
76
79
  if (!session.history) return shared;
80
+
77
81
  const history = followBranch(session.history, followed);
78
82
  const known = [...session.annotations, ...(session.shelvedAnnotations ?? [])];
79
83
 
@@ -7,7 +7,9 @@ export function isShareViewerAllowed(
7
7
  githubLogin: string | undefined,
8
8
  ): boolean {
9
9
  if (!policy.requireAuth) return true;
10
+
10
11
  if (githubLogin === undefined) return false;
12
+
11
13
  const login = githubLogin.toLowerCase();
12
14
 
13
15
  return policy.allowlist.some((allowed) => allowed.toLowerCase() === login);
@@ -60,6 +60,14 @@ export interface ThreadAgentState {
60
60
  outcome?: "completed" | "failed" | "cancelled";
61
61
  })[];
62
62
  configOptions?: AgentConfigOption[];
63
+ handoff?: {
64
+ target: string;
65
+ requestedTarget?: string;
66
+ operationId: string;
67
+ prompt?: string;
68
+ source?: { id: string; label: string; sessionId?: string };
69
+ } & ({ status: "queued" } | { status: "summarizing" } | { status: "prepared"; text: string });
70
+ continuation?: string;
63
71
  }
64
72
 
65
73
  /** A submission freezes its input; retry keeps the same mirror and discussion origin. */
@@ -69,6 +77,7 @@ export interface AgentSubmission {
69
77
  messageId?: string;
70
78
  prompt: string;
71
79
  harnessPrompt?: string;
80
+ attemptId?: string;
72
81
  quote?: string;
73
82
  context?: string;
74
83
  status: "queued" | "running" | "completed" | "failed";
@@ -17,6 +17,7 @@ export function mergeTextCut(
17
17
  // SAFETY: a no-op returns the caller's array unchanged; callers never mutate the result.
18
18
  return cuts as TextCut[];
19
19
  }
20
+
20
21
  const ranges = [
21
22
  ...cuts.map((cut) => ({ start: cut.start, end: cut.end })),
22
23
  { start: boundedStart, end: boundedEnd },
@@ -52,12 +53,14 @@ export function restoreTextCut(
52
53
  const cutIndex = cuts.findIndex((cut) => cut.start <= start && end <= cut.end);
53
54
 
54
55
  if (cutIndex === -1 || end <= start) return null;
56
+
55
57
  const cut = cuts[cutIndex]!;
56
58
  const replacements: TextCut[] = [];
57
59
 
58
60
  if (cut.start < start) {
59
61
  replacements.push({ start: cut.start, end: start, quote: source.slice(cut.start, start) });
60
62
  }
63
+
61
64
  if (end < cut.end) {
62
65
  replacements.push({ start: end, end: cut.end, quote: source.slice(end, cut.end) });
63
66
  }
@@ -68,6 +71,7 @@ export function restoreTextCut(
68
71
  /** Apply valid source ranges in source order, ignoring stale or overlapping records. */
69
72
  export function applyTextCuts(source: string, cuts: readonly TextCut[]): string {
70
73
  if (cuts.length === 0) return source;
74
+
71
75
  let cursor = 0;
72
76
  const pieces: string[] = [];
73
77
 
@@ -76,6 +80,7 @@ export function applyTextCuts(source: string, cuts: readonly TextCut[]): string
76
80
  )) {
77
81
  if (cut.start < cursor || cut.start < 0 || cut.end > source.length || cut.end <= cut.start)
78
82
  continue;
83
+
79
84
  if (source.slice(cut.start, cut.end) !== cut.quote) continue;
80
85
 
81
86
  pieces.push(source.slice(cursor, cut.start));
@@ -190,7 +195,9 @@ export function restoreBlock(
190
195
  const insertedLines = sourceChunk(base, block).split("\n");
191
196
 
192
197
  if (before.length && before[before.length - 1]!.trim() !== "") insertedLines.unshift("");
198
+
193
199
  if (after.length && after[0]!.trim() !== "") insertedLines.push("");
200
+
194
201
  const restored = [...before, ...insertedLines, ...after].join("\n");
195
202
 
196
203
  return signature(restored) === signature(base) ? undefined : restored;