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.
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- In
packages/ts/agent-dev/src/changeset.ts, export aresolveBase(git: Git, base: string): stringthat implements the rule above. Test ancestry with the easy-git sync client’sgit.isAncestor(base, 'origin/' + base). RewritebaseRef()asresolveBase()and update its doc comment to state both halves of the rule. - Have
forkPoint()callresolveBase(). Leave the signatures ofchangedFiles()andfileDiffs()unchanged, so each caller resolves once and passes the resolved ref in. - In
solutions/ontological/lib/services/verify/ops/changes.ts, computeconst resolved = resolveBase(git, base)after pickingbase, and passresolvedtochangedFiles/fileDiffs. Add aresolvedBase: z.string()field to the output schema next tobase. Render it whereverout.baseis printed: when it differs frombase, printagainst <base> (resolved to <resolvedBase>); otherwise printagainst <base>. Add the field to the report kind inlib/services/verify/reports/changes/. - Apply the same resolve-once-and-print change in
packages/ts/agent-dev/src/cli.ts, including the JSON payload’sbasecompanion field. - 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 averify changes --dry-runtest that asserts the resolved ref appears in the output.
| Area | Note |
|---|---|
packages/ts/agent-dev/src/changeset.ts | Base resolution rule, the new resolveBase export |
packages/ts/agent-dev/src/cli.ts | dfv resolves once and prints the resolved ref |
packages/ts/agent-dev/src/index.ts | Re-export resolveBase |
solutions/ontological/lib/services/verify/ | verify changes output field, renderers, report kind |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: In a clone where local
featureis two commits ahead of a pushedorigin/featureandHEADis atopicbranch with one commit on top of localfeature,changedFiles(git, 'feature')returns onlytopic’s file plus uncommitted and untracked work. It does not return the files fromfeature’s two unpushed commits. A new test inchangeset.test.tscovers this. - AC-2: In a clone where local
mainis behindorigin/main,resolveBase(git, 'main')returnsorigin/main. A test asserts it, which keeps the stale-mainprotection. - AC-3: When local
<branch>andorigin/<branch>have diverged,resolveBasereturns the local<branch>. A test asserts it. - AC-4:
resolveBase(git, 'HEAD')andresolveBase(git, <sha>)return their input unchanged. The existing--base HEADtest still passes. - AC-5:
sdlc verify changes --base <branch> --dry-run --jsonincludes aresolvedBasefield. The text output reads(resolved to origin/<branch>)whenever the resolved ref differs from--base. A test inlib/services/verify/asserts both. - AC-6:
bun test packages/ts/agent-dev solutions/ontological/lib/services/verifypasses, and so doesbunx tsc --noEmit.
Out of scope
Section titled “Out of scope”- Changing
defaultBase(): resolving the remote’s default branch throughorigin/HEADis unaffected. - A
--remote/--localoverride 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 changesstays offline.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”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.