Skip to content

feat(get-affected-actors): three-dot diff, narrowed per Actor with --since - #204

Merged
metalwarrior665 merged 8 commits into
masterfrom
claude/three-dot-diff
Oct 26, 2026
Merged

metalwarrior665 merged 8 commits into
masterfrom
claude/three-dot-diff

Conversation

@metalwarrior665

@metalwarrior665 metalwarrior665 commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

The core logic seems correct and clear improvement. Keeping in draft for further polish.

This is #196's "Solution 2" implemented on top of the simplified get-affected-actors, so we can discuss a concrete diff. It changes behaviour compared to master. The changes are listed below.

What it does

  • --base: inspects git diff $(git merge-base base HEAD) HEAD, which is the same as base...HEAD and GitHub's "Files changed" tab. That is what the branch changes, not what it merged in from base.
  • --base and --since: also classifies since..HEAD, which is everything since the last validation, including what came in through a merge or a rebase. It then keeps only the Actors that changed in both diffs. An Actor counts as functional only when both diffs say functional. On the question left open in Diff files logic refactor #196, that means taking the least impactful classification; taking the most impactful would re-test scenario A on every push.
  • --since alone (releases): unchanged.
  • Rerun and missing --since: when --since is HEAD (a rerun) or isn't in the local history (a force-push), it is ignored and the whole branch is checked.

What goes away

  • hasMergeFromTarget, getBranchOnlyChangedFiles and getOwnChangesIfMergedFromBase, together with the merge-from-target log messages and the per-Actor TODO, which this resolves.
  • Change detection now takes a { fromRef, toRef } range instead of a commit list. The cosmetic .actor JSON check therefore compares the right endpoints for each diff, which also resolves its "validate this is the right commit range" TODO.

bin/: 93 insertions, 132 deletions. With tests and README: 318 insertions, 410 deletions.

Behaviour on a scratch repo

I ran this PR's getAffectedActors on a scratch repo where Actors a and c share src/ and b is isolated. The "Before" column is the code in #203, which matches master.

Scenario Before This PR
Branch change already validated, then merged a master that only changed README, CHANGELOG and .vscode – – (a, c cosmetic)
Validated foo, then merged a master bar in the same shared context a, c a, c
Branch's own commits only change a README, but the merge commit itself edits src – (missed) a, c
Branch changes src, then merged a master that only changed b a, c, b a, c
Rebased onto a master that only changed b a, c (full check) –
Rebased onto a master that changed shared src a, c a, c
Only README edits since validation – – (a cosmetic)
Rerun (--since = HEAD) a, c a, c

Open points

  • After a force-push, the old --since commit usually isn't in a fresh checkout, so the rebase improvement only kicks in if the workflow fetches that commit (git fetch origin <sha>). I haven't checked whether GitHub serves force-pushed commits that way. Without the fetch, it falls back to checking the whole branch, which is what master does today.
  • With criss-cross merges, git merge-base picks one of several merge bases.
  • The workflows are untouched; the PR workflow already passes --base and --since.

Checks

tsc, eslint, prettier and vitest (399 passed) all pass locally. Knip shows only the existing false positives caused by bin/main.ts not being a knip entry.

Refs #196

🤖 Generated with Claude Code

https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX


Generated by Claude Code

@metalwarrior665 metalwarrior665 changed the title feat(get-affected-actors): three-dot diff, narrowed per Actor with --since JUST FOR ILLUSTRATION feat(get-affected-actors): three-dot diff, narrowed per Actor with --since Oct 15, 2026
@metalwarrior665 metalwarrior665 changed the title JUST FOR ILLUSTRATION feat(get-affected-actors): three-dot diff, narrowed per Actor with --since feat(get-affected-actors): three-dot diff, narrowed per Actor with --since Oct 15, 2026

@JuanGalilea JuanGalilea left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

main thing we could do after (not here) is some accommodation for rebasing (fetch then diff) so we can process whatever --baseCommit we get

Comment thread bin/affected-actors.ts Resolved
Comment thread bin/affected-actors.ts Outdated
head: headSha,
commits,
// Empty `git log x..x` output doesn't parse as a commit
commits: range.fromRef === headSha ? [] : await getCommitsBetween(range.fromRef, range.toRef),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need this anymore?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

From Claude

It was still needed: get-affected-actors skipped the commit list when the range was empty, because getCommitsBetween couldn’t parse git’s empty output and threw. I fixed that at the source instead:

