diff --git a/hubs/cli-git.md b/hubs/cli-git.md
index 4985b2f..86839f0 100644
--- a/hubs/cli-git.md
+++ b/hubs/cli-git.md
@@ -2,11 +2,13 @@
summary: Best-effort git queries for scoping and rename-following — advisory only, the gate never depends on them.
anchors:
- claim: >
- changed_files returns the repo-relative paths changed between the merge base of base..HEAD
- and the working tree, used to diff-scope the check. A missing merge base (shallow clone)
- falls back to diffing the ref directly; if git can't answer at all it returns None.
+ changed_files returns the workspace-root-relative paths changed between the merge base of
+ base..HEAD and the working tree (via git diff --relative), used to diff-scope the check —
+ so the set intersects workspace-relative anchors even when the workspace is a repo
+ subdirectory. A missing merge base (shallow clone) falls back to diffing the ref directly;
+ if git can't answer at all it returns None.
at: surf-cli/src/git.rs > changed_files
- hash: 9f422d548239
+ hash: 454e65cc8aa3
- claim: >
show returns the contents of a file at a git ref (git show :), used to recover
the previous source for advisory old_code/magnitude. None when unavailable — the verdict is
diff --git a/surf-cli/src/check.rs b/surf-cli/src/check.rs
index 8ca0bdb..99cb46d 100644
--- a/surf-cli/src/check.rs
+++ b/surf-cli/src/check.rs
@@ -733,6 +733,60 @@ mod tests {
assert!(unmatched.is_empty(), "glob matched an anchored file");
}
+ #[test]
+ fn base_scope_works_when_workspace_is_a_repo_subdir() {
+ // The workspace (surf.toml) sits in `proj/`, a subdirectory of the git repo. A real
+ // logic drift in an anchored file must still fail `check --base`. Regression for the
+ // silent bypass where `git diff` emitted repo-root-relative paths (`proj/src/m.rs`)
+ // that never matched the workspace-relative anchor (`src/m.rs`), scoping the gate to
+ // zero claims and exiting clean.
+ let tmp = tempfile::tempdir().unwrap();
+ let repo = tmp.path();
+ let proj = repo.join("proj");
+
+ let v1 = "pub fn add(a: i64, b: i64) -> i64 { a + b }\n";
+ let h = stored_hash(v1, "src/m.rs > add");
+ write(&proj, "surf.toml", "");
+ write(&proj, "src/m.rs", v1);
+ write(
+ &proj,
+ "hubs/a.md",
+ &format!("---\nsummary: x\nanchors:\n - claim: add sums\n at: src/m.rs > add\n hash: {h}\n---\n"),
+ );
+
+ // Initialize the repo at the parent, not the workspace.
+ git(repo, &["init", "-q"]);
+ git(
+ repo,
+ &["-c", "user.email=t@t", "-c", "user.name=t", "add", "."],
+ );
+ git(
+ repo,
+ &[
+ "-c",
+ "user.email=t@t",
+ "-c",
+ "user.name=t",
+ "commit",
+ "-q",
+ "-m",
+ "v1",
+ ],
+ );
+
+ // Diverge the anchored span in the working tree.
+ write(
+ &proj,
+ "src/m.rs",
+ "pub fn add(a: i64, b: i64) -> i64 { a - b }\n",
+ );
+
+ let ws = ws_at(proj.clone());
+ let scoped = check_workspace(&ws, Some("HEAD"), &[]).unwrap().0;
+ assert_eq!(scoped.len(), 1, "subdir --base must still catch the drift");
+ assert_eq!(scoped[0].kind, DivergenceKind::Changed);
+ }
+
#[test]
fn no_flags_checks_everything() {
let tmp = tempfile::tempdir().unwrap();
diff --git a/surf-cli/src/git.rs b/surf-cli/src/git.rs
index 7f5c4c0..9ec73cf 100644
--- a/surf-cli/src/git.rs
+++ b/surf-cli/src/git.rs
@@ -8,8 +8,13 @@ use std::path::Path;
use std::process::Command;
/// Files changed between the merge base of `base`..HEAD and the working tree. Paths are
-/// repo-root-relative; they match `Anchor.file` (workspace-root-relative) when the workspace
-/// root is the repo root, the normal case. `None` if git can't answer.
+/// emitted relative to `root` (the workspace root) via `--relative`, so they match
+/// `Anchor.file` (also workspace-root-relative) even when the workspace is a *subdirectory*
+/// of the git repo — without `--relative`, `git diff` reports repo-root-relative paths
+/// (e.g. `proj/src/x.rs`) that never intersect a workspace-relative anchor (`src/x.rs`),
+/// silently scoping the `--base` gate to zero claims and passing real drift (exit 0).
+/// `--relative` also drops changes outside the workspace, which can never be anchored anyway.
+/// `None` if git can't answer.
pub fn changed_files(root: &Path, base: &str) -> Option> {
let merge_base = Command::new("git")
.current_dir(root)
@@ -23,7 +28,7 @@ pub fn changed_files(root: &Path, base: &str) -> Option> {
let output = Command::new("git")
.current_dir(root)
- .args(["diff", "--name-only", &merge_base])
+ .args(["diff", "--name-only", "--relative", &merge_base])
.output()
.ok()?;
output.status.success().then(|| {