feat(blocks): NODE_ENV dev default, deco schema --check, multivariate stops at the first match

TL
2h ago
+468-42

What this PR does

Three product-owner decisions (2026-10-14/15) in @decocms/blocks. The rest of the stack (#694 and up) merges this branch.

1. createCMS reads NODE_ENV again to decide dev mode (d03448c1)

  • With no dev option, a createCMS with site stays on local files when process.env.NODE_ENV === "development". Hosted releases are never swapped in, and drafts still load.
  • The read is guarded (typeof process !== "undefined" && process.env != null && …), so a Worker or browser without process counts as production. It is spelled process.env.NODE_ENV so a bundler's define can replace it.
  • The explicit dev option, which shipped in 8.0–8.2, stays as an override in both directions (dev: false forces swaps even when NODE_ENV=development).
  • This is the SDK's only environment read. noEnv.test.ts allows exactly that one line. remoteLoader itself still reads no environment.
  • Conformance: four new H-5 tests (default, override, production or unset, no process). Each was mutation-checked.
  • Removing isNextDev from @decocms/nextjs, the migrator, the skill and the smoke apps happens in #697 and #700, not here.

2. deco schema --check (fa834811)

  • For CI: generates the schema in memory, writes nothing, and exits 1 when .deco/schema.gen.json is missing or differs from what deco schema would write. The message says what moved:

    • blocks that are new in code or gone,
    • the number of changed definitions,
    • the generating version,
    • or formatting only.

    It ends with: Run deco schema and commit .deco/schema.gen.json.

  • Schema errors exit 1 with the errors. --check and --watch can't be combined, and there is no dev watch.

  • Usage line: deco schema [--root <dir>] [--watch | --check]. Seven tests in cli/run.test.ts, including one through the bin under plain Node. dd-06 follows the new usage line.

  • The migrator adds it to each site's CI workflow in #700.

3. multivariate stops at the first matching rule, as v7 did (ac23a701)

  • Before this change, the resolver resolved every variant's rule before the built-in multivariate ran. A matcher after the winner still ran, so a random rule there drew a number and recorded a cohort.
  • Now the built-in multivariate (marked the way lazy is) gets its rules resolved in order, one at a time, up to the first true one. Later variants are left out and their matchers are never called.
  • A multivariate that the site declares in its block map replaces the built-in and still gets every rule resolved, like any other block.
  • Legacy multivariate types (website/flags/multivariate.ts, …) go through the same path.
  • builtin-03/mv-02 now claims first-match order. That test changed because the docs claim changes. New tests cover in-order evaluation, a random rule after the winner never drawing, legacy types, and the block-map override. Mutation-checked: 4 fail without the change.
  • CLAUDE.md now says that lazy and the built-in multivariate are the resolver's special cases.

Docs site follow-up (deco-sites/docs-tanstack, /next/*)

  • api-reference / releases-and-drafts: dev defaults to NODE_ENV === "development" (guarded; false without process). Pass dev to override it, e.g. dev: import.meta.env.DEV on a Worker. Remove "the SDK reads no environment variable" wherever it refers to dev.
  • cli / checking: deco schema --check (usage line, exit codes, the message) and a CI snippet that runs deco schema --check.
  • matchers-and-variants line 75: replace "Rules aren't lazy: they're cheap booleans and always run." with "Rules run in order and stop at the first true one, as in v7: a later rule's matcher never runs, so a random rule after the winner draws nothing and sets no cookie. A multivariate you declare in your block map gets every rule."
  • how-resolution-works: lazy is no longer the only special case. Add the built-in multivariate's in-order rules.

Gates

  • bun run check passes.
  • bun run build passes.
  • skills:check and skills:readme:check pass.
  • A plain-Node import of every built export works (22), and so does deco --help.
  • bun run test (2 GB): 2092 pass, 3 fail, 95 skipped.
    • DS-2 is a known failure on main since #689.
    • Two schema.test.ts tests hit the 5 s timeout under full-suite load. They pass alone (116/116 with noEnv, guides.cli and telemetry).

Draft shape and block JSON: unchanged (2026-10-16)

Two who-edited-what experiments came and went on this branch, and the net change is none:

  • A top-level edits field on the draft (00125500), reverted in a886d720.
  • A reserved __meta key on saved blocks that the resolver dropped (b1cd9a13, 36fb75f1), reverted in the commit on top. Both reverts are merged up the stack.

PO: "only have metadata and changed by on v8 hosted". Studio keeps who and when in the hosted draft object's storage metadata, so the draft stays exactly { set, delete } and blocks has no reserved metadata key. The tree at M2 equals its state before these commits.

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

Main refresh (2026-10-17, 63b9e6d2)

  • Merged main 8bd3d88c (#709) into this branch (normal merge, no conflicts), then merged up the stack to M2 #707.
  • Gates at the top of the stack (M2 5131a3c1): bun run check, bun run build, skills:check, skills:readme:check pass. bun run test (2 GB memory cap): 122 files pass, 1 skipped; 2,745 tests pass, 95 skipped, 0 fail. DS-2 now passes (fixed on main by #709); no timeouts this run.
  • CI on this PR's new head: all checks pass.

šŸ¤– Generated with Claude Code