v0.4.11 — latent drift fix (range-diff sweep + distinct counters)#17
Conversation
Fixes a class of "invisible drift" where decisions silently went stale because link_commit only swept files in HEAD's own diff. After a gap of N commits without a bicameral invocation, drift introduced by intermediate commits stayed hidden until someone happened to re-edit the same files. Two changes: 1. Range-diff sweep. ingest_commit now diffs last_synced..HEAD via `git diff --name-only` and sweeps every file in the range. New sweep_scope field on LinkCommitResponse reports head_only (first sync, or fallback) vs range_diff (default after first sync) vs range_truncated (range exceeded 200-file cap). Falls back to head-only when the base SHA is unreachable (force-push, shallow clone). New range_size field reports how many files were swept. 2. Distinct intent counters. decisions_drifted and decisions_reflected now count distinct intent_ids that flipped, not (region, intent) pairs. Witnessed on the Accountable demo where one Google Calendar decision flipped 4 regions and the old counter reported decisions_drifted=4 while only 1 distinct intent was actually drifted. Fix: dedupe by intent_id via sets; counters now match what users mentally expect from "how many decisions just changed status." Both changes were teed up in conversation after the Phase 2 drift demo on Accountable surfaced the per-region count discrepancy and the latent-drift mental model. Tests: 11 cases in tests/test_v0411_latent_drift.py covering the range-diff helper directly (in-range files, empty range, unreachable base), the sweep_scope semantics (first sync = head_only, second sync after gap = range_diff, fast-path = head_only with size 0, unreachable cursor = head_only fallback), and the distinct-intent counter dedup. Full v0.4.11 regression: 163 passed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe pull request extends the git synchronization system to sweep files across the full Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Handler as handle_link_commit
participant Adapter as SurrealDBLedgerAdapter
participant GitStatus as ledger/status
participant Git as git (subprocess)
Client->>Handler: link_commit(commit_hash)
Handler->>Adapter: ingest_commit(commit_hash)
Adapter->>Adapter: read last_synced_commit from sync state
alt Range cursor exists & not empty
Adapter->>GitStatus: get_changed_files_in_range(last_synced, commit_hash)
GitStatus->>Git: git diff --name-only last_synced..commit_hash
alt git diff succeeds
Git-->>GitStatus: file list
GitStatus->>GitStatus: check file count vs 200 limit
alt file count <= 200
GitStatus-->>Adapter: file list (sweep_scope=range_diff)
else file count > 200
GitStatus-->>Adapter: truncated list (sweep_scope=range_truncated)
end
else git diff fails/times out
Git-->>GitStatus: error
GitStatus-->>Adapter: None (sentinel)
Adapter->>GitStatus: get_changed_files(commit_hash) [fallback]
GitStatus->>Git: git show --name-only commit_hash
Git-->>GitStatus: file list
GitStatus-->>Adapter: file list (sweep_scope=head_only)
end
else First sync or empty cursor
Adapter->>GitStatus: get_changed_files(commit_hash)
GitStatus->>Git: git show --name-only commit_hash
Git-->>GitStatus: file list
GitStatus-->>Adapter: file list (sweep_scope=head_only)
end
Adapter->>Adapter: process files & compute decision flips (by intent_id dedup)
Adapter->>Adapter: construct LinkCommitResponse with sweep_scope & range_size
Adapter-->>Handler: response payload
Handler->>Handler: populate sweep_scope & range_size on LinkCommitResponse
Handler-->>Client: LinkCommitResponse
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…attempt GATE TRIBUNAL entry for plan-48-pre-push-drift-hook.md. Verdict: PASS first-attempt — no remediation cycle needed. Chain hash bf890347 extends from BicameralAI#16 (BicameralAI#44 SEAL, 567170e0) on dev. Three non-blocking observations recorded: - O1: run_setup parameter-name cosmetic nit (functionally fine). - O2: latent post-commit-hook bug — bicameral-mcp link_commit HEAD is not a registered subcommand. Recommend separate issue. Out of scope for BicameralAI#48. - O3: two-renderer modules (cli/drift_report.py for PR comments vs cli/branch_scan.py for terminal hooks) accepted as parallel non-duplication; different output formats and exit semantics. SG-PLAN-GROUNDING-DRIFT instance BicameralAI#4 prevented — first plan this session where author-side grounding mitigation worked rather than audit-side catching. Issue BicameralAI#114 (CI lint enforcement) remains the durable countermeasure. Plan PASS at 79abcc2; chain to /qor-implement.
Phase 0 — branch-scan CLI subcommand: - cli/branch_scan.py (177 LOC): pure-function render_terminal_summary + main() CLI entry. Lazy-imports handle_link_commit; graceful skip when no ~/.bicameral/ledger.db. Exit codes 0/1/2 documented in module header. - server.py: add `branch-scan` subparser to cli_main, dispatch to cli.branch_scan:main. - tests/test_branch_scan_cli.py (144 LOC, 7 tests): renderer shape + exit-code matrix (no drift, drift+block-env, drift+non-TTY, etc.) Phase 1 — setup_wizard pre-push hook: - setup_wizard.py: new _GIT_PRE_PUSH_HOOK constant + _install_git_pre_push_hook function modeled on _install_git_post_commit_hook. Idempotent install with append-on-existing-hook-without-bicameral semantics. - run_setup() gains keyword-only `with_push_hook: bool = False`. Setup wizard step 7b conditionally installs the hook. - server.py setup_parser gains `--with-push-hook` flag. - tests/test_setup_pre_push_hook.py (92 LOC, 5 tests): fresh-install, idempotent re-install, append-when-non-bicameral, no-git-root path, executable-bit (POSIX-only). Phase 2 — docs: - CHANGELOG.md [Unreleased] entry - docs/guides/pre-push-drift-hook.md (129 LOC): What/When/Quickstart/ Reference/Pitfalls/See-also user guide Validation: - 11 new tests + 8 BicameralAI#49 regression = 19/20 green (1 Windows-only chmod test skipped on this platform) - ruff check + format: clean on all 5 changed/new source files - mypy on cli/branch_scan.py: no issues - End-to-end smoke: `bicameral-mcp branch-scan` dispatches via cli_main → cli.branch_scan.main → graceful skip with exit 0 (no ledger) Razor: cli/branch_scan.py 177 LOC (≤250); all entry funcs ≤25 LOC; helpers ≤20 LOC; nesting ≤2; zero nested ternaries. Maintainer-locked design (audit-PASS at META_LEDGER BicameralAI#17, chain bf890347): Q1 cli/ placement, Q2 deliberate non-modeling on possibly- broken post-commit-hook predecessor, Q3 HEAD-only v1, Q4-Q6 TTY/skip behaviors all per plan. Audit's separate-issue recommendation (post-commit hook command not registered as CLI subcommand) NOT addressed in this PR — out of scope. Closes BicameralAI#48 Plan: plan-48-pre-push-drift-hook.md (audit PASS, chain hash bf890347). Implementation chains to seal in /qor-substantiate.
Substantiation seal for plan-48-pre-push-drift-hook.md (Issue BicameralAI#48, audit PASS at META_LEDGER BicameralAI#17, chain hash bf890347). Verification gates (10 of 12 passed; 2 advisory skipped per capability shortfalls): - Reality vs Promise: ✓ all 4 new + 3 modified files exist; zero plan deviations - Test audit: 27/28 (11 new + 16 regression on PR BicameralAI#113 drift_report tests; 1 chmod test skipped on Windows non-POSIX) - Razor final: cli/branch_scan.py 177 LOC (≤250); entry funcs ≤25; helpers ≤20; nesting ≤2; zero nested ternaries - Skill file integrity: N/A (no MCP tool changes) - SYSTEM_STATE.md synced - Merkle seal computed: eacc6f89f707ce958fa2485177c9706808fdfeb32 - Step 4.6 reliability sweep: skipped (qor/reliability/ absent) - Step 7.5 version bump: skipped (per maintainer direction) This is the first implementation in the session with ZERO plan deviations — plan was thorough enough that implementation was direct. Pairs with the prior turn's first-attempt audit PASS (Entry BicameralAI#17). Both ends of the QOR cycle clean. Audit's separate-issue recommendation (post-commit hook command not a registered subcommand) tracked but NOT addressed in this PR — separate workstream. Chain: 18 entries on this branch; integrity VALID. Next: /qor-document.
Summary
Fixes a class of "invisible drift" where decisions silently went stale because `link_commit` only swept files in HEAD's own diff. After a gap of N commits without a bicameral invocation, drift introduced by intermediate commits stayed hidden until someone happened to re-edit the same files.
Two changes:
Range-diff sweep. `ingest_commit` now diffs `last_synced..HEAD` via `git diff --name-only` and sweeps every file in the range. New `sweep_scope` field on `LinkCommitResponse` reports `head_only` (first sync, or fallback) vs `range_diff` (default after first sync) vs `range_truncated` (range exceeded 200-file cap). Falls back to head-only when the base SHA is unreachable. New `range_size` field reports how many files were swept.
Distinct intent counters. `decisions_drifted` and `decisions_reflected` now count distinct intent_ids that flipped, not (region, intent) pairs. Witnessed on the Accountable demo where one Google Calendar decision flipped 4 regions and the old counter reported `decisions_drifted=4` while only 1 distinct intent was actually drifted.
Both changes were teed up in conversation after the Phase 2 drift demo on Accountable surfaced the per-region count discrepancy and the latent-drift mental model.
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests