Skip to content

refactor: dedup orchestrator setup, split executeTask, share tally - #70

Merged
Exelord merged 1 commit into
mainfrom
claude/refactor-architecture
May 13, 2026
Merged

refactor: dedup orchestrator setup, split executeTask, share tally#70
Exelord merged 1 commit into
mainfrom
claude/refactor-architecture

Conversation

@Exelord

@Exelord Exelord commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Architect-reviewed refactor pass: dedup the most-duplicated logic, split the most-tangled function, share the most-drift-prone helper. 0 behaviour change, 414 tests still pass, no CACHE_VERSION bump.

What changed

1. Extract prepareRun from run and planRun

Both entry points were duplicating ~50 lines: workspace discovery → config load → graph build → cache open. New src/orchestrator/prepare.ts does it once; callers stay thin.

PreparedRun.empty is a small discriminated union (null / 'no-tasks-declared' / 'empty-graph') so callers handle empty cases their own way without re-deriving the reason.

2. Split executeTask into three named functions

The 200-line executeTask body had three orthogonal paths behind nested ifs. Now executeGroupTask, executePersistentTask, executeCachedTask behind a 3-line dispatcher. buildIsolatedEnv hoisted to a private taskEnv(node, step) helper.

3. Share tallyOutcomes between summary + run-artifacts

summary.ts and run-artifacts.ts were each computing the same per-status counts. New orchestrator/tally.ts:tallyOutcomes is the single source of truth; group-task exclusion baked in.

Small follow-ons in the same PR

  • isGroupTask(node) predicate added to graph/task-graph.ts; applied at 6 inline call sites.
  • expandRequested moved from orchestrator.ts to graph/task-graph.ts (paired with buildTaskGraph).
  • Dead taskId re-export from orchestrator.ts removed.
  • formatBriefDuration (byte-identical to formatDuration) replaced with an import.
  • OnDiskMeta in layered-cache.ts derived as Omit<CacheEntry, 'hash' | 'outputFiles' | 'source'> — stays in sync with the cache contract automatically.
  • SaveArgs marked @internal.

Extension-point notes

For the next obvious features the architect noted clean seams now exist for:

  • Named inputs / target defaults — rewrite each ProjectEntry.config between project-config load and graph build inside prepareRun.
  • OTel telemetryTelemetry sink slots into PreparedRun as a no-op default.
  • --output-logs modesdefaultLogger({ colors, mode }) extension; the per-status dispatcher in (2) makes the hooks cleaner.

Diff stat

File +/−
src/orchestrator.ts -113 lines (the duplication moved into prepare.ts)
src/orchestrator/execute-task.ts now 3 named functions + 2 helpers behind a dispatcher
src/orchestrator/prepare.ts new, +131
src/orchestrator/tally.ts new, +54
src/orchestrator/run-artifacts.ts local tallyOutcomes removed, imports shared
src/orchestrator/summary.ts uses shared tallyOutcomes
src/orchestrator/framed-output.ts imports formatDuration from summary.ts (was a byte-identical local copy)
src/graph/task-graph.ts +isGroupTask, +expandRequested
src/cache/layered-cache.ts OnDiskMeta derived from CacheEntry
src/cache/cache.ts SaveArgs JSDoc'd @internal
tests/summary.test.ts fixture updated to populate node.config.exec (was implicit before)

Tests: 414 pass, 0 fail (was 414 pass, 0 fail). Net source reduction: ~120 lines.

Test plan

  • bun src/bin.ts run lint — clean
  • bun src/bin.ts run format-check — clean
  • bun test — 414 pass, 0 fail
  • No source-semantics changes; no public-surface changes (src/index.ts exports identical)
  • No CACHE_VERSION bump
  • CI green

Generated by Claude Code

Architect review identified concrete duplication + complexity that
could be reduced without behaviour change. Three focused refactors,
414 tests still passing, 0 source semantics changed, no CACHE_VERSION
bump.

## 1. Extract `prepareRun` from `run` and `planRun`

Both entry points were duplicating ~50 lines: findWorkspaceRoot,
loadWorkspace, loadWorkspaceConfig, listProjects, loadProjectConfig
×N, buildPackageGraph, computeNestedProjectDirs, expandRequested,
buildTaskGraph, computeWorkspaceFingerprint, new Cache +
wrapWithRemoteCache. New `src/orchestrator/prepare.ts` does it once;
the two callers stay thin and handle their own divergence (run
logs + returns NOT-ok; planRun returns empty plan).

The `PreparedRun.empty` field is a small discriminated union
(`null` | `'no-tasks-declared'` | `'empty-graph'`) so callers can
produce their own messaging without re-deriving the reason. The
cache handle is constructed unconditionally so callers always have
a uniform `try { ... } finally { cache.close() }` shape.

## 2. Split `executeTask` into three named functions

The 200-line `executeTask` body contained three orthogonal paths
(group / persistent / cached) behind nested ifs. Split into
`executeGroupTask`, `executePersistentTask`, `executeCachedTask`
behind a 3-line dispatcher. `buildIsolatedEnv` was constructed
identically in two paths; hoisted to a private `taskEnv(node, step)`
helper. Naming `executeCachedTask` (rather than "normal") matches
the architect's note — every path through this function is the
"cached path" when cacheEnabled is true.

## 3. Share `tallyOutcomes` between summary + run-artifacts

`summary.ts` and `run-artifacts.ts` were each computing the same
per-status counts (successful / failed / skipped / cachedLocal /
cachedRemote / total) with subtly different filtering. New
`src/orchestrator/tally.ts:tallyOutcomes(outcomes) -> Tally` is the
single source of truth; group-task exclusion is baked in via the
shared `isGroupTask` predicate. Both surfaces now derive identical
numbers from one place.

## Small follow-ons (same PR)

- `isGroupTask(node)` predicate added to `graph/task-graph.ts`;
  applied at six call sites that previously inlined
  `node.config.exec === undefined`.
- `expandRequested` moved from `orchestrator.ts` to
  `graph/task-graph.ts` next to `buildTaskGraph` — they're paired.
- Dead `taskId` re-export from `orchestrator.ts` removed (no
  consumers; was a leftover from an earlier surface).
- `formatBriefDuration` in `framed-output.ts` was byte-identical to
  `formatDuration` in `summary.ts`; replaced with an import.
- `OnDiskMeta` in `layered-cache.ts` is now
  `Omit<CacheEntry, 'hash' | 'outputFiles' | 'source'>` so the
  on-disk meta schema stays in sync with the cache contract
  automatically.
- `SaveArgs` marked `@internal` (it's a structural shortcut for
  `LayeredCache`, not part of the conceptual cache contract).

## Extension-point notes (for future)

The architect flagged where common future features plug in cleanly
after these refactors:
- **Named inputs / target defaults.** Rewrite each
  `ProjectEntry.config` between project-config load and graph build
  inside `prepareRun`. The seam exists.
- **OTel telemetry.** `Telemetry` sink slots into `PreparedRun` as a
  default no-op; consumers (`recordRun`, the per-task callbacks)
  pick it up.
- **`--output-logs` modes.** `defaultLogger(colors)` becomes
  `defaultLogger({ colors, mode })`. The dispatcher in (2) makes the
  per-status hooks easier.

## Lint / format / test

414 pass / 0 fail. `tests/summary.test.ts:outcome` fixture updated
to populate `node.config.exec` so `isGroupTask` returns false (was
previously implicit because the orchestrator pre-filtered group
tasks; now the shared `tallyOutcomes` does the filter itself).
@Exelord
Exelord merged commit 0de10a7 into main May 13, 2026
1 check passed
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.

2 participants