getCommitsBetween now returns [] for an empty range, so the special case in get-affected-actors is gone.
getCommits is shared by get-commits, get-latest-commit and get-changed-files, which all need at least one commit. It still fails on an empty range, but now with a clear message (“ has no commits that are not on ”) instead of “Failed to parse commit string”.
The two mocked tests that checked the special case are gone. Real-git tests replace them:
a branch with no commits of its own, and a release with nothing new, both report nothing;
getCommitsBetween('HEAD', 'HEAD') returns [];
getCommits fails with the new message.

I will go through the refactored code (it tried to satisfy you :) ) once I address everything

Comment thread test/unit/bin/git.test.ts Resolved
Comment thread test/unit/bin/git.test.ts Resolved
Comment thread bin/affected-actors.ts Outdated

logger.info(
'[MERGE-FROM-TARGET-OPTIMIZATION]: There is merge from target branch, checking if there are no functional changes in our own branch. If so, we can skip tests',
const narrowToChangedSince = (branch: ChangedActors, sinceValidated: ChangedActors): ChangedActors => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Personally this function is a bit too cryptic, I would never be able to understand what it is trying to do just by looking at the code. The JSDoc doesn't really help much

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yeah, same.
I remember spending quite a while reading it until i got it because i was so sure there was a bug there somewhere (there isnt) but its quite a smell.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I hand polished it:

  1. Rewrote JSDoc to be closer to how we see it (hopefully)
  2. Renamed variables to be more explicit and added more comments. more verbose code.
  3. Collapsed some if statements. We redo one git command because of that because we intersect equal sets but I think it is cleaner this way. Basically, since defaults base if no validated commit.

There is still room for improvement but I'm quite happy now

Base automatically changed from claude/remove-composite-commands to master October 21, 2026 23:11
…ctor with --since

--base now inspects `git diff base...HEAD`: what the branch changes, not what
it merged in from base. This replaces the merge-from-target detection and the
branch-only changed files lookup, and applies that optimisation per Actor:
Actors that only base changed are skipped even when the branch changes others.

With --since as well, the changes since the last validated commit are
classified separately and only Actors changed in both are reported, as
functional only when both diffs are functional. --since no longer has to be on
the branch, so a rebase only re-checks Actors that base changed too. A --since
that is HEAD (rerun) or missing locally (force-push) checks the whole branch.

Change detection now takes a { fromRef, toRef } range instead of a commit
list, so the cosmetic .actor JSON check compares the right endpoints for each
diff.

Refs #196

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
…examples

Shows how each diff classifies the Actors for a push with only the branch's
own changes, a cosmetic merge from base and a functional merge from base, and
notes that the scheme assumes the branch is up to date with base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
claude and others added 4 commits October 22, 2026 18:21
…I on real git

Adds CLI tests that build real histories and run the compiled CLI on them:
the branch's own changes since the last validated commit, a cosmetic merge
from base, a functional merge from base, Actors only base changed, code
changed in the merge commit itself, a rebase onto unrelated base changes,
and a rerun or a --since missing from the history. Five of them fail against
master's detection, which is what this PR changes.

Following review, the mocked getMergeBase test is dropped, since it only
restated the implementation, and findCommitSha is tested on real git. The
CLI compile step is shared with the build CLI tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
…p the empty-range special case

narrowToChangedSince now walks the branch's Actors and decides each one in
steps: left out when nothing changed since the last validated commit,
functional only when both diffs are functional, cosmetic otherwise. The output
is the same.

getCommitsBetween returns no commits for an empty range instead of failing to
parse git's empty output, so get-affected-actors no longer special-cases it.
getCommits keeps failing on an empty range, now with a clear message, since
get-commits, get-latest-commit and get-changed-files need a commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
…d the bundle

instagram-scraper-rs was another repository's checkout added by accident as
a gitlink. affected-actors.ts gets Prettier's formatting, and the bundle is
rebuilt so the CLI bundle check matches the refactor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
Comment thread bin/affected-actors.ts
* Exception: if the branch has any functional changes alongside the merge, we must re-test — even
* individually validated changes can have novel interactions when combined.
* Returns the Actors the branch's own commits changed, or undefined when it didn't merge base in.
* This is Juan's magical algorithm where we need just 2 git diffs to determine functional changes on Actors.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

😂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

kinda sus all the git manipulations.
Seems like it should work even for worktrees tho.

@metalwarrior665
metalwarrior665 merged commit fa9a0e7 into master Oct 26, 2026
11 checks passed
metalwarrior665 pushed a commit that referenced this pull request Oct 26, 2026
#204 removed hasMergeFromTarget and getBranchOnlyChangedFiles, so those
conflicts resolve to master's side. getChangedFilesBetween, now the only
place changed files are listed, keeps --no-renames. A real-git CLI test
covers a file moved between Actors, which fails without the flag. The bundle
is rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX
metalwarrior665 added a commit that referenced this pull request Oct 26, 2026
## Problem

Git turns on rename detection by default. With it, `git diff
--name-only` lists a moved file only under its new path. If a file moves
out of an Actor's context, that Actor doesn't count as changed, so it
isn't rebuilt or tested even though it lost the file.

## Change

`getChangedFilesBetween` lists changed files with `--name-only
--no-renames` (the shared `LIST_CHANGED_PATHS` in `bin/git.ts`). A move
then shows up as a deletion plus an addition, so both paths count. Since
#204, this is the only place changed files are listed:
`get-affected-actors` uses it for both the `--base` and `--since` diffs,
and `getChangedFiles` delegates to it.

The CLI bundle is rebuilt.

## Testing

- **Real-git CLI test** (`affected-actors-cli.test.ts`): moving
`actors/a/main.ts` to `actors/c/moved.ts` reports both `a` and `c` as
functionally changed. Without the flag it reports only `c`; I checked
that the test fails that way.
- **Unit tests**: the mocked ones assert the new argument.
- Typecheck, lint, Prettier and the 420 unit tests pass locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_012dp3mSDDgp5u2QVBT54EpX

---------

Co-authored-by: Claude <noreply@anthropic.com>
metalwarrior665 added a commit that referenced this pull request Nov 4, 2026
…ed-files (#228)

I think this makes sense, users can use `get-affected-actors` to get
changedFiles too.

## AI

`get-affected-actors` (#204) now computes commits, changed files and
changelog in one place from a single diff path. The three older commands
(`get-commits`, `get-latest-commit`, `get-changed-files`) are documented
as "kept for older workflows", but no workflow in this repo uses them.
They still carry a second, parallel git model:

- `getCommits`, which has its own rerun and base-commit heuristics and
duplicates what `since` does in `get-affected-actors`.
- `getChangedFiles`, which is `getChangedFilesBetween` with a `~` hack.
- `parseBaseCommit`, with the "SHA or Commit JSON" overload that only
existed to pipe `get-latest-commit` output back in.
- The `--source-branch`, `--target-branch` and `--base-commit` flags,
the `gitRangeOptions` group and the `Config` type.

#218 left these out on purpose and said the call belongs to the
maintainers. This PR is that proposal.

## What

- Remove the three commands and everything only they used. About 140
lines of source and about 250 lines of tests go away.
- `resolveReleaseBaseCommit` now validates the SHA inline and reuses
`findCommitSha`, instead of a second copy of the "`rev-parse` exit code
1 means missing" handling.
- Drop the "Legacy git commands" section from `docs/cli.md`.
CONTRIBUTING's CLI example now uses `get-affected-actors --base master`.
- Delete `logging.test.ts`. It compiled the whole project with `tsc`
(the cause of #227's timeout) just to check log levels through
`get-commits`. The same check is now two cases in `cli-bundle.test.ts`
that run `get-affected-actors` against the committed bundle.
- Rebuild the CLI bundle.

## Breaking

Anyone who calls these three commands directly breaks. The
`workflows-v0` reusable workflows don't, so only external users
scripting the CLI are affected. Since we're before v1, this is the
cheapest time to drop them. Whether to do it is the open question for
reviewers.

## Other ideas I considered and left out

Each overlaps an open PR, so it would conflict:

- #218 (build pipeline), #223 (`test` and `notify` commands) and #224
(bundle-based CLI tests) already cover the build and reporting code.
- Filling `lib/` gaps from issues (`fails` modifier #159, standby
without a task #127) are features, not refactors.

## Checks

- [x] `tsc`, eslint, prettier, knip (the unused exports it lists were
already on master) and `npm test` pass: 31 files, 438 tests.

---
_Generated by [Claude
Code](https://claude.ai/code/session_016XtpQvEqmuEhj1cBAaUxML)_

Co-authored-by: Claude <noreply@anthropic.com>
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.

4 participants