Skip to content

docs: correct before* bulk dispatch to the per-row model on two customer pages - #18220

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17976-before-hooks-per-row
Sep 14, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-17976-before-hooks-per-row

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

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:

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:

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.ai/code/session_017ef78bLdybu3AffehKkhfk


Generated by Claude Code

…tomer pages

Both pages still described the batch-dispatch model ADR-0058 Addendum II
retired: `before*` hooks dispatch once per matched row and `previous` is
bound per row, the premise the #16074 ruling was written on.

Five sites, not the two quoted on the card — the neighbouring paragraphs
carried the same retirement:

- formulas.mdx scope table: `previous` was documented as unbound in
  `before*`.
- formulas.mdx "Write it once": described the per-row model as an `after*`
  property only.
- formulas.mdx ceiling paragraph: attributed the 10 000-row refusal to
  objects with `after*` hooks; one ceiling covers both phases and is
  checked before the first dispatch.
- formulas.mdx warn Callout: stated `beforeUpdate`/`beforeDelete` fire once
  for the whole batch, that `previous` is unbound there, and that a
  `previous`-reading `before*` condition fails the write with a diagnostic
  naming the batch — that diagnosis is retired and has had no producer
  since the per-row dispatch landed. Replaced with the rule that is now
  load-bearing: the payload stays batch-scoped, so only a
  row-invariant-in-effect, in-place rewrite is in contract, backed by the
  engine's key-divergence refusal.
- data-flow.mdx hook-order prose: "The `after*` events instead dispatch
  once per matched row" implied `before*` does not.

Docs-only; `content/` is outside every package's `files[]`, so nothing
published moves.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 14, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 14, 2026
@claude
claude Bot marked this pull request as ready for review September 14, 2026 18:01
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit e88289b Sep 14, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-17976-before-hooks-per-row branch September 14, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: two pages still say before* hooks fire once for the batch — false since #5574, and it is the axis that decides how a hook must be written

1 participant