Skip to content

chore: speed up validation and cut agent token usage - #435

Merged
V3RON merged 1 commit into
mainfrom
chore/faster-agent-validation
Aug 20, 2026
Merged

chore: speed up validation and cut agent token usage#435
V3RON merged 1 commit into
mainfrom
chore/faster-agent-validation

Conversation

@V3RON

@V3RON V3RON commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

Makes validation in this repository faster and much less verbose, primarily for agents working in it.

  • Turborepo no longer builds the entire monorepo before every lint run. Linting the 29 non-playground packages from cold goes from 59.5s to 9.6s.
  • Successful and cache-replayed task logs are suppressed; failures still print in full.
  • pnpm install is silent on success — a cold install drops from 82 lines to 0.
  • Adds :affected script variants so a two-file change no longer validates 38 packages.
  • Fixes two cache-correctness bugs that could produce stale or false results.

Related Issue

None — this is repository tooling rather than a user-facing change, and was not tracked by an existing issue.

Context

Why lint no longer depends on build. The shared eslint.config.mjs uses typescript-eslint's recommended preset with no parserOptions.project and no projectService. Without those the parser never constructs a TypeScript program, so no rule can read a .d.ts. Lint therefore cannot observe a stale or missing build — verified by linting a file that imports a non-existent export from an unbuilt package, which exits 0. Building first bought nothing and cost a full monorepo build on every lint.

