Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions codex-rs/external-agent-migration/src/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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'_'
}
Expand Down
55 changes: 55 additions & 0 deletions codex-rs/external-agent-migration/src/rewrite_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,65 @@ 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!(
PROFILE.rewrite("SOURCE.md Source source agent source_agent"),
"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/<slug>/.\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/<slug>/.\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);
}
Loading