Skip to content

feat(cli): recce check-base + MCP startup freshness warning (M2)#1353

Merged
even-wei merged 6 commits intomainfrom
spacedock-ensign/cascade-003-pre-pr-one-sentence
May 8, 2026
Merged

feat(cli): recce check-base + MCP startup freshness warning (M2)#1353
even-wei merged 6 commits intomainfrom
spacedock-ensign/cascade-003-pre-pr-one-sentence

Conversation

@even-wei
Copy link
Copy Markdown
Contributor

@even-wei even-wei commented May 5, 2026

Summary

  • Adds recce check-base subcommand to cli.py; returns JSON with status (FRESH | STALE_TIME | STALE_SHA | MISSING), recommendation, and human-readable message.
  • Adds check_base_freshness() helper (48 h mtime threshold + optional DBT_GIT_SHA manifest comparison; STALE_SHA is best-effort — no raise when env var absent).
  • MCP server startup calls check_base_freshness() after load_context(); emits [Warning] Base artifacts are stale to stderr for STALE_TIME/STALE_SHA (AC-3).
  • Stores base_status on server object; exposed via _tool_get_server_info().
  • 5 tests in tests/test_check_base.py: FRESH, STALE_TIME, STALE_SHA, MISSING, SHA-absent-no-raise — all pass.

Test plan

  • uv run pytest tests/test_check_base.py -v

Cascade self-review evidence

Triplet review at every stage. Mechanical correctness verified:

  • 5 unit tests pass: FRESH, STALE_TIME, STALE_SHA, MISSING, SHA-absent-no-raise (R9 best-effort)
  • recce check-base Click command registered (grep-confirmed)
  • check_base_freshness() helper called from mcp_server.py startup after load_context() (grep-confirmed)
  • get_server_info exposes base_status field
  • Reviewer traces in spacedock-ops/docs/cascade/003-pre-pr-one-sentence/_trace/: design / build / verify / deliver all PASS first-try

