← Engineering deep dives

ForgeGraph

Incremental Build Engine & DAG Visualizer

ForgeGraph live DAG dashboard after a warm cache run, eleven tasks marked cache hit

Problem

Task graphs waste time redoing unchanged work, and cache misses that only say “hash changed” are not actionable. ForgeGraph is a local engine: schedule independent work concurrently, skip what is still valid, and explain every hit or miss.

System design

forge.jsonGraph builderSchedulerCache / explainProcessesEvent busCLIJSON reportRun historySSE → dashboard
Single event bus feeds CLI, JSON, run history, and the embedded dashboard.

Key engineering decisions

Content, not mtime

Cache identity is SHA-256 over schema version, task name, canonical command, configured env-value digests, sorted input paths and file bytes, and sorted dependency result fingerprints. A hit also requires declared outputs to still exist.

Downstream fingerprints

Result fingerprints include output hashes so a mutated producer output invalidates dependents even if the producer itself cache-hits.

Bounded workers

Remaining-dependency counts plus a worker pool (`--jobs`, default from the Go runtime). Dependents start only after every dependency succeeded or cache-hit. Failed deps skip downstream; unrelated branches continue.

One event stream

Immutable events fan out to the CLI, JSON report, `.forge/runs` history, and SSE. Cancelled work is never written as a successful cache record.

Single binary

The Vite/React UI is embedded with `go:embed`. `forge ui` binds loopback at `127.0.0.1:7331`.

Measured results

Local demo (`examples/demo-project`). Warm is 1ms, so this is reported as cards rather than a bar chart that would hide it.

Cold

1.33s

11 executed · 0 hits · max parallel 3

Warm

1ms

0 executed · 11/11 hits

One-file change

1.09s

4 executed · 7 hits

Cold sum of task durations was 2.62s against 1.33s wall clock (max observed parallelism 3). After changing src/api.txt, invalidated tasks were compile-api, unit-api, integration, and package. Unrelated branches remained cache hits.

Explainability

ForgeGraph timeline for the one-file incremental run with compile-api cache miss explained by src/api.txt SHA change
Cache miss

Changed input:
src/api.txt

Previous SHA-256:
a3d82997…

Current SHA-256:
443ebffd…

Verification

  • 56 Go tests
  • 4 frontend tests
  • go test -race ./... passed
  • go vet ./... passed
  • TypeScript typecheck and Vite production build passed
  • CI passed on the v0.1.0 tag

Release commit 9adfbeae5403ff5abdd916d80452229ea03912aa

Engineering notes

Validation caught a scheduler race (workers started before the initial enqueue), a re-lock deadlock risk on task finish, aggregate targets counted as executed, dashboard polling that overwrote a selected historical run, and run IDs containing dots being rejected. Those are fixed in v0.1.0.

What I learned

Cache correctness is an invariant problem, not a hash-printout problem. Fingerprints have to be ordered, env values digested rather than stored, and cancellation must not write success. Observability is cheaper when CLI, history, and UI share one event model.