getChangedFilePathsInGitSince(projectRoot: string,ref: string,timeoutMs?): Task<ChangeScan, GitScanFailure>
Resolves the working-tree paths that differ from ref, plus all uncommitted
modifications/additions, as absolute paths — or scope: 'everything' when a blast-radius
file (package.json, tsconfig*.json, …) changed.
timeoutMs is injectable for tests; production always uses the default.
Returns a lazy, retryable Task: the two git calls run only when the Task is awaited, and a
caller can .retry() a transient failure (e.g. index.lock contention) for free — Task.all
rebuilds over restarted members, so a retry re-runs this whole chain, git calls included. The
git boundary is the only throwing step, so .mapErr remaps any rejection there to a
declared GitScanFailed; a parsing bug in .map below is downstream of it and stays a bug,
never masked as a Failure. Callers await the value, or .result() for the bare
ChangeScan | GitScanFailure union.
import * as Failure from '../result/failure.ts'; // Defined, not invoked: awaiting the Task spawns the git subprocesses. async function scan(projectRoot: string): Promise<ChangeScan | 'run-all'> { const result = await getChangedFilePathsInGitSince(projectRoot, 'main').result(); return Failure.is(result) ? 'run-all' : result; // GitScanFailed degrades to a full run }