Skip to content

dogfood: collapse the agent iteration loop on first-time webjs scaffolds #804

Description

@vivek7405

Problem

While scaffolding a simple full-stack todo app at ~/Documents/Projects/webjs-test (single-user, Node 24+, SQLite, full-stack template), the agent (myself) burned five write → check → rewrite cycles before webjs check went green and webjs typecheck exited 0. Every cycle was for a fix the agent already had enough context to write correctly the first time, given a sharper signal from the toolchain. This pattern has surfaced repeatedly (the host filed a prior issue for the same shape; #449 already exists for the server-import variant of this loop, and #789 / #789 closed the docs side of it). The remaining gap is the SUM of small per-cycle frictions, not any one of them.

Concretely, the cycles were:

  1. Cycle 1 — transitive server-only import via a type-only module. Agent wrote modules/todos/types.ts that re-exported Todo from db/schema.server.ts, then app/page.ts registered the component AND called the server query, so the page elision verdict shipped the page to the browser with the schema in its closure. no-server-import-in-browser-module fired AFTER 100+ lines of code were written. Fix: move Todo to a JSDoc typedef in types.ts, drop the schema import, import the component in app/layout.ts (where the layout is the carrier) rather than app/page.ts. Took two rewrites because both halves (typedef split AND carrier choice) had to land.

  2. Cycle 2 — Drizzle 1.0.0-rc.3 query API mismatch. Agent wrote the canonical db.select({ id, completed }).from(todos).where(eq(todos.id, id)) from agent-docs/recipes.md and got error TS2554: Expected 0 arguments, but got 1 (the typed overload does not match the docs example in rc.3). Same family on db.delete(todos).where(...).returning({ id }) (rc.3 has only the no-arg .returning() overload). Fix: switch to the relational-query API db.query.todos.findFirst({ where: { id } }) everywhere. Three rewrites.

  3. Cycle 3 — WebComponent shadowing TS errors. Agent declared a class field private title = '' (overrides the inherited title on WebComponentBase, error TS2415 "private in type but not in base"); declared a method remove(t) (overrides HTMLElement.remove(), error TS2416). Rename + restart. Two rewrites.

  4. Cycle 4 — framework Signal export shape. Agent imported type SignalState (does not exist) and type Signal (the export is a value, the class, not a type). Two TS rewrites before settling on let TS infer.

  5. Cycle 5 — app/page.ts registration placement. Even after fixing the transitive import, no-server-import-in-browser-module still fired because the page BOTH registered the interactive component (so it shipped) AND transitively imported the schema. AGENTS.md has the rule ("register it in a layout.{ts,js} instead so the page elides again") but it is buried under carrier-and-elision prose. Agent missed it on the first read; the check was correct, the agent needed two attempts. One rewrite.

Total: ~10–15 minutes of throw-away edits, all because the agent either wrote the wrong shape the first time OR missed a one-paragraph rule buried in a 60 KB AGENTS.md. The PR is the same size either way; the iteration cost is pure overhead.

Design / approach

Three coordinated fixes, each addressing a cycle class:

A. Sharper webjs check messages — name the file chain, not the leaf.

The current no-server-import-in-browser-module message looks like:

This page ships to the browser (the build does not elide it) but transitively imports the server-only module db/schema.server.ts (app/page.ts -> … -> db/schema.server.ts).

The … -> elides the actual chain. An agent cannot tell from this whether the broken edge is in app/page.ts itself (carrier problem, cycle 5) or in a types module it forgot about (cycle 1). The message needs the full chain AND a one-line recommendation picked from the chain analysis. Concretely:

  • Print the full transitive chain (app/page.ts → modules/todos/components/todo-list.ts → modules/todos/types.ts → db/schema.server.ts → db/connection.server.ts), not .
  • Detect and recommend the carrier fix when the chain's leaf is reached via a *.server.ts import from a *.ts types-shaped module, naming the immediate cycle class.
  • Detect and recommend the layout-registration fix when the carrier (the importer of *.server.ts) is a page.ts whose only browser signal is registering a component.

B. agent-docs/ updates that codify the right shape, with the next agent finding the canonical example in <30 seconds.

Three additions:

  • agent-docs/types-and-mutations.md (new): "Where do Todo, ActionResult<T>, etc. live when the consumer ships to the browser?" Codify: server-derived types stay in the action file's local JSDoc typedef OR in modules/<feature>/types.ts as a host-interface-style JSDoc (no runtime imports from *.server.ts or db/). Show the wrong + right chain in code.
  • agent-docs/components.md addendum: list the inherited members agent code cannot shadow without renaming (HTMLElement.remove, WebComponentBase.title, plus check for any other instance fields on the base). One-table summary, no scare copy.
  • agent-docs/orm.md (new OR fold into recipes.md): the rc.3 Drizzle query surface actually installed under drizzle-orm@^1.0.0-rc.3. Two patterns preferred: (1) db.query.<table>.findFirst({ where: { id } }) / .findMany({ orderBy: ... }) for simple reads, (2) db.insert/update/delete(t).set(...).where(eq(...)) for mutations, with NO .returning(fields) (use no-arg .returning() and pull fields locally). Mark the older db.select({...}).from(...) API as a gotcha tied to a specific Drizzle overload.

C. Pre-edit hook to catch cycle 1 + cycle 5 BEFORE the file lands.

.claude/hooks/check-server-imports.sh (a PreToolUse Edit hook, the same shape as the existing require-tests-with-src.sh): on every Edit / Write to a *.ts file outside *.server.ts, run a static walk: for each non-server .ts file the file imports, recursively check that none transitively imports db/*.server.ts / lib/*.server.ts. If a violation is found, BLOCK the edit and surface the chain. This is the same check rule webjs check runs in the post-edit pass, but moved to pre-edit. The cycle-5 carrier choice (page vs layout) is harder to catch pre-edit; document it in the new agent-docs/types-and-mutations.md and let the post-edit message handle it.

This is similar to .claude/hooks/require-tests-with-src.sh (added in an earlier round). The pattern is: same rule as webjs check, moved earlier in the loop, so the agent never writes the violating file.

Implementation notes (for the implementing agent)

  • A. Sharper check messages: edit packages/server/src/check.js. The rule lives under noServerImportInBrowserModule (grep for the rule id). Replace the … -> chain collapse with the full chain (already available in the rule's static walk — see the walk helper that follows imports), and branch on the leaf shape (types-only module vs page/layout carrier) to pick a recommendation string. Test in packages/server/test/check/server-import.test.js (or extend an existing file). Run with a fixture like the one in the Problem section; assert both the chain text and the right recommendation.
  • B. Docs: new files at agent-docs/types-and-mutations.md, agent-docs/orm.md; append to agent-docs/components.md. The CONTRIB protocol (per root AGENTS.md "Keep it in sync whenever behaviour changes") wants these in the same PR as the rule change. Cross-link from agent-docs/lit-muscle-memory-gotchas.md (the existing "shipped-to-the-browser means closure must close" entry is cycle 1 — fold that pointer here or expand it).
  • C. Hook: packages/cli/templates/.claude/hooks/check-server-imports.sh, registered in packages/cli/templates/.claude/settings.json under a PreToolUse:Edit matcher (PreToolUse:Write for new files). Implementation: a small node script that walks the import graph of the file being written, recursing through non-*.server.ts imports and printing the chain. Exit non-zero on a violation; exit 0 otherwise. The scaffold has a clear template pattern in packages/cli/templates/.claude/hooks/require-tests-with-src.sh to mirror. Note this template ships from the CLI; the repo copy lives alongside (linked by the scaffold).
  • Tests + docs: each fix needs a fixture. The check rule change in (A) needs a regression test (write the Cycle-1 chain in a temp app, run check, assert the message contains the full chain AND the right recommendation). The docs (B) need a counterfactual "before-and-after" example under agent-docs/types-and-mutations.md#counterfactual. The hook (C) needs an e2e: scaffold a temp app, write the violating chain, run the hook's filter, assert it blocks. All three under packages/server/test/check/ (rule) and packages/cli/test/scaffold-integrity/ (hook + docs presence).

Acceptance criteria

  • no-server-import-in-browser-module message prints the FULL transitive chain, no … -> truncation.
  • For the same message, when the chain's leaf is reached via a types-only *.{ts,js} module that re-exports from a server file, the recommendation names typedef relocation as the fix. When the chain's carrier is a page.ts that only ships because of a <my-el> registration, the recommendation names the layout-registration fix.
  • agent-docs/types-and-mutations.md exists with both shapes (wrong / right) and references the recipes.md todo example.
  • agent-docs/components.md lists the inherited fields/methods agent code cannot shadow without renaming (title, remove, plus any others on WebComponentBase).
  • agent-docs/orm.md codifies the rc.3 Drizzle patterns preferred for the scaffold's installed version (relational query for reads, no db.select({...}) overload, no .returning({...})).
  • A counterfactual proves each cycle-fix works: write the offending chain in a fixture, run the new check, expect green on the FIRST attempt (no cycles).
  • packages/cli/templates/.claude/hooks/check-server-imports.sh exists, registered in templates/.claude/settings.json, runs as a PreToolUse:Edit, blocks the same chain at write time with a non-zero exit.
  • A new test under packages/server/test/check/server-import-message.test.js (or extension to the existing file) pins the message shape.
  • A new test under packages/cli/test/scaffold-integrity/hook-blocks-server-import.test.js pins that the hook fires on a real scaffolded-app edit.
  • Docs site (docs/app/docs/troubleshooting) cross-linked from the new agent-docs/ files for the "I imported the schema into types.ts and now check fires" symptom.

Counterfactual

If reverted, an agent rebuilding the todo-app reproduces cycles 1, 3, 5 with the first webjs check run, taking ≥ 15 minutes of edits before green. With all three landed, the first-pass scaffolded todo app passes webjs check on the FIRST render — confirmed by running the fixture under packages/cli/test/scaffold-integrity/first-pass-check.test.js.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions