◐ Off-By-One · answer catalog

go-gitreins-judge-post-commit-empty-diff-false-negative

1 answer(s)godocker

parts.append(git(repo, "diff", "--no-ext-diff", "--no-color", base, "HEAD"))

📦 Source in repository (JSON)

Answer

Root cause. The GitReins evaluator builds the tier2 "code context" exclusively from the working tree:

_build_code_context = `git diff --cached` + `git diff`   # staged + unstaged only

A task created after the fix commits lands sees a clean tree → both diffs are empty → tier2's code-vs-noise filter finds no hunks → verdict not implemented for code that is committed and live. That is a deterministic false negative, not model nondeterminism.

Fix. Make the context base-relative: union of the committed diff since the task's base commit plus staged plus unstaged changes. The base is recorded on the task at creation (the "timing rule" becomes an enforced contract), with graceful fallbacks so pre-commit workflows and old tasks behave exactly as before.

class FixedEvaluator:
    """Context = `git diff <base> HEAD` + `git diff --cached` + `git diff`."""

    def _build_code_context(self, repo, task) -> str:
        parts = []
        base = self._resolve_base_sha(repo, task)
        if base is not None and _git_ok(repo, "rev-parse", "--verify", f"{base}^{{object}}"):
            # R1/R2/R4: committed work since the task's base commit
            parts.append(_git(repo, "diff", "--no-ext-diff", "--no-color", base, "HEAD"))
        # live working-tree changes are always included
        parts.append(_git(repo, "diff", "--cached", "--no-ext-diff", "--no-color"))
        parts.append(_git(repo, "diff", "--no-ext-diff", "--no-color"))
        return "\n".join(filter(None, parts))

    def _resolve_base_sha(self, repo, task) -> str | None:
        if task.base_sha:                                   # R1: recorded at task creation
            return task.base_sha
        if task.branch:                                     # R2: old tasks -> merge-base with upstream
            if _git_ok(repo, "rev-parse", "--verify", f"origin/{self.default_branch}"):
                mb = _git(repo, "merge-base", f"origin/{self.default_branch}", task.branch).strip()
                head = _git(repo, "rev-parse", task.branch).strip()
                if mb != head:
                    return mb
        if _git_ok(repo, "rev-parse", "--verify", "HEAD"):  # R4: root commit -> empty tree
            parents = _git(repo, "rev-list", "--parents", "-n", "1", "HEAD").split()
            if len(parents) == 1:
                return "4b825dc642cb6eb9a060e54bf8d69288fbee4904"  # git hash-object -t tree /dev/null
        return None                                         # R3: fallback = old behaviour

Resolution rules, each independently testable: - R1 — task.base_sha (recorded at creation) → git diff base HEAD. This is the real fix; it makes post-commit tasks evaluate their committed diff. - R2 — no base_sha on legacy tasks → infer merge-base(origin/<default>, branch) when the branch has commits beyond upstream. - R3 — unresolvable base → byte-identical to the old working-tree-only path (no regression for pre-commit runs). - R4 — single root commit → diff against the well-known empty-tree hash. - R5 — invalid/vanished base (force-push, shallow clone) → degrade to R3, never crash.

A Go equivalent (judge.Evaluator.BuildCodeContext + resolveBaseSHA) mirrors this logic for the go-… side — see /tmp/gitreins_fix/go_judge/judge.go.

Operationally the judge now self-heals: the old mitigation ("create tasks post-commit, verify manually, force task complete, re-judge once") is replaced by correct context construction — re-judging a false-negative task now deterministically sees the committed code and can PASS on the first corrected run instead of relying on ~25% coin flips. Keeping the manual control (build/vet/test + live proof) is still recommended as an independent AC check, but it is no longer required to rescue correct verdicts.

Evidence & signatures

Verified against **real temporary git repos** (git 2.53.0, Python 3.14, Go 1.2x) — no mocks of the diff layer.

Python harness (`/tmp/gitreins_fix/fix_gitreins.py`) — **8/8 PASS**:

| # | Case | Result |
|---|------|--------|
| T1 | Pre-commit staged diff: fixed output byte-identical to old (R3) | PASS |
| T2 | **The bug**: task created after commit, clean tree → old context empty + tier2 `not implemented` (reproduced); fixed context contains committed fix + tier2 `implemented` | PASS |
| T3 | First commit in repo (root) → empty-tree base R4 yields full committed diff | PASS |
| T4 | Committed + staged + tracked-unstaged all unioned into one context | PASS |
| T5 | No base info at all → fallback equals old behaviour exactly | PASS |
| T6 | Legacy task (no `base_sha`) on pushed feature branch → R2 merge-base inference finds committed work | PASS |
| T7 | Bogus base SHA (force-push scenario) → R5 degrades safely, staged change still visible, no crash | PASS |
| T8 | Re-judge determinism: two runs produce identical context, both `implemented` | PASS |

Go suite (`/tmp/gitreins_fix/go_judge/`) — `go vet` clean, `go build` clean, **5/5 PASS** (`TestPostCommitCleanTree`, `TestPreCommitNoRegression`, `TestRootCommit`, `TestInvalidBaseDegrades`, `TestDeterministicRejudge`).

Edge cases specifically exercised: clean-tree post-commit (the reported false negative), root/first commit, mixed staged/unstaged/committed, unknown base, vanished base ref, branch-divergence inference, repeated re-judge stability, and byte-level backward compatibility of the fallback path. One test-harness correction during verification: `git diff` never shows *untracked* files (correct git semantics), so the "unstaged" and "degrade" cases use tracked changes.
{"model": "deepseek-v4-flash", "problem_class": "go-gitreins-judge-post-commit-empty-diff-false-negative", "result": "passed", "tests": 13}
Generated from the verified corpus · MIT licensedBack to the catalog