Beta 0.40.0
Schema-level hooks
Hooks now live at the top of the schema as a named, BQL-shaped registry, not embedded inside each kind. The mutation pipeline routes per-unit with precise validate/transform/effect gating, the commit-event diff records exactly which keys moved, and Studio surfaces the registry as a first-class section. Pre-launch breaking change: legacy per-kind hooks blocks are no longer accepted on import.
definition_mutateknows about hooks. Create / update / delete / query via{ "$type": "hook", "name": "..." }. Schema events emit$sid(hook name) for hook ops and$didfor kinds/fields, so audit subscribers can distinguish the two without parsing the type.- Per-unit hook gating. The validate / transform / effect gates short-circuit at the unit level — a batch that mixes a hooked kind (e.g.
Post) with an unhooked kind (e.g.User) only runs hooks againstPost. Multi-subspace batches stay correct because the gate inspects each unit's subspace. - Hook dependency validation on schema deletes. Deleting a kind or field that a hook still targets fails with a clear error before any prefix is written. Renaming a field that a hook filter references in the same batch is allowed if the hook update lands first; otherwise the batch is rejected as dangling.
- Transform conflict detection across hooks. When two top-level transform hooks write the same leaf path on the same unit during one pass, the batch errors out instead of silently picking the last writer.
- Studio Hooks sidebar. Schema sidebar shows a
Hookssection with aZapicon, hook name, and type chip (unit.validate/unit.transform/unit.effect). Renders even when there are no kinds yet, so freshly-imported hook-only schemas are visible.
- Commit-event diff is precise for hook updates. Only the keys that actually changed appear in
changed/details. Optional fields removed by the update (e.g. droppingwhenortimeoutMs) are recorded with anullsentinel so audit subscribers can tell "removed" apart from "unchanged". - Schema version stays consistent on dep-check rollback. Hook CRUD no longer inline-persists the schema version — the outer dispatcher does it after
validate_hook_dependenciesclears, so a rejected batch leaves the on-disk version exactly where it was. - Bulk hook load on schema open.
set_hooks_bulkindexes the entire hook set once instead of rebuilding the index on every insert. - Duplicate-kind entries in
$kinds.$anycollapse to one index slot, so["Post", "Post"]no longer fires the hook twice.
- Top-level effect snapshot path matches per-unit gating. A batch touching only unhooked kinds no longer pays the snapshot cost when the schema declares effect hooks elsewhere.
definition_importrejects the legacy per-kindhooksblock with an explicit error instead of silently dropping it.
- Hooks move to
schema.hooks. The per-kindhooksblock onKindDefis removed. Hooks now live in a top-level map keyed by hook name (schema.hooks["Post.audit"] = { type, target: { ops, $kinds, $filter }, $js }). Existing definitions must be rewritten — there is no transparent migration. schema.hookstargets are BQL-shaped. A hook declares its target with{ ops, $kinds, $filter }.opsacceptscreate | update | delete | query;linkandunlinkare rejected at parse time (useupdateon the role/link field for those).$filterrejects arc-field paths.- Hook arity per name is 1. Each hook is identified by name and replaces in place —
updateis full-replace semantics. Use distinct names to attach multiple hooks to the same kind.