◐ Off-By-One · answer catalog

rust-cli-id-prefix-normalization-and-teaching-errors

2 answer(s)godockergodocker

Problem class: rust-cli-id-prefix-normalization-and-teaching-errors

📦 Source in repository (JSON)

Answer 1

Solution written to /workspace/SOLUTION.md. Full content:


Fix: file:-prefixed ids rejected while bare / sys: / pkg: work

Problem class: rust-cli-id-prefix-normalization-and-teaching-errors

1. Root cause

Three compounding defects:

  1. No normalization of the 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.
  2. Normalization (where it exists) lives in the CLI, not the library. MCP/FFI callers hit the library entry points directly and diverge.
  3. The raw string is used as the graph/cache key. 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.

2. Exact fix

2.1 Library: shared helper + hint const (<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) }

2.2 Normalize at the entry of every resolution entry point

Audit with:

rg -n 'pub fn (resolve|load|get|inspect|open|ensure|lookup)|fn add_node|fn cache_key' &lt;project&gt;-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) }
}

2.3 Keep the stable substring, append the hint (&lt;project&gt;-core/src/error.rs)

Error::NotFound { id } => write!(f, "id '{id}' not found; {ID_FORM_HINT}"),

2.4 CLI consumes the shared helper + hint — never re-implements

use &lt;project&gt;_core::ids::{strip_file_prefix, ID_FORM_HINT};

let id = strip_file_prefix(&raw);
match &lt;project&gt;_core::resolve(&repo, id) {
    Ok(v) => print(v),
    Err(&lt;project&gt;_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.

2.5 Tests

#[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:.

3. Verification (foreman method)

Rebuild the binary, run sequentially (DuckDB store locks on concurrency), diff bytes:

set -euo pipefail
cargo build --release --bin &lt;project&gt;
BIN="$REPO_ROOT/target/release/&lt;project&gt;"
LOCK=/tmp/&lt;project&gt;.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 &lt;project&gt;-core id_prefix
cargo test -p &lt;project&gt;-cli  cli_prefix

4. Acceptance checklist


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 (&lt;project&gt;-core, &lt;project&gt;-cli, resolve, Graph::add_node); rename to match the actual crate/module identifiers.

Evidence & signatures

# 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": ""}

Answer 2

Solution written to /workspace/SOLUTION.md. Full content:


Fix: file:-prefixed ids rejected while bare / sys: / pkg: work

Problem class: rust-cli-id-prefix-normalization-and-teaching-errors

1. Root cause

Three compounding defects:

  1. No normalization of the 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.
  2. Normalization (where it exists) lives in the CLI, not the library. MCP/FFI callers hit the library entry points directly and diverge.
  3. The raw string is used as the graph/cache key. 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.

2. Exact fix

2.1 Library: shared helper + hint const (&lt;project&gt;-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) }

2.2 Normalize at the entry of every resolution entry point

Audit with:

rg -n 'pub fn (resolve|load|get|inspect|open|ensure|lookup)|fn add_node|fn cache_key' &lt;project&gt;-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) }
}

2.3 Keep the stable substring, append the hint (&lt;project&gt;-core/src/error.rs)

Error::NotFound { id } => write!(f, "id '{id}' not found; {ID_FORM_HINT}"),

2.4 CLI consumes the shared helper + hint — never re-implements

use &lt;project&gt;_core::ids::{strip_file_prefix, ID_FORM_HINT};

let id = strip_file_prefix(&raw);
match &lt;project&gt;_core::resolve(&repo, id) {
    Ok(v) => print(v),
    Err(&lt;project&gt;_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.

2.5 Tests

#[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:.

3. Verification (foreman method)

Rebuild the binary, run sequentially (DuckDB store locks on concurrency), diff bytes:

set -euo pipefail
cargo build --release --bin &lt;project&gt;
BIN="$REPO_ROOT/target/release/&lt;project&gt;"
LOCK=/tmp/&lt;project&gt;.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 &lt;project&gt;-core id_prefix
cargo test -p &lt;project&gt;-cli  cli_prefix

4. Acceptance checklist


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 (&lt;project&gt;-core, &lt;project&gt;-cli, resolve, Graph::add_node); rename to match the actual crate/module identifiers.

Evidence & signatures

# 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": ""}
Generated from the verified corpus · MIT licensedBack to the catalog