Commit e88289b
docs: correct
Fixes #17976
`Clause-②: no`
Prose-only realignment onto the already-declared contract: no
accept/reject behaviour moves, no public surface widens, no schema or
runtime code is touched.
## What was wrong
Both pages still taught the batch-dispatch model that ADR-0058 Addendum
II (#5574) retired. `before*` hooks on a predicate (`multi: true`) write
dispatch **once per matched row**, each on a single-record-shaped
context carrying that row's `id` and `previous` — the premise the #16074
ruling was written on and PR #17249 landed into the contract.
Verified on `origin/main` before rewriting, not taken from the card:
- `packages/objectql/src/engine.ts` — `dispatchPerRowBeforeHooks`,
docblock clauses D1/D2: "one dispatch per matched row, on the
SINGLE-RECORD shape … `previous` is that row's pre-image".
- `packages/spec/src/data/hook.zod.ts` — "Since #5574's engine half it
fires them once PER MATCHED ROW, each on a single-record-shaped context
carrying that row's `id` and `previous`".
- `packages/objectql/src/bulk-write-per-row-hooks.test.ts` — `[#5574 /
D1] a bulk write fires before-hooks once per matched row`, `[#5574 / D2]
… binds THAT row's previous`.
- `docs/adr/0058-expression-and-predicate-surface.md` — Amendment II.3
(#16074), the row-invariant-in-effect rule.
## Five sites, not the two on the card
The card's repair asked for the neighbouring paragraphs. The sweep found
three more, all in the same retirement:
| Site | Was | Now |
|:---|:---|:---|
| `formulas.mdx` scope table | `previous` "unbound in `before*` hooks,
which fire once for the batch" | bound per row in both phases |
| `formulas.mdx` "Write it once" | framed per-row dispatch as an
`after*` property only | both phases |
| `formulas.mdx` ceiling paragraph | 10 000-row refusal attributed to
objects "with `after*` hooks" | one ceiling covers both phases, checked
before the first dispatch (D6) |
| `formulas.mdx` warn Callout | `beforeUpdate`/`beforeDelete` "fire once
for the whole batch", `previous` unbound, and a `previous`-reading
`before*` condition "fails the write with an error naming the batch" |
replaced — see below |
| `data-flow.mdx` hook-order prose | "The `after*` events **instead**
dispatch once per matched row" | both phases, with the surviving payload
asymmetry named |
The Callout was the worst of the five: besides the dispatch claim it
advertised a **diagnostic that no longer has a producer**.
`HookConditionLimitation` was retired at #5574
(`packages/objectql/src/index.ts`: "was exported here until #5574 and is
RETIRED"; `hook-wrappers.ts`: "its two `HookConditionLimitation` members
had no producer — retired under ADR-0049"). An author was being promised
a guard-rail that was removed. It now carries the rule that actually
binds today: the payload stays batch-scoped, so only a
**row-invariant-in-effect, in-place** rewrite is in contract, backed by
the engine's `MULTI_UPDATE_HOOK_KEY_DIVERGENCE` refusal — including the
two shapes that slip past that guard and stay out of contract.
## Sweep method and hit counts
Ran over both pages in full, zero results given a firing control:
| Sweep | formulas.mdx | data-flow.mdx |
|:---|:---|:---|
| `grep -ni "batch"` | 4 hits → 3 in family, 1 benign (`read once for
the whole batch`, a true statement about the row read) | 2 hits → 1 in
family, 1 benign (`Query Optimizer … batch strategies`) |
| `grep -nEi "unbound\|not bound\|once per\|per matched
row\|before\*\|beforeUpdate\|beforeDelete\|after\*"` | 9 hits → 5 in
family | 4 hits → 1 in family |
| `grep -nEi "previous"` | covered by the above | 0 hits outside line
311 — **firing control**: the same pattern returns 4 hits in `skills/`
and 1 in `content/docs/releases/`, so the zero is a measurement, not a
dead pattern |
## Verification
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **40** commands from the committed
change set. All 40 run, **0 NOT-MEASURED, 0 UNRUN** (`--ran`
reconciliation). Four first refused with an unmet prerequisite (three
`exit 3`, one `exit 1` whose text is a prerequisite refusal:
`client-react` not built) — prerequisites built, then each re-run
**whole**, not patched around:
```
0 pnpm --filter @objectstack/lint run check:doc-formula-expressions
0 pnpm --filter @objectstack/lint run check:doc-security-posture
0 pnpm --filter @objectstack/spec run check:skill-examples
0 pnpm check:docs-transcript-drift
```
Representative verdict lines:
```
✓ check-docs-transcript-drift: 4 declared transcript value(s) across 402 page(s) under
content/docs/ equal what the registry derives today, and no undeclared block quotes one.
✅ 258 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc
ran the SEMANTIC pass on all of them
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 438 files /
1377 TS blocks judged clean by @objectstack/formula.
```
No package is in the diff, so there is no dependency-closure build to
owe and no package test suite is affected. Control-character self-scan
over both files: clean.
## Changeset: skip
Measured, not assumed. Across the **70** packages declaring a `files[]`,
neither edited path falls inside any of them; `content/` sits at the
repo root, outside every package directory, and the root manifest is
`private: true`. Positive control on the same matcher:
`packages/spec/dist/index.js` resolves to `@objectstack/spec` via
`dist`, and `packages/spec/README.md` via `README.md`. Nothing published
moves.
## Acceptance notes
**To file — the same retirement on a PUBLISHED, governed surface,
outside this card's file face.** Two customer-shipped skill files carry
the identical false claim, and both additionally restate the retired
rejection-by-name diagnostic:
- `skills/objectstack-formula/SKILL.md:308` — the `previous` binding
table: "**unbound** — a `before*` hook fires ONCE for the whole batch …
so there is no single prior record".
- `skills/objectstack-data/references/data-hooks.md:240-248` — the same,
plus "Reading `previous` on that dispatch is rejected **by name**, and
the rejection points you at the after-type event", which has had no
producer since #5574.
These contradict the declared contract in
`packages/spec/src/data/hook.zod.ts` (class **b**). They are strictly
worse than the two pages fixed here — `skills/**` ships to customers
*and* is read by AI authoring agents — but the file face for this card
is the two `content/docs` pages, so they are reported rather than
touched. Dedupe words: `skills before hook batch dispatch`,
`objectstack-formula previous unbound`, `data-hooks.md per-row before`,
`HookConditionLimitation retired skill`, `skill previous bulk write`.
**Noted, not filed — already carried.** #17975 covers the sibling
residue in `packages/objectql` source (`engine.ts` docblock,
`bulk-write-per-row-hooks.test.ts:570`), a different retired rule (the
"not so a rewrite can be aimed" sentence) on a different lane. #17975
remains open; carrier named, nothing to add here.
**Noted, not filed.** `formulas.mdx` line 388's "read once for the whole
batch" is correct as written (it describes the D7 single row read, not
hook dispatch) and was deliberately left alone; it is the benign hit the
sweep above accounts for. Carrier: none needed — no defect.
## Dedupe
The card's "no existing card found" rested on a semantic search that
returned `total: None` on its own control. Re-checked here: `GET
/search/issues` is **proxy-blocked in this environment** (HTTP 403,
"sessions are bound to their configured repositories"), so that endpoint
can never have produced a proven zero. Re-ran via repository-scoped
endpoints instead — all **522** open issues and **7** open PRs
enumerated and title-matched against the family; the only hits were
#17976 itself and #17975 (disposed above). `git ls-remote --heads
origin` shows no other branch on either page.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_
Co-authored-by: Claude <noreply@anthropic.com>before* bulk dispatch to the per-row model on two customer pages (#18220)1 parent 2d3d1c9 commit e88289b
2 files changed
Lines changed: 34 additions & 19 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
308 | 308 | | |
309 | 309 | | |
310 | 310 | | |
311 | | - | |
| 311 | + | |
312 | 312 | | |
313 | 313 | | |
314 | 314 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
205 | | - | |
| 205 | + | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
| |||
371 | 371 | | |
372 | 372 | | |
373 | 373 | | |
374 | | - | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | | - | |
379 | | - | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
380 | 381 | | |
381 | 382 | | |
382 | 383 | | |
| |||
385 | 386 | | |
386 | 387 | | |
387 | 388 | | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
392 | 397 | | |
393 | 398 | | |
394 | | - | |
395 | | - | |
396 | | - | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
401 | 416 | | |
402 | 417 | | |
403 | 418 | | |
| |||
0 commit comments