Skip to content

src/workspace/fingerprint.ts — workspace fingerprint

Compute a single hash for the workspace as a whole, folded into every task’s cache key. Lets a lockfile bump or workspace-shape change invalidate every cached entry at once.

export function computeWorkspaceFingerprint(workspaceRoot: string): Promise<string>

Returns a hex sha256.

Whichever of these exist at the workspace root, in this fixed order:

FileWhy
pnpm-lock.yamlpnpm resolved deps
package-lock.jsonnpm resolved deps
npm-shrinkwrap.jsonnpm published lock
yarn.lockyarn resolved deps
bun.lockBun resolved deps (text format)
bun.lockbBun resolved deps (binary legacy format)
pnpm-workspace.yamlworkspace shape

Missing files are skipped (not all workspaces use every manager). The fixed declaration order gives a deterministic fingerprint regardless of filesystem traversal order.

Per-project package.json is NOT folded in here — that’s execute-task.md’s hashProjectPackageJson (a separate projectPackageJsonHash field of CacheKeyInput).

const h = new Bun.CryptoHasher('sha256')
for (const f of FILES) {
if (file at <root>/<f> exists) {
h.update(`${f}\0`)
h.update(<bytes>)
h.update('\n')
}
}
return h.digest('hex')

The filename prefix prevents collisions between two files that happen to have the same byte content but different roles.

  • Doesn’t read inside node_modules/. The lockfile contains the resolved version set; that’s the source of truth for “did the dep tree change?”
  • Doesn’t hash arbitrary root-level files. Workspace-level globalInputs is a deferred feature.

tests/orchestrator.test.ts indirectly covers this — a lockfile edit invalidates every cached entry in the e2e fixtures. The fingerprint itself doesn’t have a dedicated unit-test file.

  1. Add the filename to WORKSPACE_FINGERPRINT_FILES.
  2. Bump CACHE_VERSION (presence of that file changes cache keys for affected workspaces).
  3. Update docs/caching.md § History.