diff --git a/codex-rs/external-agent-migration/src/rewrite.rs b/codex-rs/external-agent-migration/src/rewrite.rs index e1558850ddad..4a4b2c0ae97d 100644 --- a/codex-rs/external-agent-migration/src/rewrite.rs +++ b/codex-rs/external-agent-migration/src/rewrite.rs @@ -65,7 +65,7 @@ fn replace_with_boundaries(input: &str, needle: &str, replacement: &str) -> Stri let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); - if boundary_before && boundary_after { + if boundary_before && boundary_after && !is_literal_reference_match(bytes, start, end) { output.push_str(&input[last_emitted..start]); output.push_str(replacement); last_emitted = end; @@ -104,7 +104,7 @@ fn replace_case_insensitive_with_boundaries( let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); - if boundary_before && boundary_after { + if boundary_before && boundary_after && !is_literal_reference_match(bytes, start, end) { output.push_str(&input[last_emitted..start]); output.push_str(replacement); last_emitted = end; @@ -121,6 +121,32 @@ fn replace_case_insensitive_with_boundaries( output } +/// Product names embedded in filesystem paths, URLs, or dotted identifiers are +/// literal references to the source tool, not prose that should be retargeted. +fn is_literal_reference_match(bytes: &[u8], start: usize, end: usize) -> bool { + if let Some(before) = start.checked_sub(1).and_then(|idx| bytes.get(idx)) + && matches!(*before, b'/' | b'\\' | b'.') + { + return true; + } + + let Some(after) = bytes.get(end) else { + return false; + }; + if matches!(*after, b'/' | b'\\') { + return true; + } + + *after == b'.' + && bytes + .get(end + 1) + .is_some_and(|byte| is_reference_suffix_byte(*byte)) +} + +fn is_reference_suffix_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') +} + fn is_word_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || byte == b'_' } diff --git a/codex-rs/external-agent-migration/src/rewrite_tests.rs b/codex-rs/external-agent-migration/src/rewrite_tests.rs index 8b103cb3e11c..55740f8e5830 100644 --- a/codex-rs/external-agent-migration/src/rewrite_tests.rs +++ b/codex-rs/external-agent-migration/src/rewrite_tests.rs @@ -4,6 +4,17 @@ use pretty_assertions::assert_eq; const PROFILE: RewriteProfile = RewriteProfile::new("SOURCE.md", &["source agent"]) .with_case_sensitive_term_variants(&["Source"]); +const CLAUDE_PROFILE: RewriteProfile = RewriteProfile::new( + "CLAUDE.md", + &[ + "claude code", + "claude-code", + "claude_code", + "claudecode", + "claude", + ], +); + #[test] fn rewrites_terms_only_at_word_boundaries() { assert_eq!( @@ -11,3 +22,47 @@ fn rewrites_terms_only_at_word_boundaries() { "AGENTS.md Codex Codex source_agent" ); } + +#[test] +fn preserves_source_specific_filesystem_references() { + assert_eq!( + PROFILE.rewrite( + "Read ./SOURCE.md, C:\\repo\\SOURCE.md, ~/.Source/config, and .Source before using Source." + ), + "Read ./SOURCE.md, C:\\repo\\SOURCE.md, ~/.Source/config, and .Source before using Codex." + ); +} + +#[test] +fn preserves_source_specific_urls_and_dotted_identifiers() { + assert_eq!( + PROFILE.rewrite( + "See https://Source.dev/docs, docs.Source.dev, and plugin.Source.config. Source." + ), + "See https://Source.dev/docs, docs.Source.dev, and plugin.Source.config. Codex." + ); +} + +#[test] +fn rewrites_standalone_doc_name_but_not_path_qualified_doc_name() { + assert_eq!( + PROFILE.rewrite("SOURCE.md lives beside ../SOURCE.md and $HOME/SOURCE.md."), + "AGENTS.md lives beside ../SOURCE.md and $HOME/SOURCE.md." + ); +} + +#[test] +fn preserves_claude_skill_paths_while_retargeting_prose() { + let input = concat!( + "Session transcripts live in ~/.claude/projects//.\n", + "See also .claude/commands/example.md and C:\\Users\\me\\.claude\\plans\\today.md.\n", + "Use Claude Code UI and read CLAUDE.md." + ); + let expected = concat!( + "Session transcripts live in ~/.claude/projects//.\n", + "See also .claude/commands/example.md and C:\\Users\\me\\.claude\\plans\\today.md.\n", + "Use Codex UI and read AGENTS.md." + ); + + assert_eq!(CLAUDE_PROFILE.rewrite(input), expected); +}