Why @rozenite/playground is the exception. It runs expo lint, and eslint-config-expo enables eslint-plugin-import, whose import/no-unresolved rule does resolve @rozenite/* imports to dist. Unbuilt, it reports 26 false failures, so the build dependency is kept for that one task.

Why expo lint gets --no-cache. Its ESLint cache lives under the gitignored .expo/, which Turborepo can neither see nor invalidate. A lint run made before the workspace packages were built keeps replaying its stale import/no-unresolved failures even after dist/ exists — the exact trap someone hits in a fresh worktree: lint fails, build, lint again, still fails. Turborepo already caches the task at a higher level, and the flag measures at ~0.3s.

Cache-correctness fixes. 14 package tsconfig.json files extend tsconfig.base.json and 11 ESLint configs import the root eslint.config.mjs, but neither root file was a cache input — editing either left every cached task stale. They are now globalDependencies. Separately, every changeset release rewrites all 31 CHANGELOG.md files, which invalidated the entire task graph; README.md and CHANGELOG.md are now excluded from task inputs (verified that no build tooling reads them).

Scope of the speedup. The lint improvement applies to standalone pnpm lint:affected / lint:all. Commands that also run typecheck are build-bound regardless, since typecheck genuinely needs .d.ts — the task graph for typecheck lint is unchanged at 114 tasks, and CI's typecheck build lint test is unchanged at 152. CI timings are therefore not expected to move.

preferOffline was considered and deliberately rejected: it saves nothing on a lockfile install (20.9s vs 21.7s cold) and, once pnpm's metadata cache goes stale, resolves ranges against that cache instead of revalidating — reproduced with semver@^7.0.0 resolving to 7.6.0 rather than 7.8.5. The reasoning is recorded in docs/agents/validation.md so it is not re-attempted.

Testing

  • npx turbo run typecheck lint test118 tasks, all successful
  • npx turbo run lint --force from a fully cold state (all dist/, the Turborepo cache and the expo cache wiped) — 58/58 successful, including @rozenite/playground
  • Cold A/B on the lint change, with an isolated cache dir: 59.5s → 9.6s for all packages except playground; 7.51s → 2.45s for a single package
  • pnpm install --frozen-lockfile — silent (0 lines); loglevel: warn verified not to affect pnpm run output, with byte-identical stdout for a passing vitest run and a failing lint, including the error and exit status
  • Deprecation warnings verified still visible at loglevel: warn
  • pnpm format:all clean for every file touched here
  • pnpm release:plan — no version plan required (root config, docs, and apps/playground are excluded from versioning)

Known pre-existing issues, not addressed here

  • pnpm format:all fails on three files that predate this branch (websocket-interceptor.ts, fuseboxReactDevToolsDispatcher.ts, compatibility.mdx). They look like leftovers from a Prettier-era print width.
  • No workflow runs pnpm format:all, which is why that drift went unnoticed.
  • CI does not cache .turbo, so every run rebuilds affected packages and their dependency chains from scratch.

Turborepo rebuilt the whole monorepo before every lint run, and every task
replayed its full logs on a cache hit. Both are expensive for agents working
in this repository, and neither bought anything.

- turbo.json: drop the build dependency from `lint`. The shared ESLint config
  uses typescript-eslint's `recommended` preset with no `parserOptions.project`
  or `projectService`, so no rule ever constructs a TypeScript program or reads
  a `.d.ts` — it cannot observe a stale or missing build. Linting the 29
  non-playground packages from cold goes from 59.5s to 9.6s.
- turbo.json: keep that dependency for `@rozenite/playground#lint` only, which
  runs `expo lint`; eslint-config-expo enables eslint-plugin-import, whose
  `import/no-unresolved` rule does resolve `@rozenite/*` imports to `dist`.
- apps/playground: pass `--no-cache` to `expo lint`. Its ESLint cache lives
  under the gitignored `.expo/`, invisible to Turborepo, so a lint run made
  before the workspace packages were built kept replaying stale
  `import/no-unresolved` failures even after `dist/` existed. Turborepo already
  caches the task and the flag costs about 0.3s.
- turbo.json: add `globalDependencies` for `tsconfig.base.json` and
  `eslint.config.mjs`. 14 package tsconfigs extend the former and 11 ESLint
  configs import the latter, but neither was a cache input, so editing either
  left every cached task stale.
- turbo.json: exclude README.md and CHANGELOG.md from task inputs. Every
  changeset release rewrites all 31 changelogs, invalidating the whole graph.
- turbo.json: quiet successful and replayed task logs via `outputLogs`.
  Failures still print in full.
- package.json: add `:affected` script variants, plus `test:all`.
- pnpm-workspace.yaml: set `loglevel: warn`. A cold install drops from 82 lines
  to 0. Warnings and errors still print, and the setting does not affect
  `pnpm run` output, so Turborepo task logs are untouched.
- docs: add docs/agents/validation.md, and point AGENTS.md, CONTRIBUTING.md and
  the pull request guide at the `:affected` scripts.
@V3RON
V3RON merged commit 90baa1c into main Aug 20, 2026
4 checks passed
@V3RON
V3RON deleted the chore/faster-agent-validation branch August 20, 2026 08:11
V3RON added a commit that referenced this pull request Aug 20, 2026
## Description

`pnpm format:all` fails on `main` today. That matters more since #435,
because CONTRIBUTING.md and the pull request guide now point
contributors at `pnpm checks:affected`, and that script ends in
`format:all`.

- Reformats three files that predate the move to oxfmt.
- Adds a `Check formatting` step to CI so it cannot drift again.
- Ignores Claude Code's local state, which was breaking `format:all`
independently.

## Related Issue

None — repository tooling, follow-up to #435.

## Context

**Why the three files were unformatted.** The diffs *unwrap* lines
rather than wrapping them — a six-line union type collapses to one line,
a markdown table's column padding widens. That is the signature of files
formatted at Prettier's narrower print width and never reformatted when
the repo moved to oxfmt. They are stale leftovers, not new drift.

The only token-level changes are trailing commas appearing/disappearing
as arguments collapse onto one line, and one leading union `|` — all
semantically inert. Verified by checksumming both revisions with
whitespace, commas and that pipe stripped, and by a full
typecheck/lint/test pass on both affected packages. No behavioral
change, so no version plan.

**Why nothing caught it.** No workflow has ever run `pnpm format:all`;
CI runs `turbo run typecheck build lint test --affected` only. The new
step runs before the turbo step so it fails fast — oxfmt checks all 1246
files in under 400ms.

One gap this does not close: the workflow has `paths-ignore:
website/**`, so a website-only PR runs no CI at all and could still land
unformatted `.mdx`. Whether website PRs should gate on CI is a separate
decision.

**Why only two `.claude/` paths are ignored.**
`.claude/settings.local.json` and `.claude/worktrees/` were excluded
only through per-machine files — the global git excludes file that
Claude Code writes to, and `.git/info/exclude`. Git therefore ignored
them but oxfmt did not, since oxfmt reads `.gitignore`/`.prettierignore`
from the repo only. The result was that `pnpm format:all` failed for
anyone using Claude Code, regardless of the three files above.

Ignoring `.claude/` wholesale would be wrong: per the Claude Code
settings documentation, `.claude/settings.json`, `agents/`, `skills/`
and `commands/` are all intended to be committed and shared with the
team. Only the two machine-local paths are ignored here.

## Testing

- `pnpm format:all` — passes (1246 files, 383ms)
- `npx turbo run typecheck lint test` — **118 tasks, all successful**
- `npx turbo run typecheck lint test
--filter=@rozenite/network-activity-plugin --filter=@rozenite/web` — 38
tasks, all successful
- Confirmed the reformatting is semantically inert by checksum, as
described above
- CI workflow YAML re-parsed after editing; step order verified
V3RON added a commit that referenced this pull request Aug 20, 2026
## Description

Caches Turborepo artifacts in CI, restoring on every run but writing
only from pushes to `main`.

The Validate job spends **319s of its 6m19s** inside `turbo run`.
Nothing persists between runs today: `.turbo` is gitignored and only the
pnpm store is cached, so every run rebuilds affected packages and their
entire dependency chain from scratch.

> Stacked on #436 — the base will retarget to `main` once that merges.
Only the two workflow files are this PR's own change.

## Related Issue

None — repository tooling, follow-up to #435.

## Context

**Why saves are restricted to `main`.** GitHub scopes a pull request's
cache to its merge ref, and that cache "can only be restored by re-runs
of the pull request" — so a fork PR cannot reach `main`'s scope or
another PR's regardless. Restricting saves closes the remaining case: a
run writing a cache that a later re-run *of the same PR* would restore.

That distinction matters more for Turborepo than for the pnpm store. A
poisoned store still leaves every task to actually run. A poisoned
Turborepo artifact makes tasks report a cache hit **without executing**
and replays their stored logs — and #435's `outputLogs` now suppresses
cache-hit logs, so the replay is quieter than it used to be. A fork PR
could use that to forge a green check on a diff that would otherwise
fail. Nothing detects this for us: pnpm documents `verifyStoreIntegrity`
as catching accidental corruption, explicitly not deliberate tampering,
and Turborepo's signature verification applies only to *remote* caches,
not the filesystem cache `actions/cache` persists. Scope is the control
that matters.

Saving only from `main` means every cached artifact was produced by
already-merged, reviewed code. PRs still restore `main`'s cache, which
covers every dependency build they did not change; they only re-execute
tasks for the packages they actually touch, which `--affected` already
bounds.

**Why the key rotates weekly with no fallback.** Turborepo never prunes
`.turbo/cache`. The accumulated local cache in this repository is
already **1.8GB across 10050 entries**. Because each `main` run restores
before it saves, the saved artifact is the union of everything cached so
far — so an unrotated key grows without limit until it hits the 10GB
per-repository cache limit and evicts everything else, including the
pnpm store cache.

There is deliberately no `restore-keys` fallback to the previous period:
falling back would carry the accumulation forward and defeat the reset.
The cost is one cold run at the start of each week, which is no worse
than today's behaviour, where every run is cold.

**Why `release.yml` stays uncached.** It holds `contents: write` and
`id-token: write` and publishes to npm. A restored artifact would be
published with provenance attesting to something that was never built
from this source. The job now carries a comment saying so, since this is
exactly the kind of thing a later "optimization" would undo.

## Testing

- Both workflow files re-parsed as YAML after editing; step order and
`if:` conditions verified
- `actions/cache` pinned to `55cc8345863c7cc4c66a329aec7e433d2d1c52a9`,
confirmed via the API to be v6.1.0 in the first-party `actions/` org,
matching this repository's pin-everything convention
- Confirmed the `restore` and `save` sub-actions exist at that exact SHA
- `npx turbo run typecheck lint test` — **118 tasks, all successful**
- `pnpm format:all` — passes

Cache behaviour itself can only be confirmed on a real run. Two things
to watch:

1. **The path.** Turborepo resolves `.turbo/cache` at the repository
root in a plain clone; I could only verify the resolution logic locally
inside a git worktree, where it deliberately redirects to the main
checkout. If the path were wrong the symptom is a cache that never hits,
not a wrong result.
2. **Effect on runtime.** The baseline to beat is the 6m19s Validate run
on #435. The first push to `main` after this merges is still cold — it
populates the cache. Subsequent runs are the ones to measure.
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.

1 participant