Skip to content

fix #12: silent-empty output when --root is relative#13

Merged
oubiwann merged 2 commits into
mainfrom
fix/bug12-relative-root
May 7, 2026
Merged

fix #12: silent-empty output when --root is relative#13
oubiwann merged 2 commits into
mainfrom
fix/bug12-relative-root

Conversation

@oubiwann

@oubiwann oubiwann commented May 7, 2026

Copy link
Copy Markdown
Contributor

Closes #12.

What

cascade v0.1.0's documented invocation cascade --base=origin/main --head=HEAD (no --root, taking default .) silently emitted empty output and exited 0 against any real Go monorepo. The exact silent-zero-packages failure mode that drove the gta → cascade pivot.

Three-layer holistic fix (per plan-mode user decisions for defense-in-depth + diagnostic loudness):

  1. Librarypkg/changeset/changeset.go: absolutize cfg.moduleRoot via filepath.Abs before the dirMap lookup. Defensive at the library level — protects ANY current/future caller (deferred pkg/cascade.Run; third-party imports of pkg/changeset).
  2. CLIinternal/cli/cli.go: absolutize cfg.root once after parseFlags succeeds. Surgical fix; the four downstream consumers all receive a path absolutized exactly once.
  3. Diagnosticinternal/cli/cli.go: stderr breadcrumb when len(affected) == 0 and changedFiles contains .go-suffix entries. Filtered on .go so docs-only PRs (legitimate empty case) stay silent. Doesn't change exit code.

Where

Path Action Notes
pkg/changeset/changeset.go Modify filepath.Abs(cfg.moduleRoot) after the moduleRootSet check; updated WithModuleRoot godoc to document the new behaviour.
pkg/changeset/changeset_test.go Modify New TestResolve_RelativeModuleRoot_AbsolutizedInternally (subtests for . and "").
internal/cli/cli.go Modify filepath.Abs(cfg.root) after parseFlags; diagnostic-warning block in Run after runPipeline.
internal/cli/seam_test.go Modify 3 new tests: TestRun_DefaultRootResolvesCWD, TestRun_DiagnosticOnSuspiciousEmpty, TestRun_NoDiagnosticOnDocsOnlyEmpty.
README.md Modify --root flag row clarifies absolutization.
docs/dev/0013-bug12-fix-relative-root.md Create Closing retrospective.

Root cause (verified)

Location What goes wrong
internal/cli/cli.go:189 Flag default is the literal string ".".
internal/cli/cli.go:279 changeset.WithModuleRoot(cfg.root) hands the relative "." to the library.
pkg/changeset/changeset.go:120 dirMap[p.Dir] = p.ImportPath — keyed on absolute paths (per golist.Package.Dir's contract).
pkg/changeset/changeset.go:136 filepath.Join(".", "rel/path") returns relative — mismatch.
pkg/changeset/changeset.go:139 Lookup miss → no seeds → empty RevDepClosure([]) → empty stdout, exit 0.

The bug lived in a parameter-space cell that no test happened to hit (relative moduleRoot × absolute pkg.Dir). Both M4 and M5 had 100% statement coverage but the pairing was untested in either layer.

How to verify

# Reproduce the bug locally (against this branch — should pass post-fix):
make check-all                                   # 100% on all 5 packages, no contortions
go test -run TestResolve_RelativeModuleRoot ./pkg/changeset       # library-layer regression
go test -run TestRun_DefaultRootResolvesCWD ./internal/cli        # CLI-layer regression
go test -run 'TestRun_(Diagnostic|NoDiagnostic)' ./internal/cli   # diagnostic behaviour

# Smoke test against cascade-against-cascade with default --root
# (would have produced empty output on v0.1.0; produces 5-package affected-set on this branch):
go build -o /tmp/cascade-fixed ./cmd/cascade
echo "pkg/golist/golist.go" | /tmp/cascade-fixed --changed-files=-
# Expected: github.com/geomyidia/cascade/{cmd/cascade,internal/cli,pkg/{changeset,depgraph,golist}}

Tests added

Test Layer What it pins
TestResolve_RelativeModuleRoot_AbsolutizedInternally pkg/changeset Library-layer regression. Subtests for . and "". Pre-fix fails (empty); post-fix passes.
TestRun_DefaultRootResolvesCWD internal/cli End-to-end CLI integration — cli.Run with no --root flag against absolute pkg.Dir values. Pre-fix produced bug; post-fix produces affected-set.
TestRun_DiagnosticOnSuspiciousEmpty internal/cli Diagnostic stderr line fires when .go files in changedFiles + empty affected-set. Pins the count format too.
TestRun_NoDiagnosticOnDocsOnlyEmpty internal/cli Diagnostic does NOT fire on docs-only changesets. Pins the .go-suffix filter.

Notable findings

  • Behavioral-space gap as methodology lesson. M5 retro claimed "100% coverage on first try, no test contortions" — accurate for line coverage, but coverage didn't catch this bug. The bug lived in a parameter-space cell that no test happened to hit. Documented in the retrospective as a carry-forward: future tests for path-handling code should explicitly enumerate the relative-vs-absolute matrix.
  • CC self-check protocol gap exposed. The 5-for-5 zero-softpedals streak across M3/refactor/M4/M5/M6 is for textual softpedals (evidence-vs-criterion mismatches). Doesn't extend to behavioral-space gaps. Worth recording as a known limitation.
  • Five-seam pattern paid off for regression coverage. All 4 new tests drive the scenarios in-process via existing seams (runGitDiff, runGoListWrapper, signalContext); no real subprocess invocations.

Post-merge follow-ups

Checklist

  • make check passes locally
  • All 5 cascade packages at 100% coverage post-fix
  • New/changed exported symbols have godoc comments (only WithModuleRoot's doc-comment changed; updated to document the absolutize behaviour)
  • Tests added/updated for behaviour change (4 new tests across two packages)
  • No public-API breakage (the WithModuleRoot documented contract is refined, not changed: relative paths now resolve correctly instead of silently failing)
  • If this PR closes a milestone ledger — N/A (bug fix, not a milestone). Both pre-PR self-check + the retrospective walk the three-layer fix's evidence-vs-claim matching.

Breaking change?

No. The library-layer change refines WithModuleRoot's documented behaviour (relative paths now resolve correctly via filepath.Abs) without changing any contractual guarantee. Tests passing absolute paths see no observable change (absolutize-of-absolute is identity). The CLI-layer change makes --root=. (the default) work as documented; users who had been working around the bug by passing absolute --root see no observable change either.

🤖 Generated with Claude Code

oubiwann and others added 2 commits May 7, 2026 16:40
…ken)

Bug: cascade v0.1.0's documented invocation 'cascade --base=origin/main
--head=HEAD' (no --root, taking default '.') silently emitted empty
output and exited 0 against any real Go monorepo. The exact silent-
zero-packages failure mode that drove the gta → cascade pivot.

Root cause: changeset.Resolve does
  abs = filepath.Clean(filepath.Join(cfg.moduleRoot, file))
With cfg.moduleRoot="." and file="rel/path", filepath.Join returns a
relative result. filepath.Dir of that yields a relative parent, which
can't match the absolute keys in dirMap (built from golist.Package.Dir
values, which are documented absolute). Lookup miss → no seeds → empty
RevDepClosure → empty stdout, exit 0.

