## Problem Assets-backed Builder pages can take 6–7 seconds to become usable even when their Assets resources only request metadata. On the production-derived Blog project, a single Assets query measured 8,703 ms and an immediate repeat measured 8,110 ms. Four sibling Assets resources repeated the same authorization, development-build load/parse, compilation, canonical-asset preparation, runtime metadata load, and document work. ## Architecture The loader now treats remote resources holistically while preserving provider boundaries: - The client scheduler is resource-provider neutral. It versions logical requests, frees capacity when obsolete requests are replaced, ignores stale results, and aborts physical batches only when every member is obsolete. - Ordinary Assets requests in one server resource-loader call use a deferred Assets provider and execute as one repository batch. Independent HTTP, GraphQL, and other resources start immediately instead of waiting for Assets preparation. - Arbitrary remote responses are **not** coalesced or persistently cached. Their auth headers, cookies, upstream freshness, and `Vary` semantics remain isolated. - The Assets provider authorizes and creates its API context once per batch, then loads only `props`, `dataSources`, and `resources` from the development Build. - Queries covered by the saved Build compilation plan reuse that exact plan/artifact, preserving published byte limits, truncation, and pinned-revision behavior. Uncovered unpinned queries use a deterministic literal union and safely fall back to their existing individual path if shared preparation cannot preserve behavior. - Request-scoped document loading uses npm's byte-bounded `lru-cache` so sibling queries do not reread the same referenced author/content files. - Sibling dynamic queries with the same filter, sort, output, and content shape stitch into one document-graph resolution while keeping filter values, pagination windows, results, and failures independent. Aggregate document/byte-limit failures fall back to isolated execution. - Provider-neutral batching lives in the SDK runtime, and project asset-query preview orchestration lives in the shared project package so Builder and MCP paths can reuse the same behavior. - Cancellation flows from the client batch through the SDK/server/build/query path, with checkpoints between expensive phases. - Runtime asset metadata remains conservatively artifact-wide because URL/size filters, structured references, markdown hydration, materialized rows, and saved plans have different reachability rules. To make 1,000-asset artifacts safe, PostgREST `.in(...)` lookups are deduplicated and split into 100-ID chunks with bounded concurrency. ## Implementation checklist - [x] Trace the production Blog resource path and identify repeated work - [x] Copy the production Blog project into an ignored disposable local project - [x] Record production and local baselines - [x] Add provider-neutral client scheduling, stale-result protection, and cancellation - [x] Batch ordinary Assets queries without coupling or caching HTTP/GraphQL responses - [x] Load only Build fields required to create the saved content compilation plan - [x] Preserve saved-plan, truncation, fallback, diagnostics, and revision semantics - [x] Share Assets artifacts, runtime metadata, and revision-keyed document reads per request - [x] Stitch compatible query shapes into one graph resolution with isolated fallback - [x] Extract reusable SDK batching and project preview orchestration for MCP/runtime consumers - [x] Bound large runtime asset metadata lookups for the 1,000-asset project limit - [x] Add regression coverage and demonstrate new tests failing before their fixes - [x] Run focused and broad tests, type checks, lint, format, package-boundary, and diff checks - [x] Review documentation impact; no authoritative `docs/` content changes are required - [x] Regenerate and verify the affected asset-query runtime fixture; no production data is changed - [x] Address all actionable PR review comments - [x] Rerun the final parity-preserving implementation against the copied Blog project ## Measurements Production trace before the change: - Updates Assets query: 8,703 ms - Immediate identical repeat: 8,110 ms - Query artifact: 9,344 bytes / 7 documents - Saved source database: 42,086 bytes / 28 documents Copied-project local baseline: - Builder navigation until resource loading: 5,111 ms - Exact four-resource batches: 1,448 / 797 / 1,377 / 774 / 3,018 ms During implementation, before the final saved-plan parity tightening: - Exact four-resource batch: 174 ms first run, then 51–80 ms - Actual Builder resource-loader requests: 234–246 ms Final parity-preserving copied-project rerun: - Preflight: 10 ms - Exact four-resource batch: 435 ms cold, then 119 / 97 / 90 / 79 ms warm - Every response returned HTTP 200 with the same 16,884-byte payload The final cold batch is 70% faster than the best copied-project cold baseline, and the fastest warm batch is 90% faster than the best warm baseline. The production trace remains the authoritative production baseline; these before/after comparisons use the same disposable local project and captured request payload. ## Verification Passed: - Full package suites: Builder (212 files / 2,496 tests), asset-uploader (22 / 193), content-engine (40 / 274), project (7 / 84), SDK (29 / 508), and project-build (82 / 2,070) - Final focused regressions: Builder preview/API/resource integration (3 files / 42 tests), SDK resource loader (23 tests), and document-graph resolution (6 tests) - Builder, asset-uploader, content-engine, project, SDK, and project-build type checks - Fixture regeneration is deterministic; the generated vendor exactly matches the source runtime bundle - Fixture typecheck and production build - Changed-file Oxfmt, repository Oxlint, and package-boundary checks - `git diff --check` - `git diff --name-only HEAD --exit-code` New behavioral tests were demonstrated to fail before implementation and pass afterward, including Assets batch coordination, saved-plan wiring, truncation and pinned revisions, shared document reads, cancellation checkpoints, stale/replaced client requests, and large PostgREST ID filters. Agent evaluations were not run because they require separate explicit approval. ## Compatibility and remaining work The public resource result/error shape and on-demand diagnostics contract stay compatible. Generic HTTP/GraphQL caching semantics are unchanged. Compilation already in progress is not force-cancelled because safely interrupting shared compilation requires ownership/reference tracking; cancellation prevents later phases from starting. Closes #6003
Builder e2e
Fast local reruns
Use the full command for CI-like validation:
pnpm e2e:builder
The first run after the migrations change applies the regular migration
pipeline and writes an ignored schema cache under .cache/builder-e2e.
Subsequent runs in the same checkout restore that cache. CI does not preserve
it between runs. Refresh it explicitly with:
pnpm e2e:builder:refresh-schema-cache
For local test authoring, keep the e2e backend and Builder dev server running and rerun only the matching test. This avoids rebuilding Builder and avoids restarting Docker for every edit.
Terminal 1:
pnpm e2e:builder:dev:backend
Terminal 2:
pnpm dev
Terminal 3:
E2E_TEST_FILTER="Builder can copy, duplicate, and delete a page" pnpm e2e:builder:dev
E2E_TEST_FILTER is a substring match against the test name. It is not a
regular expression. If the filter matches no tests, the runner fails and prints
the available test names before starting Docker or building Builder.
Use E2E_TEST_FILTERS for a newline-separated list of exact or substring
matches. Each entry is matched against both the test name and
<suite> › <test>.
E2E_TEST_FILTERS='Builder can insert through the engine bridge
Builder-created data variables and resources persist' pnpm e2e:builder
CI shards the full e2e suite by file name tags such as
pages-actions.[shard-3].e2e.ts. Use E2E_TEST_SHARD=shard-3 to run one shard.
The goal is to keep all coverage while preventing one long serial e2e command
from hitting the command timeout.
When adding a new e2e file, include one shard tag in the filename:
my-workflow.[shard-1].e2e.ts
my-workflow.[shard-2].e2e.ts
my-workflow.[shard-3].e2e.ts
If one workflow file grows too slow, split it by scenario into files with the same base name and different shard tags:
large-workflow.[shard-1].e2e.ts
large-workflow.[shard-2].e2e.ts
large-workflow.[shard-3].e2e.ts
Choose the shard by current CI timing, not by feature ownership. Put new or expensive files into the fastest shard, and rebalance later by renaming files. CI retries a failed shard once because a single shard rerun is cheap enough to absorb occasional browser/backend flakes.
CI discovers its shard matrix directly from these filename tags. When one test file contains enough serial work to dominate a shard, add more shard tags to that filename. Its tests are distributed deterministically across those shards without duplicating the file or its helpers:
large-workflow.[shard-2].[shard-5].[shard-6].e2e.ts
pnpm e2e:builder:dev defaults to E2E_BUILDER_URL=https://127.0.0.1:3000,
so it runs against the already-running Vite dev server instead of building and
serving the production bundle. Override E2E_BUILDER_URL when the dev server is
running on another port. The E2E harness maps wstd.dev and project subdomains
to loopback for Chromium when this URL is loopback, so focused local runs do not
require machine-level host-file edits.
The dev backend uses E2E_DB_BOOTSTRAP=if-empty and keeps Docker containers
running. The e2e runner still resets test data before each run.
When you need a clean backend:
E2E_SKIP_CLEANUP=false pnpm e2e:builder