Skip to content

Warlock.js 5.38.0

Choose a tag to compare

@hassanzohdy hassanzohdy released this 25 Oct 21:25
· 163 commits to main since this release

Released 2026-10-25.

All 33 packages (@warlock.js/* and create-warlock) move to 5.38.0 together;
keep every family package on the same version. One change is breaking for typed
models (see section 1). The new defaults for test runtimes and the module boundary
apply to new apps or to new config only; existing apps keep their own settings.


1. Breaking changes

  • Typed model get() and set() reject unknown columns (cascade). For a model
    declared with a schema type, get() and set() with a top-level column name that is
    not in the schema are type errors. Add the column to the schema, use a dot path
    ("profile.city"), or declare the model without a schema type to keep loose access.
  • Nothing else in 5.38.0 is marked breaking. The changes below change behaviour
    without a type error. Review them before upgrading:
    • A 5xx is logged as one record with a new action name (section 2); log queries or
      alerts that match the old entries need updating.
    • A 401 or 403 on a page route renders the application's forbidden page (or the
      framework page) instead of a raw JSON body (section 7).
    • web.moduleBoundary defaults to "warn" for existing apps (section 4).

2. Request log (core, devtools, logger)

  • Persistent request log. With http.logging.requests on, each request writes one
    redacted JSON record after the response is sent: id, trace id, method, URL (credential
    query values masked), route, params, query, headers, session user, IP, status, duration,
    phases, log entries, a query summary (count, slowest query, N+1 findings) and, for a
    5xx or a thrown request, the error with its stack, phase, page file and site.
  • Defaults. On in development and off elsewhere; in development every request is
    kept, elsewhere only errors. Records go to a daily file under storage/logs/requests/
    by default. slowMs, sampleRate, include (headers, body, query, params, session,
    response, stack, logs, queries, phases), maxBodyBytes (16 KB by default) and
    retention: { maxDays, maxSizeMb } tune it. Outside development only allowlisted headers
    are recorded, and bodies are left out unless include.body or include.response is set.
  • One error record per 5xx. The three earlier error entries (request-error,
    model-write-validation-error, unhandled-request-error) are replaced by one error-level
    record with module: "http" and action: "request.<phase>", logged once per request. A
    returned 503 is treated as an intentional answer and is not logged. A 4xx is never logged
    as an error. The per-request http.log line is unchanged.
  • Warlock CLI and MCP. warlock logs requests (filters --status, --route, --since,
    --limit, --json) and warlock logs request <id> read the log without booting the app.
    The MCP tool request_log is read-only. A request-log skill ships with the release.
  • Devtools. The Requests panel reads the persisted log, so history survives a restart,
    and it filters by status, route, method, time and text. It copies any request as curl,
    with redacted values left as [REDACTED].
  • Agents tab. The development dashboard at /__warlock shows per-turn agent traces
    (section 5). It needs no Panoptic.

3. Tests (core, create-warlock)

  • Unit and integration projects. *.unit.spec.ts and *.unit.test.ts run in a Vitest
    unit project with no setup files and no app boot. Every other spec, and anything under
    tests/, runs in integration. guardUnitProject() fails a unit spec that imports a
    connector, the connectors manager, bootstrap or a test-server helper.
  • Worker lifetime for new apps. warlock add test and the starter set
    lifetime: "worker" and resetDatabase: true in src/config/tests.ts, and use one
    database per Vitest worker. WARLOCK_TEST_LIFETIME=file switches back. Existing files
    are never overwritten.
  • Per-file reset in worker lifetime. Open test HTTP servers are closed, the request
    context is cleared, the route table returns to the boot snapshot, and a real in-memory
    cache is flushed. setupTest({ lifetime: "file" }) opts one file out of the shared
    runtime.
  • Boot profile. tests.profileSetup: true or WARLOCK_TEST_PROFILE=1 prints per-phase
    timings, including each connector. WARLOCK_TEST_PROFILE_JSON=<path> writes them as JSON.
  • Fixes. The HTTP test server registers the connectors warlock.config.ts declares, so a
    page request no longer answers 404. A test run that booted the web connector exits cleanly
    (Vite, its HMR websocket and its watchers are closed on shutdown).

4. Build (web)

  • Payload budgets. warlock build measures build.budgets.payload for each page's
    translations and browser route table. A page over its limit fails the build with
    mode: "error", or prints a warning with mode: "warn".
  • Client chunk integrity. Each build writes client/.vite/warlock-integrity.json. The
    next build fails, naming each file, when a file name now maps to different bytes. The
    previousBuild option points the check at another build, such as the deployed release.
  • Stable chunk names. Production chunks are named after their final bytes. Byte-identical
    non-entry chunks are merged, and the manifest, preload lists, budget report and .br and
    .gz files use the final names.
  • Module boundary. The client build enforces web.moduleBoundary. Browser code may
    import only browser-safe files (*.client.ts(x), schema and type files, the module's
    web/ folder and non-code files). "warn" is the default, "error" fails the build and
    "off" disables the check. The starter sets "error".
  • Two tracing phases. translations and named-api-routes are reported through core's
    TracingHooks.onPhase.

5. Validation and AI (seal, core, ai)

  • Readable field names in validation messages. A field without a translated label is
    named in words: max_dumbbell_kg reads "Max dumbbell kg". Labels under attributes.<field>
    still win. validation.humanizeAttributes: false keeps the raw key.
  • Label check. warlock doctor check validation-labels (WL4002) is off by default
    and lists request-schema fields without a label when validation.warnMissingLabels is on.
  • Schema helpers. humanizeAttribute(key) is exported from seal.
    ObjectValidator#partial() keeps its types. .internal() marks a server-set field, and
    withoutInternal() and internalKeys() remove or list those fields.
  • OpenAI strict JSON Schema. An optional enum, literal, union or $ref is nullable in
    toJsonSchema("openai-strict"), so a client can send null. draft-2020-12 and
    openapi-3.0 nullable output is corrected too.
  • Tool schema lint. ai.tool() checks its schema against OpenAI strict, Anthropic and
    Google. In development it logs one warning per tool. lint: false or per-tool options
    tune it. describeToolSchema() and describeToolSchemas() return findings and an
    estimated token cost.
  • Tool-use contract (opt-in). Agents can set actWhenAsked, onToolRefusal and
    backstops. Agents without them behave as before.
  • Eval scorers. ai.eval.requiredToolCalled() and ai.eval.changeClaimedWithoutToolCall().
  • Agent traces. Each run records system prompt hash, tools offered, calls, refusals,
    retries, usage and cost. Traces are on in development and off under test and in
    production. trace sets this per agent. Traces are redacted and kept in an in-memory
    store. diffAgentTraces() and formatAgentTrace() help compare runs.
  • CLI. warlock ai tools lists per-tool token estimates and schema findings, and
    warlock ai trace lists or prints agent runs from the running dev server. warlock ai
    no longer marks --model as required in its option list.

6. Core tooling (core, logger, create-warlock)

  • warlock upgrade --check rehearses an upgrade on a scratch copy of the app and never
    writes the real project. It reports the breaking changes that affect the app and the
    outcome of each step. Child processes run one at a time with a memory cap
    (--memory-mb, default 4096) and a time limit (--timeout-minutes, default 20).
  • warlock doctor sites check renders each configured site's root page in
    development and fails (WL5005) on any 5xx.
  • Capabilities. defineCapability, provideCapability, useCapability,
    hasCapability and listCapabilities give a module-cycle-breaking slot a typed home.
    useCapability throws WL2008 when nothing provides it. warlock doctor warns about a
    required capability with no provider. A use-capabilities skill ships with the release.
  • warlock inspect --section wiring (and the MCP tool inspect_wiring) lists every
    auto-loaded file with its load order, then every capability slot with its provider and
    consumers.
  • Package manager detection follows the packageManager field, then the manager that
    installed node_modules, then the lockfile that matches package.json. A stale workspace
    file no longer wins. warlock update prints the manager it chose and why.
  • warlock agents-md lists the skill of a package that ships a single skills/SKILL.md
    (the AI provider packages and @warlock.js/sitemap).
  • Logger. LogChannel.passive marks a channel that only observes entries. The terminal
    fallback used when no log channel is configured ignores passive channels.
  • Loading the config keeps @warlock.js/* imports external, so a monorepo checkout no
    longer loads a second copy of core.

7. Web UX (web, create-warlock)

  • Forbidden page. forbidden.page.tsx (with an optional forbidden.setup.ts) is the
    page for a refused request. A 401 or 403 on a page route renders it with that status and
    receives { error, status } (plus debug outside production). The starter ships one, and
    requireUser({ userType }) and requireUser({ when }) now throw ForbiddenError (or
    UnAuthorizedError for a guest) on a page route instead of answering JSON. A guest with
    auth.pageAuth.loginPath is still redirected to sign in.
  • Framework fallback page. It shows the request id and a "Back to home" link. A 401 or
    403 gets its own "You don't have access to this page." message.
  • Development details. The error page receives debug outside production, and
    hydrating it no longer fails on that payload.
  • Multi-site dev server. The dev server accepts the hostnames in web.sites, which
    fixes the 403 "Blocked request" answer. Any host is allowed when a site is dynamic.
  • Route table fixes. apiHref() and useSubmitForm({ route }) find the route table
    during server render in development, including when the renderer is a separate module copy.
    Production pages resolve the table once per site and viewer kind.
  • Server-side browser globals. In development SSR, a *.client.* module that reads
    window, document, navigator, localStorage or another browser global while imported
    gets one warning that names the file and line.
  • Failure logs. A page failure is logged with its phase, page file, route, site and
    request id, and production pages stay generic.

8. Models (cascade)

  • Typed get() and set(). Both are typed from the model's schema. get("publishedAt")
    is Date | null, a column with a default is never undefined, and set(key, value) rejects
    a value of the wrong type. Dot paths stay loosely typed. A model without a schema type keeps
    loose access.
  • Types. ModelData<typeof schema>, ModelDataSnapshot<T>, ModelKey, ModelValue and
    ModelPath are exported.
  • Snapshots and request schemas. model.toDataSnapshot() returns a typed, read-only copy
    without hidden columns. Model.requestSchema() drops .internal() fields, and
    Model.create() still accepts them.
  • Assignability. A typed model is assignable to the loose Model and ChildModel again.
  • Development warning. Reading a schema column as a property (model.title) logs one
    warning per model and column that names model.get("title"). Nothing is installed in
    production.

9. Conventions (core, web, create-warlock)

  • Unit spec file names. Use *.unit.spec.ts or *.unit.test.ts for specs that do not
    touch the app. warlock add test, the starter and the template skills follow it.
  • Client-only boundary. *.client.ts(x) is a client-only boundary. Pages, layouts and
    components may import it, and it renders on the server unless wrapped in <ClientOnly>.
    Server-only code may not import it.
  • Per-feature locale files. src/app/<module>/utils/locales/<feature>.json merges into
    the module's utils/locales.json, under the module namespace unless the file names its
    own $group. A key may live in one file only.
  • Legacy locales are deprecated. An auto-loaded utils/locales.ts logs a deprecation
    warning that names every such file. Removal is planned for v6.
  • Optional barrels. generate: { barrels: false } in warlock.config.ts stops
    generators from writing index.ts barrels. The default stays true.
  • Starter. The starter keeps its translations in JSON files.

10. New check codes

  • WL9024 (warning): an integration spec that imports nothing needing the app. Move it to
    the unit project.
  • WL9021 (warning): a Cascade column read or written as a model property. warlock check --fix
    rewrites reads to model.get(...) and plain assignments to model.set(...).
  • WL9022 (warning): server-only code imports a *.client.ts(x) module, directly or through
    other files. The message names the import chain.
  • WL9105: a translation key of a module dictionary is missing a configured locale.
  • WL9023 (opt-in): a hand-maintained index.ts barrel in an app module. Enable it with
    rules.WL9023 in .warlock-check.json.
  • WL4002: request-schema field without a label (validation-labels, off by default).
  • WL5005: a warlock doctor site page answers 5xx.
  • WL2008: a required capability has no provider, or useCapability was called with none.

11. Upgrade notes

  1. Rehearse first. Run npx warlock upgrade --check. It lists the breaking changes that
    hit the app and runs typecheck, warlock doctor and the unit project on a scratch copy.
  2. Update the family. Run npx warlock update. It re-resolves the family packages in one
    install. npm 11.6.2 or newer is required for that step.
  3. Fix typed model calls. Add any column that get() or set() names but the schema does
    not declare, or switch to a dot path. Then run npx warlock check --fix to rewrite property
    reads and assignments to get() and set(). Review the result, because the rewrite covers
    only the statically recognised cases.
  4. Check the module boundary. Existing apps now get web.moduleBoundary: "warn". When your
    client code imports only browser-safe module files, set web.moduleBoundary: "error" in
    your web config (the starter sets it in src/config/web.ts). The default becomes "error" in a later release.
  5. Update log queries. Replace matches on request-error, model-write-validation-error
    and unhandled-request-error with action: "request.<phase>" on module: "http".
  6. Review page-route 401 and 403 handling. A refused page request now renders the forbidden
    page. Check any client code or test that expects a JSON body on a page route.
  7. Opt in to the worker test lifetime (optional). Existing apps keep their test setup. The
    test-runtime-worker-lifetime upgrade step lists where to opt in and edits nothing. Keep
    clearMocks in mind: Vitest 5 clears calls a module made when it was imported, so import
    such a module inside the test or set clearMocks: false.
  8. Move legacy locales (optional). Run npx warlock doctor --fix to convert
    utils/locales.ts to utils/locales/legacy.json, or to utils/locales.json when the module
    has none. The fix does nothing when a key appears in both files.
  9. Request log in production (optional). The request log is off outside development. Set
    http.logging.requests.enabled to turn it on, and set retention to bound disk use.