Plan: Devstack Staleness Guard
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Create |
Done (2026-04-09) — 350+ lines: RebuildType, VolumeAction, SeedAction enums, StalenessReport, check_staleness(), write_markers(), auto_refresh() |
2 |
Wire |
Done (2026-04-09) — Action::Refresh in dev.rs, auto_refresh() wired, status shows staleness summary |
3 |
Wire |
Done (2026-04-09) — auto-refresh before integration tests |
4 |
Add |
Done (2026-04-09) — .devstack/ directory exists and is ignored |
5 |
Update documentation (local-dev.md, CLAUDE.md, CHANGELOG.adoc, developer-guide.adoc) |
Done (2026-04-09) — cargo xtask dev refresh documented in CLAUDE.md |
Issues: #301
Branch: feature/devstack-staleness-guard
Context
Running cargo xtask test or cargo xtask e2e after modifying Rust code tests against stale container binaries that have not been rebuilt.
This causes false test failures and wastes significant debugging time.
Today, the only options are manual dev reload (preserves volumes, always rebuilds) or dev restart (wipes everything, always rebuilds with --no-cache).
Both do a full rebuild regardless of what actually changed.
There is no mechanism to detect whether containers are current, whether volumes need wiping, or whether seed data needs refreshing.
The goal is to make xtask automatically detect what changed since the last successful build and perform the minimum action needed — skip Docker entirely when nothing changed, do a cached rebuild when only source changed, use --no-cache only when Cargo.toml/Cargo.lock/Dockerfile changed, wipe volumes only when existing migrations were modified or deleted, and re-seed only when seed source changed or volumes were wiped.
Scope
In scope:
-
SHA-256 marker files in
.devstack/tracking source, deps, Dockerfile, compose, infra, rulesets, static assets, migrations, and seed source -
Mtime fast path to skip hashing when nothing has been modified (filesystem stat only)
-
Migration add-vs-modify detection via manifest file (distinguishes additive migrations from modified/deleted ones)
-
auto_refresh()function that executes the minimum Docker action based on three independent dimensions -
cargo xtask dev refreshsubcommand for manual auto-detection without running tests -
Staleness summary in
cargo xtask dev status -
--no-refreshflag ontestande2ecommands -
Documentation updates across local-dev.md, CLAUDE.md, CHANGELOG.adoc, developer-guide.adoc
Out of scope:
-
Selective per-service rebuild (single Dockerfile builds all binaries; per-service granularity requires Dockerfile restructuring)
-
Docker BuildKit cargo cache mounts (separate optimization, orthogonal to staleness detection)
-
Changes to CI pipeline (CI builds fresh every time;
--no-refreshflag available if needed)
Design
Staleness detection uses three independent dimensions whose results combine into the minimum action.
Dimension 1: Rebuild Type
| What changed | Type | Docker action |
|---|---|---|
Nothing image-related |
|
Skip Docker entirely |
|
|
|
Rulesets, |
|
|
|
|
|
New migration files (additive only) |
|
|
Modified/deleted existing migration |
|
Same (volume wipe handled separately) |
|
|
|
Dimension 2: Volume State
| What changed | Action | Data impact |
|---|---|---|
Nothing migration-related |
|
Volumes untouched |
New migration files (additive) |
|
|
Modified/deleted existing migration |
|
|
Dimension 3: Seed State
| What changed | Action |
|---|---|
Nothing seed-related |
|
|
|
|
|
Volumes were just wiped |
|
Combined Action Sequence
1. If volume_wipe → docker compose down -v
2. If rebuild=NoCache → docker compose build --no-cache → up -d
If rebuild=Cached → docker compose up -d --build
If rebuild=None && volume_wipe → docker compose up -d (restart for migrations)
3. Wait for health
4. Write updated markers
5. If reseed → cargo xtask seed
Key: a modified migration with unchanged Cargo.toml/Dockerfile only does down -v + up -d --build (cached rebuild), NOT --no-cache.
Marker Files
Stored in .devstack/ at workspace root (added to .gitignore).
| File | Contents | Tracks |
|---|---|---|
|
SHA-256 hex |
|
|
SHA-256 hex |
|
|
SHA-256 hex |
|
|
SHA-256 hex |
|
|
SHA-256 hex |
|
|
SHA-256 hex |
|
|
SHA-256 hex |
BFF static assets |
|
SHA-256 hex |
All |
|
Sorted paths |
Migration file inventory (add vs modify detection) |
|
SHA-256 hex |
|
|
String |
Which deployment profile was started |
|
Boolean string |
Whether |
Profile and Flag Mismatch Detection
The profile and shared-db markers are not just informational — they are part of the staleness check.
If the current invocation’s profile or shared-db flag differs from the stored marker, the guard must treat this as a compose-level change (equivalent to docker-compose.yml change): down the old profile, up the new one.
This prevents running tests against containers started with --profile snap-only when the test expects --profile full.
dev refresh reads profile and shared-db from existing markers and reuses them — no explicit flags required.
Only dev start requires explicit flags.
If markers don’t exist and no flags are provided, dev refresh errors with: "no devstack markers found — run cargo xtask dev start first."
Hash computation: walk matching files recursively, sort by relative path (normalized to forward slashes), feed relative_path\0content into a single SHA-256 digest.
Performance note: on a warm filesystem cache this completes in <1s for the full workspace.
On cold cache, consider using git ls-files -s tree hashes as an optimization in a future pass — git already has content hashes indexed.
For now, direct file reads are simpler and correct.
Mtime fast path: before computing hashes, check filesystem mtimes against marker files.
If any tracked file is newer than the marker, that dimension is definitely stale (skip hashing, go straight to rebuild).
If no tracked file is newer, still hash — mtime is unreliable as a freshness proof.
Git operations (pull, checkout, rebase) can set all file mtimes to the checkout time, making unchanged files appear newer or equal.
NTFS mtime resolution is fine (100ns), but git’s behavior means "same or older mtime" does NOT guarantee "unchanged content."
The mtime check is a one-directional fast path: it can prove staleness early, but it cannot prove freshness.
Migration Modification Detection
Compare current migration file list against migrations.manifest.
If the current list is a strict superset of stored (all stored paths present with unchanged content, just new files added), it is additive — volumes Keep.
If any stored path is missing or its content differs, volumes Wipe.
Steps
Step 1: Create devstack_guard.rs
Files: xtask/src/devstack_guard.rs
Create the core staleness detection module with:
-
RebuildTypeenum (None,Cached,NoCache) -
VolumeActionenum (Keep,Wipe) -
SeedActionenum (Skip,Reseed) -
StalenessReportstruct combining all three dimensions plus human-readable reasons -
check_staleness() → Result<StalenessReport>— reads markers, computes current hashes, compares -
write_markers() → Result<()>— writes all marker files after successful dev start/reload/restart -
auto_refresh(project) → Result<StalenessReport>— reads profile/shared-db from stored markers, checks staleness, executes minimum action -
markers_exist() → bool— checks whether.devstack/markers are present (devstack has been started at least once) -
Internal helpers:
hash_files(),marker_dir(),read_marker(),write_marker(),check_migration_modifications(),mtime_fast_path()
Reuse docker::workspace_root() from xtask/src/docker.rs for path resolution.
Use sha2::Sha256 (already a workspace dependency) for hashing.
Step 2: Wire into dev.rs
Files: xtask/src/cmd/dev.rs, xtask/src/main.rs
-
Add
mod devstack_guard;tomain.rs -
Call
devstack_guard::write_markers()afterdo_start()succeeds inStart,Reload, andRestartarms -
Clear
.devstack/markers beforeRestartstarts (so stale markers don’t persist if start fails) -
Add
Refreshvariant toActionenum (no flags — reads profile and shared-db from stored markers) — callsauto_refresh()without running tests -
Error if markers don’t exist: "no devstack markers found — run
cargo xtask dev startfirst" -
In
Statusarm, appendcheck_staleness()summary afterdocker compose psoutput
Step 3: Wire into test.rs and e2e.rs
Files: xtask/src/cmd/test.rs, xtask/src/cmd/e2e.rs
-
Add
--no-refreshflag (bool) to bothArgsstructs -
In
test.rs: before integration tests run (not for--unit), callauto_refresh()unless--no-refresh -
In
e2e.rs: callauto_refresh()at top ofrun()unless--no-refresh; subsumes existing "ensure devstack is running" logic
Step 5: Documentation updates
Files: .claude/docs/local-dev.md, .claude/CLAUDE.md, CHANGELOG.adoc, docs/modules/ROOT/pages/developer-guide.adoc
-
local-dev.md: adddev refreshto Standard Commands; add staleness guard explanation; update Testing section; document--no-refreshflag -
CLAUDE.md: update Build & Test section withdev refresh; mention staleness guard in Conventions -
CHANGELOG.adoc: entry under== Unreleased→=== Added -
developer-guide.adoc: adddev refreshto Common Tasks; update Running tests section
Files Touched
| File | Change |
|---|---|
|
Create — core staleness detection, hashing, markers, auto-refresh (~280 lines) |
|
Add |
|
Add |
|
Add |
|
Add |
|
Add |
|
Document |
|
Update Build & Test and Conventions sections |
|
Add entry under Unreleased |
|
Add |
Verification
-
cargo nextest run --workspace --lib— unit tests pass -
cargo xtask dev start --shared-db— markers written to.devstack/ -
cargo xtask dev status— shows staleness report (all up to date) -
Edit a
.rsfile, runcargo xtask dev status— showsSTALEfor source -
cargo xtask dev refresh— reads profile/shared-db from markers, performs cached rebuild, markers updated -
cargo xtask test --unit— no staleness check, tests pass -
cargo xtask test --no-refresh— skips staleness check -
Modify existing migration, run
cargo xtask dev status— shows volume wipe needed + cached rebuild (NOT--no-cache) -
cargo xtask e2e— auto-refreshes and re-seeds if needed -
cargo xtask dev refresh(without priordev start) — errors with "no devstack markers found" -
git pullthencargo xtask dev status— still detects staleness even though mtimes may be equal (hash-based, not mtime-only) -
Change
jurisdiction.toml, runcargo xtask dev status— shows both rulesets stale AND seed stale -
Start with
--profile snap-only, thencargo xtask dev status --profile full— detects profile mismatch
Documentation Updates
-
.claude/docs/local-dev.md—dev refresh, staleness guard,--no-refresh -
.claude/CLAUDE.md— Build & Test, Conventions -
CHANGELOG.adoc— entry under== Unreleased -
docs/modules/ROOT/pages/developer-guide.adoc—dev refresh, test auto-refresh