Fix at three layers (defense in depth, per plan-mode user decisions):

1. Library (pkg/changeset/changeset.go): absolutize cfg.moduleRoot via
   filepath.Abs before the dirMap lookup. Protects ANY current/future
   library consumer (the deferred pkg/cascade.Run; third-party imports
   of pkg/changeset) from the bug class. filepath.Abs("") and
   filepath.Abs(".") both resolve to cwd.

2. CLI (internal/cli/cli.go): absolutize cfg.root once after parseFlags
   succeeds, before validateConfig. The four downstream consumers
   (runGitDiff, classifyGitDiffError, golist.WithDir,
   changeset.WithModuleRoot) all receive a path absolutized exactly
   once.

3. Diagnostic (internal/cli/cli.go): when len(affected) == 0 and
   changedFiles contained .go-suffix entries, emit a stderr breadcrumb
     cascade: N changed Go file(s) did not resolve to any package; check --root
   Doesn't change exit code. Filtered on .go-suffix so docs-only PRs
   stay silent (legitimate empty case). Bug #12 would have surfaced in
   seconds during the original Banyan integration with this in place.

Regression coverage at three layers:
- pkg/changeset/changeset_test.go: TestResolve_RelativeModuleRoot_
  AbsolutizedInternally (subtests for "." and ""). Pre-fix this test
  fails with empty result; post-fix it passes.
- internal/cli/seam_test.go: TestRun_DefaultRootResolvesCWD. End-to-
  end CLI invocation with no --root flag (default ".") + relative
  changedFile + cwd-rooted absolute pkg.Dir values.
- internal/cli/seam_test.go: TestRun_DiagnosticOnSuspiciousEmpty +
  TestRun_NoDiagnosticOnDocsOnlyEmpty. Pin the diagnostic's .go-suffix
  filter behaviour; the second test is the explicit anti-regression
  for docs-only PRs.

All 5 cascade packages at 100% coverage post-fix. make check-all green.
Smoke test against the cascade repo itself (cascade-against-cascade
with default --root) yields the expected 5-package affected-set, where
v0.1.0 yielded empty.

Verification target post-tag: getBanyan/api PR #3474 (parked as the
cascade-fix verification PR). Once v0.1.1 ships, that PR's CI re-trigger
provides the real-codebase closure (M2 F-18 / M5 F-19 lineage).

Closes #12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README: --root flag row gains "Absolute or relative; cascade absolutizes
via filepath.Abs before use, so the default '.' resolves to the process
cwd." Documents the fix without committing to brittle prose about the
old bug.

Retrospective at docs/dev/0013-bug12-fix-relative-root.md walks the
three-layer fix shape, the four-test regression coverage, and the
methodology lesson the bug surfaced: 100% line coverage doesn't imply
behavioral-space coverage. The bug lived in a parameter-space cell
(relative moduleRoot × absolute pkg.Dir) that no test happened to hit.
Carry-forward into M7+: enumerate the parameter-space matrix explicitly
when designing tests for new APIs, especially path-handling code.

Verification post-merge:
- v0.1.1 tag + GH release (standard M6 flow; manual tag+push to
  bypass the Makefile's release-dry-run go.sum bug).
- getBanyan/api PR #3474 CI re-trigger against v0.1.1 (load-bearing
  real-codebase closure, M2 F-18 / M5 F-19 lineage).

Note: the retro mentions the WithModuleRoot godoc update was bundled
into commit 7f8d183 alongside the library fix; that's where the doc-
comment lives, so the doc change naturally rode with the impl change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oubiwann oubiwann merged commit e0c1477 into main May 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.1.0: silent empty output when --root is relative (default '.' is broken in real-world CI use)

1 participant