Human reviewer must check (cascade can't verify these)

# Concern What to look for
1 metadata.env.DBT_GIT_SHA adapter consistency Captain decided best-effort (R9). Across dbt-postgres, dbt-snowflake, dbt-bigquery, dbt-databricks etc., is this field populated identically when DBT_GIT_SHA env var is set? If adapter divergence exists, the STALE_SHA branch may fire inconsistently.
2 48-h freshness threshold default Hard-coded default. Is 48 h appropriate for the typical dbt project workflow? Some teams iterate hourly; others ship weekly. Should this be configurable in a recce.yml or dbt_project.yml-relative file, not just --freshness-threshold-hours per invocation?
3 CLI output JSON schema as public contract Once recce check-base ships, the JSON output keys (status, recommendation, message, artifact_age_hours, base_sha, current_sha, threshold_hours) become a public contract that downstream agents parse. Forward-compat: if a future field is added, will agents tolerate it? Should keys be marked deprecated/removed via a versioning convention?
4 base_status enum stability `"fresh"

References

Resolves recce-pre-pr-summary-in-one-sentence-claude-code-and-codex-2fd8dfb5866e (M2)

Paired PR: DataRecce/recce-claude-plugin#26 (M1, M4) — must merge after this one.

🤖 Generated with cascade workflow (triplet review at every stage: design / build / verify / deliver — all PASS).

…(M2)

Adds `recce check-base` CLI subcommand with statuses FRESH / STALE_TIME /
STALE_SHA / MISSING and --format json|text output. Exports
check_base_freshness() as a module-level helper so mcp_server.py can call
it at startup without duplicating logic (R8: cli.py-primary split).

feat(mcp): emit stale-base warning at startup; expose base_status (M2, AC-3)

Calls check_base_freshness() after load_context() in run_mcp_server() and
prints a [Warning] line to stderr when status is STALE_TIME or STALE_SHA.
Adds base_status field to get_server_info tool response so agents can
programmatically detect stale state without parsing stderr.

test: cover FRESH/STALE_TIME/STALE_SHA/MISSING + best-effort SHA absent (R9)

5 unit tests in tests/test_check_base.py; all pass. The absent-field test
(test_sha_absent_no_raise) confirms DBT_GIT_SHA absence falls through to
FRESH without raising.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: even-wei <evenwei@infuseai.io>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 97.31801% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
recce/mcp_server.py 77.27% 5 Missing ⚠️
recce/cli.py 98.05% 2 Missing ⚠️
Files with missing lines Coverage Δ
tests/test_check_base.py 100.00% <100.00%> (ø)
recce/cli.py 68.74% <98.05%> (+1.56%) ⬆️
recce/mcp_server.py 91.19% <77.27%> (-0.38%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@even-wei even-wei requested a review from wcchang1115 May 6, 2026 04:10
@wcchang1115
Copy link
Copy Markdown
Collaborator

wcchang1115 commented May 6, 2026

Updated Review — PR #1353 (round 3)

Re-reviewed b50c5b34 against the round-2 finding and did a fresh pass on the newly-added code. Round-2 finding: resolved. Fresh pass: no new BLOCKERs / ISSUEs; two NOTEs.

Verification on the pushed SHA: python -m pytest tests/test_check_base.py -v15 passed; make flake8 → clean.

Resolution of round-2 finding

Round-2 finding Status Evidence
[Warning] MCP startup freshness check ignores --project-dir Resolved New shared helper resolve_target_base_path() in cli.py:2947. CLI's check_base (cli.py:3101) and MCP startup (mcp_server.py:2618-2629) now route through one helper, so the join logic cannot drift. Locked in by 4 new tests, including a regression test that builds a real manifest under tmp_path/my_dbt_project/target-base/ and asserts the helper returns the correct path.

The chosen approach — extracting a shared helper rather than duplicating the inline join — is the right call. It eliminates the very class of drift that produced the round-2 finding (CLI fixed, MCP missed).

Fresh pass on b50c5b34

Validation

Pass Result Notes
A — Correctness & logic PASS Helper handles absolute / relative / None project_dir correctly. Smoke-tested: resolve_target_base_path(None, "target-base") == "target-base" (relative, resolved against CWD downstream — equivalent to pre-fix CLI behaviour).
C — Cross-reference PASS Import from recce.cli import check_base_freshness, resolve_target_base_path at mcp_server.py:2618; argument order matches helper signature; --target-base-path Click default ("target-base") makes the runtime None case impossible.
D — Error handling PASS Helper is pure (no I/O, cannot raise on documented input). MCP try/except wrapper unchanged; startup is still best-effort.
E — Test quality PASS 4 new tests cover relative / absolute / None / end-to-end-like paths. See NOTE 2.
F — Diff specifics PASS (with NOTE) Only two call sites of the inline join existed; both migrated. No stale callers. See NOTE 1.
G — Performance N/A Pure path operation.
H — Async/concurrency PASS Helper is sync and pure; safe in async context. The sync check_base_freshness file I/O inside async def run_mcp_server is pre-existing and gated to one-time startup.

Notes

  1. Diff is dominated by autoformatter cosmetic changes unrelated to the fix. recce/cli.py shows -445 lines and recce/mcp_server.py -259 in the stat, but the actual MCP fix is ~30 lines (helper + call-site + import). The remainder is line-collapsing where main's style was reapplied after 12fc92d0 (merge from main) preserved the branch's wrapped form. Behaviour-preserving but it inflates the review surface for a focused fix and complicates targeted reverts. Future fix-up commits should isolate format normalization into a separate commit so the actual fix can be reviewed without scrolling past hundreds of cosmetic changes.

  2. test_resolve_mcp_startup_finds_artifacts_under_project_dir does not actually exercise MCP startup. It calls the helper directly and runs check_base_freshness on the result. If a future change reverts mcp_server.py to the inline Path("./") join (i.e. drops the helper), this test would still pass — it would not catch the very regression its name advertises. Either rename to test_helper_resolves_under_project_dir to match what's verified, or upgrade the body to invoke run_mcp_server against a fake context and assert on server._base_status / log output.

Verdict: GO

The functional fix is small, correct, well-tested, and addresses the round-2 finding cleanly. Both NOTEs are non-blocking — the first is a hygiene observation about commit shape, the second is a test-naming / test-strength suggestion that doesn't affect runtime behaviour.

Copy link
Copy Markdown
Collaborator

@wcchang1115 wcchang1115 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review: Critical issues found. See review comment for details.

Critical:
- base_status enum: standardize on all-lowercase ("fresh" / "stale_time" /
  "stale_sha" / "missing" / "single_env" / "unknown"). Document the full
  enum in get_server_info MCP tool description so LLM agents have a
  stable contract.
- check-base: honor --project-dir (and DBT_PROJECT_DIR envvar) like every
  other dbt-aware command. Resolves target-base-path relative to
  project-dir unless absolute.

Warnings:
- MCP startup now also warns on `missing` (not just stale_*), with a
  distinct rebuild-path message; `print(..., file=sys.stderr)` swapped
  for `logger.warning` to match the rest of mcp_server.py.
- check-base exit codes split: 0 fresh, 1 missing, 2 stale_*. Documented
  in the docstring so shell automation can branch without parsing JSON.
- Suggested-fix messages now interpolate target_base_path into the
  --target-path flag (no more hardcoded "target-base" misleading users
  on non-default paths).
- Add 6 CliRunner tests covering JSON schema, text rendering, exit-code
  mapping per status, and --project-dir resolution.

Nitpicks:
- Drop unused target_path parameter from check_base_freshness().
- Bare `except Exception:` now logs at debug for diagnosability.

Refs: #1353

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: even-wei <evenwei@infuseai.io>
@even-wei
Copy link
Copy Markdown
Contributor Author

even-wei commented May 7, 2026

@wcchang1115 Thanks for the thorough review — every finding was actionable and is now addressed in ef1fa711. Per-item rundown:

Critical

base_status enum casing — Standardized on all-lowercase (fresh / stale_time / stale_sha / missing / single_env / unknown). Lowercase matches the recommendation field's casing. The full enum is now documented in the get_server_info MCP tool description so LLM agents have the contract spelled out, not just inferred from a runtime sample. (recce/cli.py:3120, recce/mcp_server.py:738-746, recce/mcp_server.py:2173-2174)

check-base ignores --project-dir — Added --project-dir with envvar="DBT_PROJECT_DIR". target-base-path is now resolved via project_dir / target_base_path unless an absolute path is supplied. Added test_cli_project_dir_resolves to lock it in. (recce/cli.py:3204-3260)

Warnings

MCP startup asymmetry on missing — Real oversight. missing now emits a distinct logger.warning(...) at startup with the rebuild-path message (dbt build --target-path {tb}). Also swapped the bare print(..., file=sys.stderr) for logger.warning(...) to match the rest of mcp_server.py. (recce/mcp_server.py:2747-2776)

Exit code conflation — Split into three classes: 0=fresh, 1=missing (rebuild), 2=stale_time/stale_sha (regenerate). Documented in the check_base docstring; covered by test_cli_exit_code_{fresh,missing,stale_time}. (recce/cli.py:3236-3296)

No CliRunner tests — Added 6 CLI tests against the check_base Click command: JSON schema, text rendering, all three exit codes, and --project-dir resolution. (tests/test_check_base.py:120-251)

Hardcoded target-base in suggested-fix messages — All three messages in check_base_freshness() and the MCP startup warning now interpolate the actual target_base_path into the --target-path flag. test_status_missing asserts the user-supplied path appears in the message. (recce/cli.py:3146, :3164, :3186, recce/mcp_server.py:2755, :2766)

Nitpicks

  • Unused target_path parameter — Dropped from check_base_freshness() signature. Call sites updated. (recce/cli.py:3111-3115, recce/mcp_server.py:2752)
  • Bare except Exception: — Now except Exception as e: with logger.debug(f"check_base_freshness: SHA check skipped ({e})") — recoverable from debug logs without polluting normal output. (recce/cli.py:3192-3195)
  • print(..., file=sys.stderr) vs logger.warning — Fixed; see "MCP startup asymmetry" above.
  • Function-local import json / import time — Kept the function-local imports to match the established convention in cli.py (e.g., cll_init at L353-357 imports json, logging, time locally; json is not imported at module scope despite the review note). The import logging for the new _logger follows the same pattern.

Verification on the pushed SHA: uv run pytest tests/test_check_base.py tests/test_mcp_server.py -q → 126 passed; uv run flake8 recce/cli.py recce/mcp_server.py tests/test_check_base.py → clean.

…ade-003-pre-pr-one-sentence

Signed-off-by: even-wei <evenwei@infuseai.io>

# Conflicts:
#	recce/cli.py
@even-wei even-wei requested a review from wcchang1115 May 7, 2026 08:18
Copy link
Copy Markdown
Collaborator

@wcchang1115 wcchang1115 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review (updated): All prior critical and warning findings have been resolved in ef1fa71. Tests pass (11/11), lint clean. See updated review comment for evidence per finding.

Copy link
Copy Markdown
Collaborator

@wcchang1115 wcchang1115 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review (round 2): Fresh pass on the new code surfaced one real bug — MCP startup freshness check ignores --project-dir (parallel to the CLI bug, just at a different call site). See updated review comment for details and a drop-in fix.

Round-2 review finding: cli.py was fixed to join --project-dir onto a
relative --target-base-path, but the parallel call site in mcp_server.py
startup was not updated. So `recce mcp-server --project-dir /foo/bar`
looks for ./target-base/manifest.json relative to CWD instead of
/foo/bar/target-base/manifest.json — spurious "missing" warnings, or
worse, silently picking up a stale manifest from another project that
happens to live in CWD.

Extracted resolve_target_base_path() next to check_base_freshness in
cli.py so the join logic lives in exactly one place. CLI's check_base
and MCP's run_mcp_server startup both call the helper, and the
resolution can no longer drift across the two call sites.

- cli.py: new helper resolve_target_base_path(); check_base uses it
- mcp_server.py: startup freshness check uses the helper, joining
  kwargs["project_dir"] with kwargs["target_base_path"]
- tests/test_check_base.py: 4 new tests
  - test_resolve_relative_joins_with_project_dir
  - test_resolve_absolute_bypasses_project_dir
  - test_resolve_no_project_dir_uses_cwd
  - test_resolve_mcp_startup_finds_artifacts_under_project_dir
    (mirrors test_cli_project_dir_resolves against the helper)

Refs round-2 review:
#1353 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: even-wei <evenwei@infuseai.io>
@even-wei
Copy link
Copy Markdown
Contributor Author

even-wei commented May 8, 2026

@wcchang1115 Round-2 finding addressed in b50c5b34.

What changed

Extracted resolve_target_base_path(project_dir, target_base_path) next to check_base_freshness in recce/cli.py and routed both call sites through it. Picked the helper over inline mirroring (your "either way" suggestion) precisely because the bug was a parallel-call-site drift — having two copies invited the next missed call site too. Now there is exactly one place that knows how to resolve target-base.

def resolve_target_base_path(project_dir, target_base_path):
    base = Path(target_base_path)
    if base.is_absolute():
        return str(base)
    return str(Path(project_dir or "./") / base)
  • recce/cli.py: check_base (CLI) replaces its inline join with resolve_target_base_path(project_dir, target_base_path).
  • recce/mcp_server.py: startup freshness check now does resolve_target_base_path(kwargs.get("project_dir"), kwargs.get("target_base_path", "target-base")) before calling check_base_freshness. Lazy-imported alongside check_base_freshness to keep the existing circular-import guard.

Tests

Mirrored test_cli_project_dir_resolves against the helper. New cases in tests/test_check_base.py:

Test Asserts
test_resolve_relative_joins_with_project_dir ("/foo/bar", "target-base") → /foo/bar/target-base
test_resolve_absolute_bypasses_project_dir ("/foo/bar", "/tmp/abs/target-base") → /tmp/abs/target-base
test_resolve_no_project_dir_uses_cwd (None, "target-base") → ./target-base (semantics, CWD-independent)
test_resolve_mcp_startup_finds_artifacts_under_project_dir end-to-end: builds a fresh manifest under tmp_path/my_dbt_project/target-base/, calls the helper + check_base_freshness, expects status: fresh

The MCP-startup test mirrors test_cli_project_dir_resolves against the shared helper rather than booting run_mcp_server (the helper is the unit; the call site is just one line). If you'd prefer an end-to-end MCP-startup assertion that drives run_mcp_server directly, happy to add one — just say the word.

Verification

  • uv run pytest tests/test_check_base.py -v15 passed (11 prior + 4 new)
  • uv run pytest tests/test_check_base.py tests/test_cli.py tests/test_mcp_server.py151 passed
  • make format && make flake8 → clean

Format churn note

make format (Black + isort) collapsed a number of multi-line statements that fit on one line, so the diff stat is large despite the code change being small. All non-helper diffs in recce/cli.py and recce/mcp_server.py are pure formatting — verifiable with git show --stat HEAD showing the actual changed regions vs git diff -w showing the substantive logic. Same Black pass that ran in round 1, just propagated to the now-touched neighbors.

Cross-referenced "things that turned out fine"

Confirmed your other-things-checked items are all still good after this change:

  • Corrupt manifest fallthrough — unchanged.
  • current_commit_hash() returns None — unchanged.
  • Future-mtime / negative age — unchanged.
  • MCP kwargs.get("target_base_path", "target-base") default vs CLI default="target-base" — both still match; helper's default behavior preserves this.
  • Lazy from recce.cli import … — extended to import the helper too; same circular-import-guard pattern.

Asking for one more pass when convenient.

Copy link
Copy Markdown
Collaborator

@wcchang1115 wcchang1115 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-3 review: round-2 finding resolved by b50c5b34. No BLOCKERs or ISSUEs on the fresh pass. Two non-blocking NOTEs in the review comment (diff bloated by unrelated autoformatter changes; the new "MCP startup" regression test doesn't actually exercise MCP startup). LGTM.

@even-wei even-wei merged commit a4b7877 into main May 8, 2026
22 checks passed
@even-wei even-wei deleted the spacedock-ensign/cascade-003-pre-pr-one-sentence branch May 8, 2026 09:15
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.

2 participants