Skip to content

feat(cli): no verb generates a resource route slice — the typed contract, cache-first loader and withResource page must be hand-copied from init #1354

Description

@rickylabs

Summary

Every element of the canonical resource slice exists in packages/fresh and is demonstrated once,
as a frozen init template for the single scaffolded example service — but no re-runnable verb
emits it
. A user who adds a resource must hand-transcribe the example: typed route contract,
definePage root, route-local (_components)/(_islands)/(_shared) layout, cache-first loader,
QueryIsland, deferred partial. The measured consequence is that agents do not transcribe it: the
Wave-6 rickylabs/loom run received the registry, /design, AGENTS.md and WEB-LAYER.md and
still hand-rolled tables, direct service calls and a 676-line island (#1333 body). Generation, not
more prose, is the remaining lever.

Evidence

Current surface

netscript ui:add page <path> --island writes three files, none of which loads data (#1357).
netscript service add --with-client writes one app-side module (#1355). Nothing in between
exists: there is no verb that takes a contract procedure and emits a working screen. The reference
shape lives only in packages/cli/src/kernel/assets/app/routes/examples/service/ +
(_shared)/service-showcase.ts.template + (_islands)/ServiceShowcaseLab.tsx.template +
routes/partials/examples/service-summary.tsx.template, rendered once by init and never again.
Route ids in the one generator are hand-derived dotted strings
(web-scaffold.ts:20) with no link to apps/<app>/.generated/routes.ts, and the generated manifest
is only seeded by init and regenerated by the Vite plugin — there is no generate routes verb, so
a CI/agent flow that never runs Vite has a stale typed route surface
(packages/cli/src/kernel/application/scaffold/writers/app-route-seeds.ts;
packages/fresh/src/application/route/manifest.ts:280-292).

Target contract

One composable generator, addressed at an app (#1356), that emits a complete, type-checking,
runnable
resource slice and can be re-run:

  1. Core (always): <route>.route.ts sidecar declaring the route contract via
    defineRouteContract (path/search schemas typed, not Record<string, unknown>); index.tsx
    containing a definePage() root with zero presentational JSXwithRouteContract/
    withRoute, withResource for the shared read, withLayer per region, withLayout slots,
    withMeta; route-local (_components)/<resource>-view.tsx holding all markup;
    (_shared)/<resource>-loaders.ts holding the contract-derived cache-first loader
    (createNetScriptQueryClientfetchQuerydehydrateQueryClientcachedAt);
    (_islands)/<Resource>Island.tsx binding QueryIsland + useIslandQuery to the generated
    query factory from feat(cli): app-side client/query wiring is a one-shot template with hardcoded names, colliding 'service' cache keys and a no-op invalidation #1355 (clientKey, not a string literal) and passing
    initialDataUpdatedAt (fix(scaffold): the canonical island never passes initialDataUpdatedAt, so the loader's cachedAt is computed, displayed and discarded #1360); registration in router.ts/appRoutes derived from the
    generated manifest, not an inline createRouteReference.
  2. Optional flags, composable and independently testable: --form (a withForm region with a
    Zod schema whose messages are user-facing copy, csrf: true, redirectTo through the typed
    route map, spanName, and a component reading firstFieldError), --partial (a deferred
    withLayer + a routes/partials/... partial whose name is derived, not a matching string
    literal), --stream (a @netscript/fresh/streams consumer).
  3. State: when a slice needs request-scoped state, the generator extends the app's
    utils.ts State type instead of leaving Record<string, never>.
  4. Components: all markup composes app-owned Fresh-UI primitives from
    components/ui/mod.ts — the generator never emits raw Tailwind for something the registry ships.
  5. Ownership comments: each emitted route-local directory carries a one-line header stating what
    belongs in it ((_components) = markup only, (_islands) = hydration only, (_shared) =
    loaders/types, (_lib) = route-local pure helpers), so the vocabulary survives the first edit.
  6. Generator semantics match the rest of the CLI: --dry-run, --force, and a
    content-compare written/skipped result like generate runtime-schemas
    (packages/cli/src/public/features/generate/runtime-schemas/generate-runtime-schemas.ts:44-77)
    — not the current hard refuse at web-scaffold.ts:60.
  7. Optional contribution inclusion (RFC-A-gated): when installed plugins contribute route/SDK
    surfaces, the generator includes them. This clause and only this clause depends on Epic: Typed SDK client contributions — credentials, transport policy, metadata, and cache-safe extensions #1348;
    the core generator ships without it and must not be sequenced behind the RFC.

Acceptance

  • A single documented verb emits the core slice listed in Target contract §1 for a named
    contract procedure.
  • The emitted slice type-checks and lints inside a freshly scaffolded project with no manual
    edits.
  • The emitted page file contains no presentational JSX outside withLayout slots.
  • The emitted island binds the generated query factory's clientKey, not a string-literal key.
  • The emitted route registers through router.ts/appRoutes derived from the generated
    manifest, not an inline createRouteReference.
  • --form, --partial and --stream each add exactly their own files and are independently
    re-runnable.
  • The verb supports --dry-run and --force and reports written/skipped on re-run.
  • A second run with no input change writes zero files and exits 0.
  • Negative test: generation into a project whose contract lacks the named procedure fails
    non-zero with the missing procedure named, and writes no files.
  • Negative test: a golden test fails if any emitted file contains any, a raw fetch( call,
    a hand-written query-key array literal, or manual JSON.parse of a service response.
  • Negative test: removing the app-root resolution (fix(cli): every ui:* command writes to the workspace root instead of apps/<app>, and the E2E gate asserts the wrong root #1356) makes the generator fail loudly
    rather than write outside apps/<app>/.
  • gate: deno task e2e:cli run scaffold.runtime --cleanup covers generating a slice and
    type-checking the app that contains it.

Boundaries

Docs/consumer proof

Adoption is proved by a generated project, not by prose: the scaffold.runtime E2E generates a
slice and type-checks it; the app AGENTS.md/WEB-LAYER.md reference the verb as the first step of
the one-screen path; and a consumer-shaped check re-derives the eis-chat conventions S1–S4/S7 from
generated output rather than from docs/design/BUILD-CONTRACT.md in a downstream repo
(research/external/eis-chat.md §11).

Provenance

Seed run plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Drafted from
research/repo-audit/mcp-cli.md §4.2/§5, research/repo-audit/web-layer.md §2.1,
research/external/eis-chat.md §11–12; all repo claims re-verified against worktree
fac9e339042c. No GitHub mutation performed.


Filed from planning seed PR #1347 · source Draft-ID T2-01 · live issue #1354.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions