Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .claude/hooks/require-scaffold-with-src.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@
#
# What a hook CANNOT do: know WHETHER a given change actually needs a
# scaffold demo (a bug fix, an internal perf change, or a tweak to an
# already-demoed feature may not). So it enforces the floor (a
# feature-source commit must stage SOME scaffold surface OR consciously opt
# out) and points at the webjs-scaffold-sync skill for the per-surface walk
# and the mandatory generate-boot-check.
# already-demoed feature may not), nor tell a real demo from a doc bullet.
# So it enforces only the FLOOR (a feature-source commit must stage SOME
# scaffold surface OR consciously opt out) and points at the
# webjs-scaffold-sync skill for the per-surface walk and the mandatory
# generate-boot-check.
#
# The TIER-2 teeth live in CI, not here: test/scaffolds/gallery-coverage.test.js
# reconciles the live framework surface (@webjsdev/core + @webjsdev/server exports
# AND the routing convention files the router parses) against
# test/scaffolds/gallery-coverage.json and FAILS when a new one is neither
# demonstrated by the scaffold nor consciously exempted. That is the un-skippable
# gate (the analogue of a test that must exist AND pass); this hook is the fast
# commit-time reminder in front of it.
#
# Scope: only fires on `git commit` Bash calls. Inspects the STAGED diff,
# so `git add` choices drive it.
Expand Down
38 changes: 31 additions & 7 deletions .claude/skills/webjs-scaffold-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,36 @@ They overlap on two surfaces (the scaffold's per-agent rule files, and the
template matrix in the framework docs/README). Whichever skill reaches that
surface must update it; when in doubt run both.

A hard commit gate enforces the FLOOR: `.claude/hooks/require-scaffold-with-src.sh`
BLOCKS a commit that stages framework-feature source (`packages/(core|server|cli)/src`)
with no scaffold surface (`packages/cli/templates` or `packages/cli/lib`) in the
same commit (escape hatch `WEBJS_NO_SCAFFOLD_GATE=1` for a change that genuinely
needs no scaffold update). The hook can only enforce "stage SOME scaffold
surface"; THIS skill does the substantive per-surface judgment and the
generate-boot-check verification.
Enforcement is TWO tiers, deliberately mirroring how tests are enforced (a
commit-time floor plus an un-skippable CI gate):

- **Tier 1, the commit floor.** `.claude/hooks/require-scaffold-with-src.sh`
BLOCKS a commit that stages framework-feature source (`packages/(core|server|cli)/src`)
with no scaffold surface (`packages/cli/templates` or `packages/cli/lib`) in the
same commit (escape hatch `WEBJS_NO_SCAFFOLD_GATE=1`). Like the test commit
gate, it only proves you *touched* a scaffold file; it cannot tell a real demo
from a doc bullet, which is exactly how #848 slipped (forbidden()/unauthorized()
staged doc bullets, shipped no gallery demo).

- **Tier 2, the CI coverage gate.** `test/scaffolds/gallery-coverage.test.js`
reconciles the LIVE framework surface against the hand-curated
`test/scaffolds/gallery-coverage.json` manifest and FAILS when a new surface is
neither demoed nor exempted. It gates THREE surfaces: **`@webjsdev/core`
exports** (a `{ demo }` pointing at a gallery file that references it),
**`@webjsdev/server` exports** (`{ demoed: true }`, verified by a generated app
importing it), and **routing convention files** (the stems the router parses,
DERIVED from `packages/server/src/router.js` so a new `stem === '...'` branch
auto-appears, each demonstrated by a file in a generated app). Every entry is
`demo`/`demoed` or `{ exempt }` with a reason (`internal: ...` for plumbing,
`deferred: ...` for an agent-facing surface not yet demoed). It runs on every
`npm test` and in CI, so it cannot be skipped with a local `--no-verify`, the
analogue of "a test must exist AND pass": a new export or convention turns CI red
until it is classified. **When you add or rename a core/server export, or add a
routing convention file the router parses, update the manifest** (a demo, or an
honest exemption), the same reflex as writing a test.

THIS skill does the substantive per-surface judgment and the generate-boot-check
verification that neither tier can automate.

## The complete scaffold surface map

Expand Down Expand Up @@ -94,6 +117,7 @@ it applies, then update or consciously skip each.
| New / changed **gallery or showcase demo** | the template file(s) or generator strings for the demo + the home-page `features`/index array + the scaffold AGENTS.md gallery list + `test/scaffolds/*` FEATURES/assertions + **generate + boot the affected template** |
| New / removed **template** | the `create.js` template branch (+ a `*-template.js` if large) + the "only N templates exist" list in EVERY per-agent rule file + the framework `AGENTS.md`/getting-started/README template matrix + the CLI `--template` validation + `--help` + `test/scaffolds/*` |
| New **control-flow throw or routing boundary file** (`notFound` / `redirect` / `forbidden` / `unauthorized` and their `not-found` / `forbidden` / `unauthorized` / `error` / `loading` / `global-error` / `global-not-found` boundary files) | a runnable **gallery demo** that exercises it (a route that throws it plus the nearest boundary file), NOT just an app-tree bullet in the rule files + the home-page `features` array + `test/scaffolds/*` FEATURES/boundary-file asserts + **generate + boot + hit the route**. A doc bullet in `AGENTS.md` / `CONVENTIONS.md` is necessary but NOT sufficient: the gallery is the primary teaching surface, so an undemoed thrower is invisible to a scaffolding agent (the #848 gap). Carve-out: a **root-only** boundary (`global-error` / `global-not-found`) cannot mount under `app/features/` without clashing with the generated app root, so teach those in the demo's PROSE rather than as a live route. |
| New / renamed **public `@webjsdev/core` or `@webjsdev/server` export, or a new routing convention file** the router parses | `test/scaffolds/gallery-coverage.json` MUST classify it (a `{ demo }` / `{ demoed: true }`, or `{ exempt }` with an `internal:` / `deferred:` reason) or the tier-2 CI gate (`gallery-coverage.test.js`) FAILS. Prefer a real demo; `deferred:` is a conscious, reviewer-visible exemption tracked for later. This is the coverage-gate teeth described above. |
| New **convention/rule** for generated apps | ALL per-agent rule files in lockstep (surface 3) + repo `CONVENTIONS.md` if the repo demonstrates it + `agent-docs` only if it also changes a framework API |
| Changed **generated file** (layout, theme, home, schema, middleware) | the generator (`create.js`/`*-template.js`) + any scaffold test asserting it + any doc/preview describing it + **regenerate + boot** |
| New **scaffold-shipped config/hook** (`.hooks/`, `webjs.*` in the generated `package.json`, a check rule) | `templates/**` + `webjs doctor`/`check` that reads it + the per-agent rule files if agents must know it |
Expand Down
12 changes: 12 additions & 0 deletions agent-docs/framework-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ The fix only repairs the LOCAL checkout. Commits and branches are always safe on

---

### Scaffold teaching-coverage gate (`gallery-coverage.test.js`)

The scaffold is webjs's primary teaching surface for AI agents, so a new framework feature must ship a runnable gallery demo, not just a doc bullet. Enforcement is two tiers, mirroring how tests are enforced:

- **Tier 1 (commit floor):** `.claude/hooks/require-scaffold-with-src.sh` blocks a commit that stages `packages/(core|server|cli)/src` with no scaffold surface. It only proves you touched a scaffold file, so a documented-but-undemoed feature can still pass (this is exactly how #848 shipped `forbidden()` / `unauthorized()` with app-tree bullets and no demo).

- **Tier 2 (CI gate):** `test/scaffolds/gallery-coverage.test.js` reconciles the LIVE framework surface against `test/scaffolds/gallery-coverage.json` and FAILS when something new is neither demoed nor exempted. It gates three surfaces: `@webjsdev/core` exports (a `{ demo }` gallery-file pointer), `@webjsdev/server` exports (`{ demoed: true }`, verified by a generated app importing it), and routing convention files (the stems DERIVED from `packages/server/src/router.js`, each demonstrated by a file in a generated app). It runs under `npm test`, so a local `--no-verify` cannot skip it: a new export or convention turns CI red until classified. The `reconcile()` / `reconcileSet()` cores are pure and their failure modes (new name, stale key, missing/over-claimed demo, empty reason) are proven with synthetic inputs alongside the real-surface assertions. The deferred backlog is tracked in #859.

**When you add or rename a `@webjsdev/core` export, update the manifest** the same way you write a test: add a demo pointer, or an honest exemption. The gate covers the export surface (where the #848-class throwers live); a new routing convention FILE type (a `*.ts` boundary) is not an export, so it stays tier-1-only for now.

---

### Changelog: per-package, per-version, auto-generated

WebJs ships per-package per-version changelogs under `changelog/<pkg>/<version>.md`. The model: **a version bump is the trigger**. When any commit on `main` changes the `version` field in `packages/<pkg>/package.json`, the scripts/backfill-changelog.js generator emits a new `changelog/<pkg>/<version>.md` summarising every conventional-commit (`feat:` / `fix:` / `breaking:` / `perf:`) that landed in that package since the prior bump. The website renders the union of all packages' files at `/changelog`.
Expand Down
14 changes: 13 additions & 1 deletion packages/cli/templates/gallery/app/features/routing/[id]/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
// params, so PageProps<'/features/routing/[id]'>['params'] narrows to
// { id: string } automatically. Rename the folder and the type follows; pass a
// route literal that does not exist and it is a compile error.
import { html } from '@webjsdev/core';
import { html, notFound } from '@webjsdev/core';
import type { PageProps } from '@webjsdev/core';

export default async function RoutingParam({ params }: PageProps<'/features/routing/[id]'>) {
// The Next-style await also works; `params.id` sync would be identical.
const { id } = await params;
// Throw notFound() to short-circuit into the nearest not-found boundary (404).
// Here the reserved id "missing" stands in for "no such record"; in a real app
// you throw this after a DB lookup returns nothing. Try /features/routing/missing.
if (id === 'missing') notFound();
return html`
<h1 class="text-h2 font-bold mb-4">Route param</h1>
<p>The <code>[id]</code> segment is: <strong>${id}</strong></p>
Expand All @@ -28,6 +32,14 @@ export default async function RoutingParam({ params }: PageProps<'/features/rout
<code class="font-mono">params</code> is awaitable too:
<code class="font-mono">const { id } = await params</code> works, same value.
</p>
<p class="text-muted-foreground text-sm mt-3">
Throwing wins over rendering: <a class="text-primary" href="/features/routing/missing">/features/routing/missing</a>
throws <code class="font-mono">notFound()</code> and renders the nearest
not-found boundary at 404. See the
<a class="text-primary" href="/features/boundaries">Boundaries</a> demo for
<code class="font-mono">forbidden()</code> and
<code class="font-mono">unauthorized()</code>.
</p>
<p class="mt-3"><a class="text-primary" href="/features/routing">Back</a></p>
`;
}
Loading
Loading