Skip to repository content1258 lines · 48.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:05:03.127Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
hashed_regions.rs
1//! Hashed Regions (V0609HashedRegions): a variant of the Smart Regions
2//! multi-region format where marker tags are identified by a short
3//! content-derived hash (e.g. `<|marker_b1f8|>`) instead of a sequence
4//! number.
5//!
6//! Hashed identifiers are self-describing: a tag can be mapped back to its
7//! location without reproducing the exact rendering order of the prompt, so
8//! markers can be placed across *all* prompt context, and budget-based
9//! truncation of related files doesn't shift the addressing of the remaining
10//! markers. All context, including the current file, lives in related files:
11//! context retrieval includes the current file via `ContextSource::CurrentFile`,
12//! so the cursor file is expected to be one of the related files. Inputs that
13//! weren't run through current-file retrieval can be normalized with
14//! [`ensure_cursor_file_excerpt`] before rendering or parsing.
15
16use crate::{ContextSource, RelatedExcerpt, RelatedFile, Zeta2PromptInput, multi_region, udiff};
17use anyhow::{Context as _, Result, anyhow};
18use std::{
19 borrow::Cow,
20 collections::{HashMap, HashSet},
21 ops::Range,
22 path::{Path, PathBuf},
23};
24
25pub const MARKER_TAG_PREFIX: &str = "<|marker_";
26pub const MARKER_TAG_SUFFIX: &str = "|>";
27pub const V0615_END_MARKER: &str = "<[end▁of▁sentence]>";
28pub const NO_EDITS: &str = "NO_EDITS";
29/// Number of base64 characters in a marker tag identifier.
30pub const TAG_ID_LEN: usize = 4;
31
32const BASE64_URL_SAFE_ALPHABET: &[u8; 64] =
33 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
34
35pub fn marker_tag(id: &str) -> String {
36 format!("{MARKER_TAG_PREFIX}{id}{MARKER_TAG_SUFFIX}")
37}
38
39/// Marker tags assigned to one contiguous snippet of context.
40#[derive(Debug, Clone)]
41pub struct SnippetMarkers {
42 pub file_ix: usize,
43 pub excerpt_ix: usize,
44 /// `(tag id, byte offset within the snippet text)`, sorted by offset.
45 /// The first marker is at offset 0 and the last at `text.len()`.
46 pub markers: Vec<(String, usize)>,
47}
48
49/// Assign hashed marker tags to every related-file excerpt of `input`.
50///
51/// The assignment is deterministic and independent of any later budget-based
52/// truncation, so the same table can be rebuilt when parsing model output.
53pub fn build_marker_table(input: &Zeta2PromptInput) -> Vec<SnippetMarkers> {
54 build_marker_table_with_filter(input, |_| true)
55}
56
57pub fn build_editable_marker_table(input: &Zeta2PromptInput) -> Vec<SnippetMarkers> {
58 build_marker_table_with_filter(input, is_hash_region_editable_context_source)
59}
60
61pub fn is_hash_region_editable_context_source(context_source: ContextSource) -> bool {
62 matches!(
63 context_source,
64 ContextSource::CurrentFile | ContextSource::EditHistory
65 )
66}
67
68fn build_marker_table_with_filter(
69 input: &Zeta2PromptInput,
70 include_context_source: impl Fn(ContextSource) -> bool,
71) -> Vec<SnippetMarkers> {
72 let mut used_ids = HashSet::new();
73 let mut snippets = Vec::new();
74 if let Some(related_files) = input.related_files.as_deref() {
75 for (file_ix, file) in related_files.iter().enumerate() {
76 for (excerpt_ix, excerpt) in file.excerpts.iter().enumerate() {
77 if include_context_source(excerpt.context_source) {
78 snippets.push(SnippetMarkers {
79 file_ix,
80 excerpt_ix,
81 markers: assign_tags(&excerpt.text, &mut used_ids),
82 });
83 }
84 }
85 }
86 }
87 snippets
88}
89
90pub fn markers_for_text(text: &str) -> Vec<(String, usize)> {
91 let mut used_ids = HashSet::new();
92 assign_tags(text, &mut used_ids)
93}
94
95fn assign_tags(text: &str, used_ids: &mut HashSet<String>) -> Vec<(String, usize)> {
96 let offsets = multi_region::compute_marker_offsets_v0618(text);
97 offsets
98 .iter()
99 .enumerate()
100 .map(|(i, &offset)| {
101 let block = match offsets.get(i + 1) {
102 Some(&next_offset) => &text[offset..next_offset],
103 // The final marker has no following block; hash the preceding
104 // one. This collides with the previous marker's tag by
105 // construction, which `unique_tag_id` resolves by reseeding.
106 None => {
107 let previous_offset = if i == 0 { 0 } else { offsets[i - 1] };
108 &text[previous_offset..offset]
109 }
110 };
111 (unique_tag_id(block, used_ids), offset)
112 })
113 .collect()
114}
115
116fn unique_tag_id(content: &str, used_ids: &mut HashSet<String>) -> String {
117 let mut seed = 0u64;
118 loop {
119 let id = encode_tag_id(hash_with_seed(content, seed));
120 if used_ids.insert(id.clone()) {
121 return id;
122 }
123 seed += 1;
124 }
125}
126
127/// FNV-1a, with the seed folded in ahead of the content.
128fn hash_with_seed(content: &str, seed: u64) -> u64 {
129 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
130 const FNV_PRIME: u64 = 0x100000001b3;
131 let mut hash = FNV_OFFSET;
132 for byte in seed.to_le_bytes() {
133 hash ^= byte as u64;
134 hash = hash.wrapping_mul(FNV_PRIME);
135 }
136 for byte in content.as_bytes() {
137 hash ^= *byte as u64;
138 hash = hash.wrapping_mul(FNV_PRIME);
139 }
140 hash
141}
142
143fn encode_tag_id(hash: u64) -> String {
144 (0..TAG_ID_LEN)
145 .map(|i| BASE64_URL_SAFE_ALPHABET[((hash >> (6 * i)) & 0x3f) as usize] as char)
146 .collect()
147}
148
149/// Write `text` into `output`, inserting marker tags at the given offsets.
150/// When `cursor` is provided, its marker string is inserted at the given byte
151/// offset within `text`.
152pub fn write_snippet_with_markers(
153 output: &mut String,
154 text: &str,
155 markers: &[(String, usize)],
156 cursor: Option<(usize, &str)>,
157) {
158 let mut cursor_placed = false;
159 for (i, (id, offset)) in markers.iter().enumerate() {
160 if !output.is_empty() && !output.ends_with('\n') {
161 output.push('\n');
162 }
163 output.push_str(&marker_tag(id));
164
165 if let Some((_, next_offset)) = markers.get(i + 1) {
166 output.push('\n');
167 let block = &text[*offset..*next_offset];
168 match cursor {
169 Some((cursor_offset, cursor_marker))
170 if !cursor_placed
171 && cursor_offset >= *offset
172 && cursor_offset <= *next_offset =>
173 {
174 cursor_placed = true;
175 let cursor_in_block = cursor_offset - offset;
176 output.push_str(&block[..cursor_in_block]);
177 output.push_str(cursor_marker);
178 output.push_str(&block[cursor_in_block..]);
179 }
180 _ => output.push_str(block),
181 }
182 }
183 }
184}
185
186/// Extract a marker-bounded span from a model-output codeblock.
187///
188/// Returns `(start tag id, end tag id, content)` where `content` is the text
189/// between the first and last marker tags, with any intermediate marker tags
190/// stripped.
191pub fn extract_marker_span(text: &str) -> Result<(String, String, String)> {
192 let (start_id, end_id, content) = extract_marker_span_allow_same(text)?;
193 if start_id == end_id {
194 return Err(anyhow!(
195 "start and end markers are the same (marker {start_id})"
196 ));
197 }
198 Ok((start_id, end_id, content))
199}
200
201pub fn extract_marker_span_allow_same(text: &str) -> Result<(String, String, String)> {
202 let first_tag_start = text
203 .find(MARKER_TAG_PREFIX)
204 .context("no start marker found in output")?;
205 let first_id_start = first_tag_start + MARKER_TAG_PREFIX.len();
206 let first_id_end = text[first_id_start..]
207 .find(MARKER_TAG_SUFFIX)
208 .map(|i| i + first_id_start)
209 .context("malformed start marker tag")?;
210 let start_id = &text[first_id_start..first_id_end];
211 let first_tag_end = first_id_end + MARKER_TAG_SUFFIX.len();
212
213 let last_tag_start = text
214 .rfind(MARKER_TAG_PREFIX)
215 .context("no end marker found in output")?;
216 if last_tag_start == first_tag_start {
217 return Err(anyhow!("output span must be bounded by two marker tags"));
218 }
219 let last_id_start = last_tag_start + MARKER_TAG_PREFIX.len();
220 let last_id_end = text[last_id_start..]
221 .find(MARKER_TAG_SUFFIX)
222 .map(|i| i + last_id_start)
223 .context("malformed end marker tag")?;
224 let end_id = &text[last_id_start..last_id_end];
225
226 let mut content_start = first_tag_end;
227 if text.as_bytes().get(content_start) == Some(&b'\n') {
228 content_start += 1;
229 }
230 let content_end = last_tag_start;
231 let content = &text[content_start..content_end.max(content_start)];
232 let content = multi_region::strip_marker_tags(content);
233 Ok((start_id.to_string(), end_id.to_string(), content))
234}
235
236pub struct RelatedFileCursor {
237 pub file_ix: usize,
238 pub excerpt_ix: usize,
239 pub offset_in_excerpt: usize,
240}
241
242struct ParseSnippet<'a> {
243 file_ix: usize,
244 first_excerpt_ix: usize,
245 last_excerpt_ix: usize,
246 end_row: u32,
247 text: Cow<'a, str>,
248 markers: Vec<(String, usize)>,
249}
250
251pub fn related_file_patch_path(cursor_path: &Path, related_path: &Path) -> PathBuf {
252 let stripped: PathBuf = related_path.iter().skip(1).collect();
253 if stripped == cursor_path {
254 return stripped;
255 }
256
257 let cursor_first_component = cursor_path.components().next();
258 let related_first_component = related_path.components().next();
259 if related_first_component.is_some()
260 && cursor_first_component != related_first_component
261 && related_path.components().count() > 1
262 {
263 stripped
264 } else {
265 related_path.to_path_buf()
266 }
267}
268
269fn line_start_offset(text: &str, row: usize) -> Option<usize> {
270 let mut offset = 0;
271 for _ in 0..row {
272 offset += text[offset..].find('\n')? + 1;
273 }
274 Some(offset)
275}
276
277pub fn locate_cursor_in_related_files(input: &Zeta2PromptInput) -> Option<RelatedFileCursor> {
278 let related_files = input.related_files.as_deref()?;
279 let excerpt_start_row = input.excerpt_start_row?;
280 let cursor_offset = input.cursor_offset_in_excerpt;
281 let excerpt_prefix = input.cursor_excerpt.get(..cursor_offset)?;
282 let cursor_row = excerpt_start_row + excerpt_prefix.matches('\n').count() as u32;
283 let cursor_column = cursor_offset - excerpt_prefix.rfind('\n').map_or(0, |pos| pos + 1);
284
285 for (file_ix, file) in related_files.iter().enumerate() {
286 if related_file_patch_path(&input.cursor_path, &file.path) != input.cursor_path.as_ref() {
287 continue;
288 }
289
290 for (excerpt_ix, excerpt) in file.excerpts.iter().enumerate() {
291 if cursor_row < excerpt.row_range.start || cursor_row > excerpt.row_range.end {
292 continue;
293 }
294 let row_in_excerpt = (cursor_row - excerpt.row_range.start) as usize;
295 let line_start = line_start_offset(&excerpt.text, row_in_excerpt)?;
296 let line_len = excerpt.text[line_start..]
297 .lines()
298 .next()
299 .unwrap_or("")
300 .len();
301 if cursor_column <= line_len {
302 return Some(RelatedFileCursor {
303 file_ix,
304 excerpt_ix,
305 offset_in_excerpt: line_start + cursor_column,
306 });
307 }
308 }
309 }
310
311 None
312}
313
314/// Ensure the cursor file is represented by a related-file excerpt that covers
315/// the cursor, synthesizing one from `cursor_excerpt` when it isn't.
316///
317/// All hashed-region context — including the current file — is addressed
318/// through `related_files` (see module docs), so a prompt built from a
319/// `Zeta2PromptInput` whose `related_files` don't cover the cursor cannot be
320/// rendered or parsed. Inputs produced by current-file context retrieval
321/// (`ContextSource::CurrentFile`) are already covered and left untouched; this
322/// normalizes the rest (e.g. raw settled-data samples, or any caller that
323/// didn't run current-file retrieval) from the `cursor_excerpt` the input
324/// already carries, so the format is usable without re-running context
325/// collection.
326///
327/// When synthesis is needed, any pre-existing excerpts of the cursor file are
328/// **replaced** by the synthesized window: the renderer emits excerpts verbatim
329/// without coalescing, so keeping overlapping fragments would duplicate lines
330/// with conflicting markers. Other related files are left untouched.
331///
332/// Returns whether the cursor file is covered after the call (already, or via
333/// the synthesized excerpt). Returns `false` only when coverage couldn't be
334/// established — e.g. a missing `excerpt_start_row` or an empty
335/// `cursor_excerpt` — in which case the input is left unchanged.
336pub fn ensure_cursor_file_excerpt(input: &mut Zeta2PromptInput) -> bool {
337 if locate_cursor_in_related_files(input).is_some() {
338 return true;
339 }
340 let Some(excerpt_start_row) = input.excerpt_start_row else {
341 return false;
342 };
343 if input.cursor_excerpt.is_empty() {
344 return false;
345 }
346
347 let cursor_excerpt = input.cursor_excerpt.clone();
348 let end_row = excerpt_start_row + cursor_excerpt.matches('\n').count() as u32;
349 let synthesized = RelatedExcerpt {
350 row_range: excerpt_start_row..end_row,
351 text: cursor_excerpt,
352 order: 0,
353 context_source: ContextSource::CurrentFile,
354 };
355
356 let cursor_path = input.cursor_path.clone();
357 let in_open_source_repo = input.in_open_source_repo;
358 let related_files = input.related_files.get_or_insert_with(Vec::new);
359 if let Some(file) = related_files
360 .iter_mut()
361 .find(|file| related_file_patch_path(&cursor_path, &file.path) == cursor_path.as_ref())
362 {
363 file.max_row = file.max_row.max(end_row);
364 file.excerpts = vec![synthesized];
365 } else {
366 related_files.insert(
367 0,
368 RelatedFile {
369 path: cursor_path,
370 max_row: end_row,
371 excerpts: vec![synthesized],
372 in_open_source_repo,
373 },
374 );
375 }
376
377 // Confirm the synthesized excerpt actually covers the cursor (guards against
378 // a cursor offset that lies outside the excerpt text).
379 locate_cursor_in_related_files(input).is_some()
380}
381
382pub fn marker_table_for_excerpt(
383 marker_table: &[SnippetMarkers],
384 file_ix: usize,
385 excerpt_ix: usize,
386) -> Option<&[(String, usize)]> {
387 marker_table.iter().find_map(|snippet| {
388 (snippet.file_ix == file_ix && snippet.excerpt_ix == excerpt_ix)
389 .then_some(snippet.markers.as_slice())
390 })
391}
392
393fn merge_contiguous_snippets(
394 input: &Zeta2PromptInput,
395 marker_table: Vec<SnippetMarkers>,
396) -> Result<Vec<ParseSnippet<'_>>> {
397 let related_files = input
398 .related_files
399 .as_deref()
400 .context("prompt inputs are missing related files")?;
401 let mut snippets: Vec<ParseSnippet> = Vec::new();
402 for snippet in marker_table {
403 let file = related_files
404 .get(snippet.file_ix)
405 .context("related file index out of range")?;
406 let excerpt = file
407 .excerpts
408 .get(snippet.excerpt_ix)
409 .context("related excerpt index out of range")?;
410 if let Some(last) = snippets.last_mut()
411 && last.file_ix == snippet.file_ix
412 && last.last_excerpt_ix + 1 == snippet.excerpt_ix
413 && last.end_row == excerpt.row_range.start
414 {
415 let text = last.text.to_mut();
416 if !text.is_empty() && !text.ends_with('\n') {
417 text.push('\n');
418 }
419 let base = text.len();
420 text.push_str(&excerpt.text);
421 last.markers.extend(
422 snippet
423 .markers
424 .into_iter()
425 .map(|(id, offset)| (id, base + offset)),
426 );
427 last.last_excerpt_ix = snippet.excerpt_ix;
428 last.end_row = excerpt.row_range.end;
429 } else {
430 snippets.push(ParseSnippet {
431 file_ix: snippet.file_ix,
432 first_excerpt_ix: snippet.excerpt_ix,
433 last_excerpt_ix: snippet.excerpt_ix,
434 end_row: excerpt.row_range.end,
435 text: Cow::Borrowed(excerpt.text.as_ref()),
436 markers: snippet.markers,
437 });
438 }
439 }
440 Ok(snippets)
441}
442
443fn snippet_path_and_start_row(
444 input: &Zeta2PromptInput,
445 snippet: &ParseSnippet<'_>,
446) -> Result<(PathBuf, u32)> {
447 let related_files = input
448 .related_files
449 .as_deref()
450 .context("prompt inputs are missing related files")?;
451 let file = related_files
452 .get(snippet.file_ix)
453 .context("related file index out of range")?;
454 let excerpt = file
455 .excerpts
456 .get(snippet.first_excerpt_ix)
457 .context("related excerpt index out of range")?;
458 Ok((
459 related_file_patch_path(&input.cursor_path, &file.path),
460 excerpt.row_range.start,
461 ))
462}
463
464fn common_prefix_suffix(a: &[u8], b: &[u8]) -> (usize, usize) {
465 let prefix = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
466 let remaining_a = a.len() - prefix;
467 let remaining_b = b.len() - prefix;
468 let max_suffix = remaining_a.min(remaining_b);
469 let suffix = a[a.len() - max_suffix..]
470 .iter()
471 .rev()
472 .zip(b[b.len() - max_suffix..].iter().rev())
473 .take_while(|(x, y)| x == y)
474 .count();
475 (prefix, suffix)
476}
477
478fn nearest_marker_id(markers: &[(String, usize)], cursor_offset: Option<usize>) -> &str {
479 let cursor = cursor_offset.unwrap_or(0);
480 markers
481 .iter()
482 .min_by_key(|(_, offset)| (*offset as isize - cursor as isize).unsigned_abs())
483 .map(|(id, _)| id.as_str())
484 .unwrap_or("unknown")
485}
486
487/// Encode a single marker-bounded edit block for one snippet, given its old and
488/// new text. The returned block starts and ends with a marker tag and does
489/// **not** include the output end marker; callers concatenate blocks and append
490/// [`V0615_END_MARKER`] once after the last block.
491pub fn encode_from_old_and_new(
492 old_text: &str,
493 new_text: &str,
494 markers: &[(String, usize)],
495 cursor_offset_in_new: Option<usize>,
496 cursor_marker: &str,
497) -> Result<String> {
498 let no_edit_id = nearest_marker_id(markers, cursor_offset_in_new);
499 if old_text == new_text {
500 let tag = marker_tag(no_edit_id);
501 return Ok(format!("{tag}{tag}"));
502 }
503
504 let (common_prefix, common_suffix) =
505 common_prefix_suffix(old_text.as_bytes(), new_text.as_bytes());
506 let change_end_in_old = old_text.len() - common_suffix;
507 let mut start_marker_ix = markers
508 .iter()
509 .rposition(|(_, offset)| *offset <= common_prefix)
510 .unwrap_or(0);
511 let mut end_marker_ix = markers
512 .iter()
513 .position(|(_, offset)| *offset >= change_end_in_old)
514 .unwrap_or_else(|| markers.len().saturating_sub(1));
515
516 if start_marker_ix == end_marker_ix {
517 if end_marker_ix < markers.len().saturating_sub(1) {
518 end_marker_ix += 1;
519 } else if start_marker_ix > 0 {
520 start_marker_ix -= 1;
521 }
522 }
523
524 let old_start = markers
525 .get(start_marker_ix)
526 .map(|(_, offset)| *offset)
527 .context("start marker out of range")?;
528 let old_end = markers
529 .get(end_marker_ix)
530 .map(|(_, offset)| *offset)
531 .context("end marker out of range")?;
532 let new_start = old_start;
533 let new_end = new_text
534 .len()
535 .saturating_sub(old_text.len().saturating_sub(old_end));
536 let new_span = &new_text[new_start..new_end];
537
538 let mut result = String::new();
539 result.push_str(&marker_tag(&markers[start_marker_ix].0));
540 result.push('\n');
541 if let Some(cursor_offset) = cursor_offset_in_new {
542 if cursor_offset >= new_start && cursor_offset <= new_end {
543 let cursor_in_span = cursor_offset - new_start;
544 result.push_str(&new_span[..cursor_in_span]);
545 result.push_str(cursor_marker);
546 result.push_str(&new_span[cursor_in_span..]);
547 } else {
548 result.push_str(new_span);
549 }
550 } else {
551 result.push_str(new_span);
552 }
553 if !result.ends_with('\n') {
554 result.push('\n');
555 }
556 result.push_str(&marker_tag(&markers[end_marker_ix].0));
557 Ok(result)
558}
559
560/// Parse student model output (raw marker spans, no markdown code fences) into
561/// a unified patch.
562///
563/// The output is a run of marker tags with no fences, so blocks are delimited by
564/// pairing tags two at a time: `(1, 2), (3, 4), ...`. This matches the encoder,
565/// which emits exactly two tags per block and no intermediate tags. Any
566/// unpaired trailing tag is ignored.
567pub fn parse_output_as_patch(
568 input: &Zeta2PromptInput,
569 output: &str,
570 cursor_marker: &str,
571) -> Result<String> {
572 let output = output.strip_suffix(V0615_END_MARKER).unwrap_or(output);
573 if output.trim() == NO_EDITS {
574 return Ok(String::new());
575 }
576
577 let spans = pair_marker_spans(output)?;
578 let (patch, _cursor) = build_patch_from_spans(input, &spans, cursor_marker)?;
579 Ok(patch)
580}
581
582/// A cursor position resolved while turning marker-span edits into a patch.
583pub struct HashRegionCursor {
584 pub path: PathBuf,
585 /// Byte offset of the cursor within `new_text`.
586 pub cursor_offset_in_new_text: usize,
587 /// Full new text of the edited snippet, after applying all of its edits.
588 pub new_text: String,
589 /// Original text of the edited snippet.
590 pub old_text: String,
591 /// 0-based row where the snippet starts in its file.
592 pub start_row: u32,
593}
594
595/// One marker-bounded edit resolved against a parse snippet.
596struct ParsedSpanEdit {
597 snippet_ix: usize,
598 range: Range<usize>,
599 new_text: String,
600 cursor_offset_in_new_text: Option<usize>,
601}
602
603/// Split raw model output into marker-bounded spans by pairing marker tags two
604/// at a time. Returns `(start_id, end_id, raw_new_span)` per pair, where
605/// `raw_new_span` may still contain the cursor marker.
606fn pair_marker_spans(output: &str) -> Result<Vec<(String, String, String)>> {
607 let tags = find_all_marker_tags(output);
608 if tags.len() < 2 {
609 return Err(anyhow!("output does not contain a marker-bounded span"));
610 }
611 let mut spans = Vec::new();
612 let mut i = 0;
613 while i + 1 < tags.len() {
614 let (start_id, _, start_tag_end) = &tags[i];
615 let (end_id, end_tag_start, _) = &tags[i + 1];
616 let content = &output[*start_tag_end..*end_tag_start];
617 let content = content.strip_prefix('\n').unwrap_or(content);
618 let content = multi_region::strip_marker_tags(content);
619 spans.push((start_id.clone(), end_id.clone(), content));
620 i += 2;
621 }
622 Ok(spans)
623}
624
625/// Find every marker tag in `text`, in order, as `(id, tag_start, tag_end)`.
626fn find_all_marker_tags(text: &str) -> Vec<(String, usize, usize)> {
627 let mut tags = Vec::new();
628 let mut search = 0;
629 while let Some(rel) = text[search..].find(MARKER_TAG_PREFIX) {
630 let tag_start = search + rel;
631 let id_start = tag_start + MARKER_TAG_PREFIX.len();
632 let Some(suffix_rel) = text[id_start..].find(MARKER_TAG_SUFFIX) else {
633 break;
634 };
635 let id_end = id_start + suffix_rel;
636 let tag_end = id_end + MARKER_TAG_SUFFIX.len();
637 tags.push((text[id_start..id_end].to_string(), tag_start, tag_end));
638 search = tag_end;
639 }
640 tags
641}
642
643/// Resolve a list of marker spans into per-snippet edits and assemble a unified
644/// patch.
645///
646/// `spans` is a list of `(start_id, end_id, raw_new_span)` where `raw_new_span`
647/// may still contain `cursor_marker`. This is shared by the student parser
648/// (which pairs raw marker tags) and the teacher parser (which extracts spans
649/// from markdown code fences). Edits that overlap an already-accepted edit in
650/// the same snippet are skipped (lenient). The cursor marker is honored in
651/// every region that contains it; the returned [`HashRegionCursor`] reports the
652/// first such position.
653pub fn build_patch_from_spans(
654 input: &Zeta2PromptInput,
655 spans: &[(String, String, String)],
656 cursor_marker: &str,
657) -> Result<(String, Option<HashRegionCursor>)> {
658 let marker_table = build_marker_table(input);
659 let snippets = merge_contiguous_snippets(input, marker_table)?;
660 let mut marker_index: HashMap<&str, (usize, usize)> = HashMap::new();
661 for (snippet_ix, snippet) in snippets.iter().enumerate() {
662 for (id, offset) in &snippet.markers {
663 marker_index.insert(id.as_str(), (snippet_ix, *offset));
664 }
665 }
666
667 let mut edits: Vec<ParsedSpanEdit> = Vec::new();
668 for (start_id, end_id, raw_new_span) in spans {
669 let &(start_snippet, start_byte) = marker_index
670 .get(start_id.as_str())
671 .with_context(|| format!("unknown start marker `{start_id}`"))?;
672 let &(end_snippet, end_byte) = marker_index
673 .get(end_id.as_str())
674 .with_context(|| format!("unknown end marker `{end_id}`"))?;
675
676 if start_snippet != end_snippet {
677 return Err(anyhow!(
678 "markers `{start_id}` and `{end_id}` belong to different context snippets \
679 that are not contiguous excerpts of the same file"
680 ));
681 }
682 if start_byte > end_byte {
683 return Err(anyhow!(
684 "start marker `{start_id}` must come before end marker `{end_id}`"
685 ));
686 }
687
688 let old_text = snippets[start_snippet].text.as_ref();
689 let old_span = &old_text[start_byte..end_byte];
690
691 let cursor_in_span = raw_new_span.find(cursor_marker);
692 let mut new_span = raw_new_span.replace(cursor_marker, "");
693 if old_span.is_empty() {
694 if !new_span.is_empty() && !new_span.ends_with('\n') {
695 new_span.push('\n');
696 }
697 } else {
698 if old_span.ends_with('\n') && !new_span.ends_with('\n') && !new_span.is_empty() {
699 new_span.push('\n');
700 }
701 if !old_span.ends_with('\n') && new_span.ends_with('\n') {
702 new_span.pop();
703 }
704 }
705
706 if !new_span.is_empty()
707 && let Some(dropped) = detect_trailing_deletion(old_span, &new_span)
708 {
709 return Err(anyhow!(
710 "edit span `{start_id}`..`{end_id}` looks truncated: the replacement \
711 stops before the end marker, which would silently delete:\n{dropped}"
712 ));
713 }
714
715 // `cursor_in_span` was located in `raw_new_span` before the trailing
716 // newline normalization above, which can drop a byte. Clamp it to the
717 // finalized replacement so the offset never points past `new_span`
718 // (downstream cursor mapping byte-slices `new_text` by this offset).
719 let cursor_offset_in_new_text = cursor_in_span.map(|offset| offset.min(new_span.len()));
720 edits.push(ParsedSpanEdit {
721 snippet_ix: start_snippet,
722 range: start_byte..end_byte,
723 new_text: new_span,
724 cursor_offset_in_new_text,
725 });
726 }
727
728 assemble_patch_from_edits(input, &snippets, edits)
729}
730
731/// Apply resolved edits to their snippets and emit one diff section per edited
732/// snippet, in the order snippets first appear in the edit sequence.
733fn assemble_patch_from_edits(
734 input: &Zeta2PromptInput,
735 snippets: &[ParseSnippet<'_>],
736 edits: Vec<ParsedSpanEdit>,
737) -> Result<(String, Option<HashRegionCursor>)> {
738 let mut snippet_order: Vec<usize> = Vec::new();
739 for edit in &edits {
740 if !snippet_order.contains(&edit.snippet_ix) {
741 snippet_order.push(edit.snippet_ix);
742 }
743 }
744
745 let mut diff_output = String::new();
746 let mut cursor = None;
747
748 for &snippet_ix in &snippet_order {
749 let snippet = &snippets[snippet_ix];
750 let mut snippet_edits: Vec<&ParsedSpanEdit> = edits
751 .iter()
752 .filter(|edit| edit.snippet_ix == snippet_ix)
753 .collect();
754 snippet_edits.sort_by_key(|edit| edit.range.start);
755
756 // Lenient overlap handling: keep edits in line order, dropping any whose
757 // range starts before the previous accepted edit ended.
758 let mut accepted: Vec<&ParsedSpanEdit> = Vec::new();
759 let mut last_end = 0usize;
760 for edit in snippet_edits {
761 if !accepted.is_empty() && edit.range.start < last_end {
762 continue;
763 }
764 last_end = edit.range.end;
765 accepted.push(edit);
766 }
767
768 let old_text = snippet.text.as_ref();
769 let (path, start_row) = snippet_path_and_start_row(input, snippet)?;
770
771 let mut new_text = String::new();
772 let mut position = 0;
773 let mut cursor_in_new_text = None;
774 for edit in &accepted {
775 new_text.push_str(&old_text[position..edit.range.start]);
776 if let Some(cursor_offset) = edit.cursor_offset_in_new_text {
777 cursor_in_new_text = Some(new_text.len() + cursor_offset);
778 }
779 new_text.push_str(&edit.new_text);
780 position = edit.range.end;
781 }
782 new_text.push_str(&old_text[position..]);
783
784 let diff = udiff::unified_diff_with_context(old_text, &new_text, start_row, start_row, 3);
785 if !diff.is_empty() {
786 let path_str = path
787 .iter()
788 .map(|component| component.to_string_lossy())
789 .collect::<Vec<_>>()
790 .join("/");
791 diff_output.push_str(&format!("--- a/{path_str}\n+++ b/{path_str}\n"));
792 diff_output.push_str(&diff);
793 if !diff_output.ends_with('\n') {
794 diff_output.push('\n');
795 }
796 }
797
798 if cursor.is_none()
799 && let Some(cursor_offset) = cursor_in_new_text
800 {
801 cursor = Some(HashRegionCursor {
802 path: path.clone(),
803 cursor_offset_in_new_text: cursor_offset,
804 new_text: new_text.clone(),
805 old_text: old_text.to_string(),
806 start_row,
807 });
808 }
809 }
810
811 Ok((diff_output, cursor))
812}
813
814/// Detects a span replacement that ends in a pure deletion of the span's tail,
815/// the signature of a model that stopped writing before reaching its end
816/// marker.
817///
818/// Returns the deleted tail if the line diff between `old_span` and `new_span`
819/// ends with a deletion-only group that reaches the last line of `old_span` and
820/// drops more than `MAX_TRAILING_DELETED_LINES` non-blank lines.
821fn detect_trailing_deletion(old_span: &str, new_span: &str) -> Option<String> {
822 const MAX_TRAILING_DELETED_LINES: usize = 3;
823
824 fn flag_if_large(deleted_tail: &str) -> Option<String> {
825 let non_blank_deleted = deleted_tail
826 .lines()
827 .filter(|line| !line.trim().is_empty())
828 .count();
829 (non_blank_deleted > MAX_TRAILING_DELETED_LINES)
830 .then(|| deleted_tail.trim_end().to_string())
831 }
832
833 // A verbatim prefix is checked at the byte level so that a replacement
834 // stopping mid-line is caught too; the line diff below would see that as a
835 // trailing replace group rather than a pure deletion.
836 if let Some(deleted_tail) = old_span.strip_prefix(new_span) {
837 return flag_if_large(deleted_tail);
838 }
839
840 // With zero context lines, hunks contain only `-` and `+` lines, and within
841 // a hunk deletions precede insertions, so a diff whose final line is a
842 // deletion ends with a deletion-only group.
843 let diff = udiff::unified_diff_with_context(old_span, new_span, 0, 0, 0);
844 let lines: Vec<&str> = diff.lines().collect();
845 let mut deletion_start = lines.len();
846 while deletion_start > 0 && lines[deletion_start - 1].starts_with('-') {
847 deletion_start -= 1;
848 }
849 let deleted: Vec<&str> = lines[deletion_start..]
850 .iter()
851 .map(|line| line.strip_prefix('-').unwrap_or(line))
852 .collect();
853 if deleted.is_empty() {
854 return None;
855 }
856
857 // The trailing `-` run is preceded by its hunk header exactly when the hunk
858 // is deletion-only (a replacement group would interpose `+` lines).
859 let header = lines.get(deletion_start.checked_sub(1)?)?;
860 let old_range_start: usize = header
861 .strip_prefix("@@ -")?
862 .split(',')
863 .next()?
864 .parse()
865 .ok()?;
866
867 // Only flag deletions that reach the end of the span; a deletion in the
868 // middle is followed by reproduced context, so the model demonstrably kept
869 // writing past it.
870 if old_range_start + deleted.len() - 1 != old_span.lines().count() {
871 return None;
872 }
873
874 flag_if_large(&deleted.join("\n"))
875}
876
877/// Encode an expected unified patch into the training output for a student.
878///
879/// Emits **one marker-bounded block per diff hunk**, in patch order (which
880/// preserves the teacher's cross-file ordering), with no markdown code fences,
881/// terminated by a single [`V0615_END_MARKER`]. Blocks are separated by a
882/// newline; the parser re-pairs marker tags two at a time, so the separator is
883/// not significant.
884///
885/// Reachability is per hunk: a hunk whose file is absent from the prompt
886/// context, or whose location can't be resolved within its snippet, is skipped.
887/// If at least one hunk is reachable, the remaining hunks are still encoded
888/// (partial edit). If no hunk is reachable, the output is `NO_EDITS`.
889pub fn encode_patch_as_output(
890 input: &Zeta2PromptInput,
891 patch: &str,
892 cursor_offset: Option<usize>,
893 cursor_marker: &str,
894) -> Result<String> {
895 if patch.lines().count() <= 3 {
896 return Ok(format!("{NO_EDITS}{V0615_END_MARKER}"));
897 }
898
899 let marker_table = build_marker_table(input);
900 let snippets = merge_contiguous_snippets(input, marker_table)?;
901 let mut parser = udiff::DiffParser::new(patch);
902 let mut blocks: Vec<String> = Vec::new();
903
904 while let Some(event) = parser.next().context("failed to parse expected patch")? {
905 let udiff::DiffEvent::Hunk {
906 path,
907 mut hunk,
908 status: _,
909 } = event
910 else {
911 continue;
912 };
913
914 // A hunk whose file isn't in the prompt context is unreachable; skip it
915 // and keep any other reachable hunks (partial edit).
916 let Some((snippet_ix, start_row)) =
917 snippets
918 .iter()
919 .enumerate()
920 .find_map(|(snippet_ix, snippet)| {
921 let (snippet_path, start_row) =
922 snippet_path_and_start_row(input, snippet).ok()?;
923 (snippet_path == Path::new(path.as_ref())).then_some((snippet_ix, start_row))
924 })
925 else {
926 continue;
927 };
928 let snippet = &snippets[snippet_ix];
929 let old_text = snippet.text.as_ref();
930 let candidates = udiff::find_context_candidates(old_text, &mut hunk);
931 // A hunk whose location can't be pinned down within the snippet is
932 // unreachable; skip it.
933 let Some(hunk_offset) =
934 udiff::disambiguate_by_line_number(&candidates, hunk.start_line, &|offset| {
935 start_row + old_text[..offset].matches('\n').count() as u32
936 })
937 else {
938 continue;
939 };
940
941 let mut new_text = old_text.to_string();
942 for edit in hunk.edits.iter().rev() {
943 let range = (hunk_offset + edit.range.start)..(hunk_offset + edit.range.end);
944 new_text.replace_range(range, &edit.text);
945 }
946 // The cursor marker is placed in every region whose span contains it.
947 // The extracted `cursor_offset` is hunk-relative, so map it through each
948 // hunk's offset; `encode_from_old_and_new` inserts it only when it lands
949 // within that block's span.
950 let cursor_in_new = cursor_offset.map(|cursor| (hunk_offset + cursor).min(new_text.len()));
951 blocks.push(encode_from_old_and_new(
952 old_text,
953 &new_text,
954 &snippet.markers,
955 cursor_in_new,
956 cursor_marker,
957 )?);
958 }
959
960 if blocks.is_empty() {
961 return Ok(format!("{NO_EDITS}{V0615_END_MARKER}"));
962 }
963
964 let mut output = blocks.join("\n");
965 output.push_str(V0615_END_MARKER);
966 Ok(output)
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use crate::{ContextSource, RelatedExcerpt, RelatedFile};
973 use std::path::PathBuf;
974 use std::sync::Arc;
975
976 fn make_input(cursor_excerpt: &str, related: &[(&str, &[&str])]) -> Zeta2PromptInput {
977 Zeta2PromptInput {
978 cursor_path: PathBuf::from("src/main.rs").into(),
979 cursor_excerpt: cursor_excerpt.into(),
980 cursor_offset_in_excerpt: 0,
981 excerpt_start_row: Some(0),
982 events: Vec::new(),
983 related_files: Some(
984 related
985 .iter()
986 .map(|(path, excerpts)| {
987 let mut row = 0;
988 RelatedFile {
989 path: Arc::from(PathBuf::from(path).as_path()),
990 max_row: 1000,
991 excerpts: excerpts
992 .iter()
993 .map(|text| {
994 let row_count = text.matches('\n').count() as u32;
995 let excerpt = RelatedExcerpt {
996 row_range: row..row + row_count,
997 text: Arc::from(*text),
998 order: 0,
999 context_source: ContextSource::CurrentFile,
1000 };
1001 row += row_count + 10;
1002 excerpt
1003 })
1004 .collect(),
1005 in_open_source_repo: false,
1006 }
1007 })
1008 .collect(),
1009 ),
1010 active_buffer_diagnostics: Vec::new(),
1011 excerpt_ranges: crate::ExcerptRanges::default(),
1012 syntax_ranges: None,
1013 in_open_source_repo: false,
1014 can_collect_data: false,
1015 repo_url: None,
1016 }
1017 }
1018
1019 #[test]
1020 fn test_ensure_cursor_file_excerpt_synthesizes_when_uncovered() {
1021 // The cursor file's only related excerpt is a fragment elsewhere in the
1022 // file (rows 40..42), not covering the cursor at row 1.
1023 let mut input = make_input(
1024 "fn main() {\n let x = 1;\n}\n",
1025 &[("src/main.rs", &["// unrelated\n// fragment\n"])],
1026 );
1027 input.cursor_offset_in_excerpt = 16; // inside " let x = 1;"
1028 input.related_files.as_mut().unwrap()[0].excerpts[0].row_range = 40..42;
1029
1030 assert!(locate_cursor_in_related_files(&input).is_none());
1031 assert!(ensure_cursor_file_excerpt(&mut input));
1032
1033 let cursor =
1034 locate_cursor_in_related_files(&input).expect("cursor covered after synthesis");
1035 let file = &input.related_files.as_ref().unwrap()[cursor.file_ix];
1036 // The fragment was replaced by the synthesized full window, so the file
1037 // content isn't duplicated with overlapping markers.
1038 assert_eq!(file.excerpts.len(), 1);
1039 assert_eq!(file.excerpts[0].context_source, ContextSource::CurrentFile);
1040 assert_eq!(file.excerpts[0].row_range, 0..3);
1041 assert_eq!(
1042 file.excerpts[0].text.as_ref(),
1043 "fn main() {\n let x = 1;\n}\n"
1044 );
1045 }
1046
1047 #[test]
1048 fn test_ensure_cursor_file_excerpt_noop_when_covered() {
1049 // make_input places the cursor file's excerpt at rows 0..3, covering the
1050 // cursor at row 1.
1051 let mut input = make_input(
1052 "fn main() {\n let x = 1;\n}\n",
1053 &[("src/main.rs", &["fn main() {\n let x = 1;\n}\n"])],
1054 );
1055 input.cursor_offset_in_excerpt = 16;
1056 let before = input.clone();
1057 assert!(ensure_cursor_file_excerpt(&mut input));
1058 assert_eq!(input, before);
1059 }
1060
1061 #[test]
1062 fn test_tag_ids_are_unique_even_for_identical_blocks() {
1063 let mut used = HashSet::new();
1064 let id_a = unique_tag_id("same content", &mut used);
1065 let id_b = unique_tag_id("same content", &mut used);
1066 assert_ne!(id_a, id_b);
1067 assert_eq!(id_a.len(), TAG_ID_LEN);
1068 assert_eq!(id_b.len(), TAG_ID_LEN);
1069 }
1070
1071 #[test]
1072 fn test_tag_ids_are_deterministic() {
1073 let mut used_a = HashSet::new();
1074 let mut used_b = HashSet::new();
1075 assert_eq!(
1076 unique_tag_id("hello\nworld\n", &mut used_a),
1077 unique_tag_id("hello\nworld\n", &mut used_b)
1078 );
1079 }
1080
1081 #[test]
1082 fn test_build_marker_table_covers_all_context() {
1083 let input = make_input(
1084 "fn main() {\n println!();\n}\n",
1085 &[
1086 ("src/a.rs", &["struct A;\n", "impl A {}\n"]),
1087 ("src/b.rs", &["struct B;\n"]),
1088 ],
1089 );
1090 let table = build_marker_table(&input);
1091 assert_eq!(table.len(), 3);
1092 assert_eq!((table[0].file_ix, table[0].excerpt_ix), (0, 0));
1093 assert_eq!((table[1].file_ix, table[1].excerpt_ix), (0, 1));
1094 assert_eq!((table[2].file_ix, table[2].excerpt_ix), (1, 0));
1095
1096 let mut all_ids = HashSet::new();
1097 for snippet in &table {
1098 assert!(snippet.markers.len() >= 2);
1099 assert_eq!(snippet.markers.first().map(|(_, offset)| *offset), Some(0));
1100 for (id, _) in &snippet.markers {
1101 assert!(all_ids.insert(id.clone()), "duplicate tag id {id}");
1102 }
1103 }
1104 }
1105
1106 #[test]
1107 fn test_write_snippet_with_markers_and_cursor() {
1108 let text = "fn main() {\n let x = 1;\n}\n";
1109 let markers = vec![("aaaa".to_string(), 0), ("bbbb".to_string(), text.len())];
1110 let mut output = String::new();
1111 write_snippet_with_markers(&mut output, text, &markers, Some((16, "<|user_cursor|>")));
1112 assert_eq!(
1113 output,
1114 "<|marker_aaaa|>\nfn main() {\n <|user_cursor|>let x = 1;\n}\n<|marker_bbbb|>"
1115 );
1116 }
1117
1118 #[test]
1119 fn test_extract_marker_span_round_trip() {
1120 let codeblock = "<|marker_aaaa|>\nnew content\n<|marker_bbbb|>";
1121 let (start, end, content) = extract_marker_span(codeblock).unwrap();
1122 assert_eq!(start, "aaaa");
1123 assert_eq!(end, "bbbb");
1124 assert_eq!(content, "new content\n");
1125 }
1126
1127 #[test]
1128 fn test_extract_marker_span_strips_intermediate_tags() {
1129 let codeblock = "<|marker_aaaa|>\nline one\n<|marker_cccc|>\nline two\n<|marker_bbbb|>";
1130 let (start, end, content) = extract_marker_span(codeblock).unwrap();
1131 assert_eq!(start, "aaaa");
1132 assert_eq!(end, "bbbb");
1133 assert_eq!(content, "line one\nline two\n");
1134 }
1135
1136 #[test]
1137 fn test_extract_marker_span_rejects_single_marker() {
1138 assert!(extract_marker_span("<|marker_aaaa|>\ncontent\n").is_err());
1139 }
1140
1141 #[test]
1142 fn test_extract_marker_span_rejects_same_marker() {
1143 assert!(extract_marker_span("<|marker_aaaa|>\ncontent\n<|marker_aaaa|>").is_err());
1144 }
1145
1146 const MULTI_FN_EXCERPT: &str = "fn alpha() {\n one();\n}\n\nfn beta() {\n two();\n}\n\nfn gamma() {\n three();\n}\n";
1147
1148 const TWO_HUNK_PATCH: &str = concat!(
1149 "--- a/src/main.rs\n",
1150 "+++ b/src/main.rs\n",
1151 "@@ -1,3 +1,3 @@\n",
1152 " fn alpha() {\n",
1153 "- one();\n",
1154 "+ uno();\n",
1155 " }\n",
1156 "@@ -9,3 +9,3 @@\n",
1157 " fn gamma() {\n",
1158 "- three();\n",
1159 "+ tres();\n",
1160 " }\n",
1161 );
1162
1163 #[test]
1164 fn test_encode_multi_hunk_emits_multiple_blocks() {
1165 let input = make_input(MULTI_FN_EXCERPT, &[("src/main.rs", &[MULTI_FN_EXCERPT])]);
1166 let output =
1167 encode_patch_as_output(&input, TWO_HUNK_PATCH, None, "<|user_cursor|>").unwrap();
1168
1169 assert!(output.ends_with(V0615_END_MARKER), "output: {output}");
1170 // Two blocks => four marker tags, exactly one end marker.
1171 assert_eq!(
1172 output.matches(MARKER_TAG_PREFIX).count(),
1173 4,
1174 "output: {output}"
1175 );
1176 assert_eq!(
1177 output.matches(V0615_END_MARKER).count(),
1178 1,
1179 "output: {output}"
1180 );
1181 assert!(output.contains("uno();"), "output: {output}");
1182 assert!(output.contains("tres();"), "output: {output}");
1183 }
1184
1185 #[test]
1186 fn test_round_trip_multi_hunk() {
1187 let input = make_input(MULTI_FN_EXCERPT, &[("src/main.rs", &[MULTI_FN_EXCERPT])]);
1188 let output =
1189 encode_patch_as_output(&input, TWO_HUNK_PATCH, None, "<|user_cursor|>").unwrap();
1190 let patch = parse_output_as_patch(&input, &output, "<|user_cursor|>").unwrap();
1191
1192 assert!(patch.contains("- one();"), "patch: {patch}");
1193 assert!(patch.contains("+ uno();"), "patch: {patch}");
1194 assert!(patch.contains("- three();"), "patch: {patch}");
1195 assert!(patch.contains("+ tres();"), "patch: {patch}");
1196 }
1197
1198 #[test]
1199 fn test_encode_partial_skips_unreachable_hunk() {
1200 // Second hunk targets a file that is not in the prompt context, so it
1201 // is unreachable. The first (reachable) hunk is still encoded.
1202 let patch = format!(
1203 "{TWO_HUNK_PATCH}--- a/other.rs\n+++ b/other.rs\n@@ -1,1 +1,1 @@\n-gone();\n+kept();\n"
1204 );
1205 let input = make_input(MULTI_FN_EXCERPT, &[("src/main.rs", &[MULTI_FN_EXCERPT])]);
1206 let output = encode_patch_as_output(&input, &patch, None, "<|user_cursor|>").unwrap();
1207
1208 assert_ne!(output.trim_end_matches(V0615_END_MARKER), NO_EDITS);
1209 assert!(output.contains("uno();"), "output: {output}");
1210 assert!(output.contains("tres();"), "output: {output}");
1211 assert!(!output.contains("kept();"), "output: {output}");
1212 }
1213
1214 #[test]
1215 fn test_encode_no_edits_when_all_hunks_unreachable() {
1216 let patch = "--- a/other.rs\n+++ b/other.rs\n@@ -1,3 +1,3 @@\n fn x() {\n- gone();\n+ kept();\n }\n";
1217 let input = make_input(MULTI_FN_EXCERPT, &[("src/main.rs", &[MULTI_FN_EXCERPT])]);
1218 let output = encode_patch_as_output(&input, patch, None, "<|user_cursor|>").unwrap();
1219
1220 assert_eq!(output, format!("{NO_EDITS}{V0615_END_MARKER}"));
1221 }
1222
1223 #[test]
1224 fn test_parse_multiple_direct_marker_blocks() {
1225 // The student emits raw marker spans with no code fences; blocks are
1226 // delimited by pairing tags two at a time.
1227 let input = make_input(MULTI_FN_EXCERPT, &[("src/main.rs", &[MULTI_FN_EXCERPT])]);
1228 let markers = build_marker_table(&input)[0].markers.clone();
1229 assert!(markers.len() >= 3, "expected internal markers: {markers:?}");
1230
1231 let tag = |ix: usize| marker_tag(&markers[ix].0);
1232 let old_first = &MULTI_FN_EXCERPT[markers[0].1..markers[1].1];
1233 let old_second = &MULTI_FN_EXCERPT[markers[1].1..markers[markers.len() - 1].1];
1234 let new_first = old_first.replace("one()", "uno()");
1235 let new_second = old_second.replace("three()", "tres()");
1236
1237 let output = format!(
1238 "{}\n{}{}\n{}\n{}{}{}",
1239 tag(0),
1240 new_first,
1241 tag(1),
1242 tag(1),
1243 new_second,
1244 tag(markers.len() - 1),
1245 V0615_END_MARKER,
1246 );
1247
1248 let patch = parse_output_as_patch(&input, &output, "<|user_cursor|>").unwrap();
1249 assert!(patch.contains("+ uno();"), "patch: {patch}");
1250 assert!(patch.contains("+ tres();"), "patch: {patch}");
1251 assert_eq!(
1252 patch.matches("--- a/src/main.rs").count(),
1253 1,
1254 "patch: {patch}"
1255 );
1256 }
1257}
1258