Problem class: rust-cli-id-prefix-normalization-and-teaching-errors
Solution written to /workspace/SOLUTION.md. Full content:
file:-prefixed ids rejected while bare / sys: / pkg: workProblem class: rust-cli-id-prefix-normalization-and-teaching-errors
Three compounding defects:
file: prefix. The resolver branches on sys:/pkg: but the bare-path fallback treats the entire raw string as a repo-relative path. It tries to open a file literally named file:src/lib.rs, which doesn't exist → a file-not-found-sounding error. The prefix is a legal alias never translated to its canonical bare form.file:src/lib.rs interns as its own node, distinct from src/lib.rs.Rule that fixes all three: normalize once, in the library, at the first line of every resolution entry point, before guards/cache lookup/graph insertion.
<project>-core/src/ids.rs)pub const SYS_PREFIX: &str = "sys:";
pub const PKG_PREFIX: &str = "pkg:";
pub const FILE_PREFIX: &str = "file:";
pub const ID_FORM_HINT: &str =
"accepted id forms: <repo-relative-path>, sys:<name>, pkg:<name>, file:<repo-relative-path>";
/// The ONLY source of truth for prefix normalization. Call at the top of every
/// resolution entry point, before guards and before cache/graph keying.
#[inline]
pub fn strip_file_prefix(id: &str) -> &str {
id.strip_prefix(FILE_PREFIX).unwrap_or(id)
}
#[inline]
pub fn normalize_id(id: &str) -> &str { strip_file_prefix(id) }
Audit with:
rg -n 'pub fn (resolve|load|get|inspect|open|ensure|lookup)|fn add_node|fn cache_key' <project>-core/src
pub fn resolve(repo: &Repo, raw_id: &str) -> Result<ResolvedId, Error> {
let id = strip_file_prefix(raw_id); // FIRST line, before all guards
if let Some(name) = id.strip_prefix(SYS_PREFIX) { return resolve_sys(repo, name); }
if let Some(name) = id.strip_prefix(PKG_PREFIX) { return resolve_pkg(repo, name); }
resolve_path(repo, id)
}
// same first-line call in load(), inspect(), open(), ensure(), lookup()...
Defense in depth at the graph/cache boundary:
impl Graph {
pub fn add_node(&mut self, raw_id: &str) -> NodeId {
let id = strip_file_prefix(raw_id);
self.intern(id)
}
fn cache_key(raw_id: &str) -> &str { strip_file_prefix(raw_id) }
}
<project>-core/src/error.rs)Error::NotFound { id } => write!(f, "id '{id}' not found; {ID_FORM_HINT}"),
use <project>_core::ids::{strip_file_prefix, ID_FORM_HINT};
let id = strip_file_prefix(&raw);
match <project>_core::resolve(&repo, id) {
Ok(v) => print(v),
Err(<project>_core::Error::NotFound { .. }) => {
eprintln!("error: id '{id}' not found; {ID_FORM_HINT}");
std::process::exit(1);
}
Err(e) => { eprintln!("error: {e}"); std::process::exit(1); }
}
MCP/FFI need no change once 2.1–2.2 land — that's why the helper is pub in the library.
#[test]
fn prefixed_and_bare_resolve_to_equal_result() {
let repo = fixture_repo();
assert_eq!(resolve(&repo, "src/lib.rs").unwrap(),
resolve(&repo, "file:src/lib.rs").unwrap());
}
#[test]
fn prefixed_id_is_not_its_own_graph_node() {
let mut g = Graph::new();
let a = g.add_node("src/lib.rs");
let b = g.add_node("file:src/lib.rs");
assert_eq!(a, b);
assert_eq!(g.node_count(), 1);
assert!(g.node_by_name("file:src/lib.rs").is_none());
}
#[test]
fn unknown_id_keeps_stable_substring_and_appends_hint() {
let msg = resolve(&fixture_repo(), "file:does/not/exist").unwrap_err().to_string();
assert!(msg.contains("not found"));
assert!(msg.contains("accepted id forms"));
}
Plus a process-level CLI test asserting byte-identical stdout/stderr for bare vs file:.
Rebuild the binary, run sequentially (DuckDB store locks on concurrency), diff bytes:
set -euo pipefail
cargo build --release --bin <project>
BIN="$REPO_ROOT/target/release/<project>"
LOCK=/tmp/<project>.verify.lock
flock "$LOCK" "$BIN" cmd src/lib.rs > /tmp/bare.out 2> /tmp/bare.err
flock "$LOCK" "$BIN" cmd file:src/lib.rs > /tmp/file.out 2> /tmp/file.err
diff -u /tmp/bare.out /tmp/file.out
diff -u /tmp/bare.err /tmp/file.err
cmp /tmp/bare.out /tmp/file.out && echo "PASS: stdout byte-identical"
cmp /tmp/bare.err /tmp/file.err && echo "PASS: stderr byte-identical"
set +e; flock "$LOCK" "$BIN" cmd file:does/not/exist > /tmp/unk.out 2>&1; set -e
grep -F "not found" /tmp/unk.out && grep -F "accepted id forms" /tmp/unk.out
"$BIN" graph nodes | grep -Fx "file:src/lib.rs" && { echo "FAIL: phantom node"; exit 1; } || true
"$BIN" graph nodes | grep -Fx "src/lib.rs" >/dev/null
cargo test -p <project>-core id_prefix
cargo test -p <project>-cli cli_prefix
strip_file_prefix is pub in the library, sole prefix-normalizing code.ID_FORM_HINT.pub const ID_FORM_HINT shared by library Display and CLI bail sites.== bare result test; no phantom graph node test.diff/cmp byte-identical.flock).Note: this environment has no repository and no Rust toolchain (/workspace holds only problem.json; cargo/rustc absent), so the build/diff verification commands above are the runnable proof to execute in the real repo — they are written to be copy-paste ready. The code structure maps 1:1 onto the named symbols (<project>-core, <project>-cli, resolve, Graph::add_node); rename to match the actual crate/module identifiers.
# Evidence - Problem class: rust-cli-id-prefix-normalization-and-teaching-errors - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T02:34:30.235Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Hilo DF-WARPFS-2: a CLI accepted bare repo-relative paths, sys: and pkg: id forms but rejected a file:-prefixed path with a file-not-found-sounding error. Fix pattern: (1) one pub shared strip_file_prefix helper in the LIBRARY crate (not the CLI) so MCP/FFI callers normalize identically; (2) normalize at the entry of EVERY resolution entry point before the guards, so downstream cache/parse paths see the bare form; (3) keep the pre-existing stable error substring (other suites assert it) and APPEND a hint naming all accepted id forms; (4) a pub const hint string shared by library + CLI bail sites; (5) tests assert prefixed and bare produce EQUAL results and that the prefixed form does not become its own graph node. Foreman verification that beat the worker self-report: rebuild the binary and diff `cli cmd <barepath>` vs `cli cmd file:<barepath>` output \u2014 byte-identical diffs are the real proof; concurrent invocations against the same DuckDB-backed store fail with a lock error, so run them sequentially.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "rust-cli-id-prefix-normalization-and-teaching-errors", "provider": "openrouter", "solved_at": "2026-09-20T02:34:30.236Z", "version": ""}Solution written to /workspace/SOLUTION.md. Full content:
file:-prefixed ids rejected while bare / sys: / pkg: workProblem class: rust-cli-id-prefix-normalization-and-teaching-errors
Three compounding defects:
file: prefix. The resolver branches on sys:/pkg: but the bare-path fallback treats the entire raw string as a repo-relative path. It tries to open a file literally named file:src/lib.rs, which doesn't exist → a file-not-found-sounding error. The prefix is a legal alias never translated to its canonical bare form.file:src/lib.rs interns as its own node, distinct from src/lib.rs.Rule that fixes all three: normalize once, in the library, at the first line of every resolution entry point, before guards/cache lookup/graph insertion.
<project>-core/src/ids.rs)pub const SYS_PREFIX: &str = "sys:";
pub const PKG_PREFIX: &str = "pkg:";
pub const FILE_PREFIX: &str = "file:";
pub const ID_FORM_HINT: &str =
"accepted id forms: <repo-relative-path>, sys:<name>, pkg:<name>, file:<repo-relative-path>";
/// The ONLY source of truth for prefix normalization. Call at the top of every
/// resolution entry point, before guards and before cache/graph keying.
#[inline]
pub fn strip_file_prefix(id: &str) -> &str {
id.strip_prefix(FILE_PREFIX).unwrap_or(id)
}
#[inline]
pub fn normalize_id(id: &str) -> &str { strip_file_prefix(id) }
Audit with:
rg -n 'pub fn (resolve|load|get|inspect|open|ensure|lookup)|fn add_node|fn cache_key' <project>-core/src
pub fn resolve(repo: &Repo, raw_id: &str) -> Result<ResolvedId, Error> {
let id = strip_file_prefix(raw_id); // FIRST line, before all guards
if let Some(name) = id.strip_prefix(SYS_PREFIX) { return resolve_sys(repo, name); }
if let Some(name) = id.strip_prefix(PKG_PREFIX) { return resolve_pkg(repo, name); }
resolve_path(repo, id)
}
// same first-line call in load(), inspect(), open(), ensure(), lookup()...
Defense in depth at the graph/cache boundary:
impl Graph {
pub fn add_node(&mut self, raw_id: &str) -> NodeId {
let id = strip_file_prefix(raw_id);
self.intern(id)
}
fn cache_key(raw_id: &str) -> &str { strip_file_prefix(raw_id) }
}
<project>-core/src/error.rs)Error::NotFound { id } => write!(f, "id '{id}' not found; {ID_FORM_HINT}"),
use <project>_core::ids::{strip_file_prefix, ID_FORM_HINT};
let id = strip_file_prefix(&raw);
match <project>_core::resolve(&repo, id) {
Ok(v) => print(v),
Err(<project>_core::Error::NotFound { .. }) => {
eprintln!("error: id '{id}' not found; {ID_FORM_HINT}");
std::process::exit(1);
}
Err(e) => { eprintln!("error: {e}"); std::process::exit(1); }
}
MCP/FFI need no change once 2.1–2.2 land — that's why the helper is pub in the library.
#[test]
fn prefixed_and_bare_resolve_to_equal_result() {
let repo = fixture_repo();
assert_eq!(resolve(&repo, "src/lib.rs").unwrap(),
resolve(&repo, "file:src/lib.rs").unwrap());
}
#[test]
fn prefixed_id_is_not_its_own_graph_node() {
let mut g = Graph::new();
let a = g.add_node("src/lib.rs");
let b = g.add_node("file:src/lib.rs");
assert_eq!(a, b);
assert_eq!(g.node_count(), 1);
assert!(g.node_by_name("file:src/lib.rs").is_none());
}
#[test]
fn unknown_id_keeps_stable_substring_and_appends_hint() {
let msg = resolve(&fixture_repo(), "file:does/not/exist").unwrap_err().to_string();
assert!(msg.contains("not found"));
assert!(msg.contains("accepted id forms"));
}
Plus a process-level CLI test asserting byte-identical stdout/stderr for bare vs file:.
Rebuild the binary, run sequentially (DuckDB store locks on concurrency), diff bytes:
set -euo pipefail
cargo build --release --bin <project>
BIN="$REPO_ROOT/target/release/<project>"
LOCK=/tmp/<project>.verify.lock
flock "$LOCK" "$BIN" cmd src/lib.rs > /tmp/bare.out 2> /tmp/bare.err
flock "$LOCK" "$BIN" cmd file:src/lib.rs > /tmp/file.out 2> /tmp/file.err
diff -u /tmp/bare.out /tmp/file.out
diff -u /tmp/bare.err /tmp/file.err
cmp /tmp/bare.out /tmp/file.out && echo "PASS: stdout byte-identical"
cmp /tmp/bare.err /tmp/file.err && echo "PASS: stderr byte-identical"
set +e; flock "$LOCK" "$BIN" cmd file:does/not/exist > /tmp/unk.out 2>&1; set -e
grep -F "not found" /tmp/unk.out && grep -F "accepted id forms" /tmp/unk.out
"$BIN" graph nodes | grep -Fx "file:src/lib.rs" && { echo "FAIL: phantom node"; exit 1; } || true
"$BIN" graph nodes | grep -Fx "src/lib.rs" >/dev/null
cargo test -p <project>-core id_prefix
cargo test -p <project>-cli cli_prefix
strip_file_prefix is pub in the library, sole prefix-normalizing code.ID_FORM_HINT.pub const ID_FORM_HINT shared by library Display and CLI bail sites.== bare result test; no phantom graph node test.diff/cmp byte-identical.flock).Note: this environment has no repository and no Rust toolchain (/workspace holds only problem.json; cargo/rustc absent), so the build/diff verification commands above are the runnable proof to execute in the real repo — they are written to be copy-paste ready. The code structure maps 1:1 onto the named symbols (<project>-core, <project>-cli, resolve, Graph::add_node); rename to match the actual crate/module identifiers.
# Evidence - Problem class: rust-cli-id-prefix-normalization-and-teaching-errors - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T02:34:30.235Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Hilo DF-WARPFS-2: a CLI accepted bare repo-relative paths, sys: and pkg: id forms but rejected a file:-prefixed path with a file-not-found-sounding error. Fix pattern: (1) one pub shared strip_file_prefix helper in the LIBRARY crate (not the CLI) so MCP/FFI callers normalize identically; (2) normalize at the entry of EVERY resolution entry point before the guards, so downstream cache/parse paths see the bare form; (3) keep the pre-existing stable error substring (other suites assert it) and APPEND a hint naming all accepted id forms; (4) a pub const hint string shared by library + CLI bail sites; (5) tests assert prefixed and bare produce EQUAL results and that the prefixed form does not become its own graph node. Foreman verification that beat the worker self-report: rebuild the binary and diff `cli cmd <barepath>` vs `cli cmd file:<barepath>` output \u2014 byte-identical diffs are the real proof; concurrent invocations against the same DuckDB-backed store fail with a lock error, so run them sequentially.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "rust-cli-id-prefix-normalization-and-teaching-errors", "provider": "openrouter", "solved_at": "2026-09-20T02:34:30.236Z", "version": ""}