Skip to content

T-PEUC-verify-changes-base-prefers-local-ref

State: closed/done · Impact: medium · Complexity: small

sdlc verify changes --base <branch> silently diffs against origin/<branch> whenever that remote-tracking ref exists, even when the caller’s local <branch> is the one they mean. With an unpushed or rewritten local branch (a restacked sdlc/p2-loops-r, say) the judge was handed 1075 unrelated files, and nothing in the output named the ref it had actually used. Resolve a branch base so that a local branch ahead of its remote copy wins, and always report the ref that was diffed.

packages/ts/agent-dev/src/changeset.ts’s private baseRef(git, base) returns origin/${base} whenever git.objectExists('origin/' + base) is true, and base as written otherwise (commit-naming revs like HEAD, abc1234 or main~2 skip the lookup via NAMES_A_COMMIT). The rationale in its doc comment is sound for the stale-local-main case: a local main that is behind would make already-merged commits look like part of the change. It is wrong when local <branch> is ahead of, or has diverged from, origin/<branch>: the merge base with HEAD is then far older than the caller’s fork point, and every commit between them is judged.

forkPoint() feeds both changedFiles() and fileDiffs(). The resolved ref never leaves the module: solutions/ontological/lib/services/verify/ops/changes.ts reports out.base, which is the caller’s input string (or defaultBase()), so verify: judged N file(s) against sdlc/p2-loops-r prints even when origin/sdlc/p2-loops-r was used. The dfv CLI in packages/ts/agent-dev/src/cli.ts has the same blind spot.

A branch-name base resolves as follows: when both <branch> and origin/<branch> exist and the local ref is an ancestor of the remote one (local is behind or equal), use origin/<branch>, which keeps today’s stale-main protection. In every other case where the local ref exists (local ahead, or diverged), use the local <branch>. When only one of the two exists, use that one. Commit-naming revs are used as written, as today.

The resolved ref is part of the changeset result. verify changes reports it in its text output, JSON output and HTML report. dfv does the same.

  1. In packages/ts/agent-dev/src/changeset.ts, export a resolveBase(git: Git, base: string): string that implements the rule above. Test ancestry with the easy-git sync client’s git.isAncestor(base, 'origin/' + base). Rewrite baseRef() as resolveBase() and update its doc comment to state both halves of the rule.
  2. Have forkPoint() call resolveBase(). Leave the signatures of changedFiles() and fileDiffs() unchanged, so each caller resolves once and passes the resolved ref in.
  3. In solutions/ontological/lib/services/verify/ops/changes.ts, compute const resolved = resolveBase(git, base) after picking base, and pass resolved to changedFiles/fileDiffs. Add a resolvedBase: z.string() field to the output schema next to base. Render it wherever out.base is printed: when it differs from base, print against <base> (resolved to <resolvedBase>); otherwise print against <base>. Add the field to the report kind in lib/services/verify/reports/changes/.
  4. Apply the same resolve-once-and-print change in packages/ts/agent-dev/src/cli.ts, including the JSON payload’s base companion field.
  5. Extend packages/ts/agent-dev/src/changeset.test.ts (it already builds a bare origin plus a clone) with the local-ahead, local-behind and diverged cases, and add a verify changes --dry-run test that asserts the resolved ref appears in the output.
AreaNote
packages/ts/agent-dev/src/changeset.tsBase resolution rule, the new resolveBase export
packages/ts/agent-dev/src/cli.tsdfv resolves once and prints the resolved ref
packages/ts/agent-dev/src/index.tsRe-export resolveBase
solutions/ontological/lib/services/verify/verify changes output field, renderers, report kind
  • AC-1: In a clone where local feature is two commits ahead of a pushed origin/feature and HEAD is a topic branch with one commit on top of local feature, changedFiles(git, 'feature') returns only topic’s file plus uncommitted and untracked work. It does not return the files from feature’s two unpushed commits. A new test in changeset.test.ts covers this.
  • AC-2: In a clone where local main is behind origin/main, resolveBase(git, 'main') returns origin/main. A test asserts it, which keeps the stale-main protection.
  • AC-3: When local <branch> and origin/<branch> have diverged, resolveBase returns the local <branch>. A test asserts it.
  • AC-4: resolveBase(git, 'HEAD') and resolveBase(git, <sha>) return their input unchanged. The existing --base HEAD test still passes.
  • AC-5: sdlc verify changes --base <branch> --dry-run --json includes a resolvedBase field. The text output reads (resolved to origin/<branch>) whenever the resolved ref differs from --base. A test in lib/services/verify/ asserts both.
  • AC-6: bun test packages/ts/agent-dev solutions/ontological/lib/services/verify passes, and so does bunx tsc --noEmit.
  • Changing defaultBase(): resolving the remote’s default branch through origin/HEAD is unaffected.
  • A --remote/--local override flag. The ancestry rule removes the need; add a flag only if a real case surfaces.
  • Fetching before resolving. The resolution uses whatever refs are local, and verify changes stays offline.
  • none

Found while restacking the sdlc 1.0 campaign’s Phase 3 branch onto the rewritten sdlc/p2-loops-r (2026-09-24). The judge run against the local, unpushed branch diffed 1075 unrelated files because origin/sdlc/p2-loops-r pointed at a different history.


← Back to Tasks