docs(objectql): state the per-row before* contract in the two hook-wrapper docblocks - #18637
Merged
huangyiirene merged 3 commits intoSep 17, 2026
Merged
Conversation
…ore* contract `pickRecordPayload`'s docblock and `pickPreviousPayload`'s "when `previous` is ABSENT" list both stated the retired batch model in the present tense: that a predicate (`multi: true`) bulk write's `before*` dispatch fires once for the batch with no prior row, so `previous` stays unbound. ADR-0058 Addendum II (ruling #16074, clauses D1/D2) retired that. The engine dispatches `before*` per matched row and binds that row's pre-image (`dispatchPerRowBeforeHooks`, `previous: coerceBooleanFields(schema, row)`). The only `before*` context that still names no row is the opt-in `dispatchUnscopedMultiWrite` dispatch, fired once for a `multi: true` write carrying no caller predicate at all — which is what the second docblock now names. Prose only: no behaviour change, no assertion change. Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af Co-authored-by: Claude <noreply@anthropic.com>
…ocks Brings in PR #18625 (card #17975), the sibling prose repair in the same contract family, so this branch's wording can be aligned with the spelling that actually shipped. Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af Co-authored-by: Claude <noreply@anthropic.com>
…bulary, add the changeset Follow-up to the same two sites after merging PR #18625 (card #17975), the sibling prose repair that landed in this package. - Both sites now use the package's shipped spelling for the dispatch model: per matched row, on the single-record shape, ADR-0058 Addendum II D1/D2. - The second site no longer names the opt-in `dispatchUnscopedMultiWrite` dispatch. `wrapDeclarativeHook` is applied at registration (`hook-binder.ts:221`), but no metadata binding can set that flag today, so naming it as an absence case this function observes would be a claim that cannot be shown to be reachable. The truthful statement — any update-shaped context whose prior row is not in hand — is true by construction. - Adds the `patch` changeset: the first docblock is emitted verbatim onto the exported `hookRecordState` declaration in the published `.d.ts`. Prose only: no behaviour change, no assertion change. Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
huangyiirene
marked this pull request as ready for review
September 17, 2026 11:36
huangyiirene
deleted the
claude/issue-18331-hook-wrappers-per-row-docblocks
branch
September 17, 2026 12:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18331
Clause-②: no
Two docblocks in
packages/objectql/src/hook-wrappers.tsstated the retired batch model for thebefore*dispatch of a predicate (multi: true) bulk write in the present tense, while ruling #16074 / ADR-0058 Addendum II (D1/D2) and the engine beneath them both dispatch per matched row and bind that row's pre-image. Prose only: no behaviour change, no assertion change.Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
The two sites, located by symbol
hookRecordState(it opens "Choose the record-shaped object the condition formula evaluates against", and describespickRecordPayload)before*dispatch still fires once for the batch with no prior row, so that payload is left exactly as it is"pickPreviousPayload, section "When it is ABSENT"previousstays unbound"What the code actually does, on this branch's base:
dispatchPerRowBeforeHooks(engine.ts) loops the matched rows and builds each context withprevious: coerceBooleanFields(schema, row); the call sites forbeforeUpdateandbeforeDeleteboth take that path whenever the object has hooks and at least one row matched. A live behavioural pin already asserts it —hook-condition-bulk-previous.test.ts, "the bulk write #5037 and #5038 both rejected now SUCCEEDS, firing per row", boots a real engine, runsmulti: truewith aprevious.done != truetransition condition onbeforeUpdate, and asserts one dispatch per row.R2 — does the text ship? Measured, and the two sites do NOT behave the same
Measured the way #17975 did, against the real published tarball (
npm pack @objectstack/objectql@17.4.0), then again against a local build of this branch's final head.dist/util-Dw5ZTIII.d.ts:8039and the matching.d.mts. The reason is worth recording:hookRecordStatehas two leading comment blocks in the source — the detachedpickRecordPayloaddocblock, then its own — and the dts bundler emits both verbatim ontodeclare function hookRecordState(...)(published.d.tsline 8061). That file is the shared chunk behind both the.and./coreentrypoints, so every consumer's editor surfaces the retired sentence on hover.fires ONCE for N matched rowsis absent from every file in the published tarball.pickPreviousPayloadis module-private; it appears indist/only as an{@link}reference inside another docblock.dist/util-DWbR4Mtj.d.ts:8223+.d.mts, site 2's unique marker ("any update-shaped context") is insrconce and in zero dist files, and neitherfires oncenorfires ONCEsurvives anywhere indist/.Because site 1 moves published text, this carries a
patchchangeset, matching the four in-repo precedents for prose repairs inside published packages and the sibling PR #18625. It adds no exported symbol, no key and no accepted value — the accept set was widened by PR #17249 inpackages/spec, not here — henceClause-②: no.What the second site says instead, and why it does not name a dispatch
The natural replacement for site 2 was to name the one
before*context that still carries no row — the opt-indispatchUnscopedMultiWritedispatch, fired once for amulti: truewrite with no caller predicate at all. That draft was withdrawn before pushing:wrapDeclarativeHookis applied at registration (hook-binder.ts:221), but the engine's own note records that no metadata binding can set that flag today, so naming it as an absence case this function observes would be a claim whose reachability cannot be shown. The sentence now states what is true by construction — any update-shaped context whose prior row is not in hand — and then says plainly that a predicate bulk write is no longer one of those.Vocabulary is deliberately aligned with what PR #18625 landed in this package (per matched row, on the single-record shape, ADR-0058 Addendum II D1/D2) rather than inventing a second spelling. #18625's own
ROW-INVARIANT-IN-EFFECTwording is about which rewrites are admitted, which neither of these two sites discusses, so it is not copied in as padding.Tests and gates, on final head
0cd458243apnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2— 299 test files, 4995 tests, all passed, lock verdictcommand-exit 0.pnpm --filter @objectstack/objectql typecheck— green, includingcheck:test-typecheck(40 files / 234 errors / 65 pinned signatures held in the shrink-only ledger, unchanged).turbo run build --filter=@objectstack/objectql14/14 successful.eslint . --no-inline-configover the whole repo: 6811 files, 0 errors, 0 warnings. No narrowing was needed, so no narrowing is claimed.scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack: 54 derived, 54 run, 0 unrun (reconciled with--ran, each line carrying its exit code captured before any pipe). 51 green; the three non-zero ones are below.pnpm check:cross-package-test-inputspackages/spec/dist/exists, which the dependency-closure build creates. The flag is rooted inpackages/cli/test/init-created-files-summary.e2e.test.tsdescendingpackages/spec/dist/; neither path is in this diff. Named, not chased, and no twin card filed.pnpm check:dual-build-cjs-loadsdist/in this worktree. Nothing was measured — recorded as NOT MEASURED, not as a pass and not as a red. CI'sBuild Corecovers it.node scripts/check-engine-split-ratio.mjs --days 90Acceptance notes
R3, judged occurrence by occurrence — both extra name-matches are false positives, and no edit is owed to either. Sweeping
fires once/fires ONCEoverpackages/objectql/srcreturns 4 hits: the 2 repaired here, plushook-input-shape-contract.test.ts:136— "before*now fires once PER MATCHED ROW (D1), so the count is the row count". This states the current contract, correctly, and the assertion under it (expect(seen).toHaveLength(2)for a two-row batch) pins the per-row model. Correct as written.plugin-metadata-event-outage.test.ts:33— "It would also fire once per event during an outage, where the boot line fires once per process". A statement about a degradation log line's emission frequency; unrelated to hook dispatch in every respect. Correct as written.Neither file was touched. No follow-up card is owed for either on this measurement; the seat decides.
Under-count check inside the declared surface. The rest of
hook-wrappers.tsmentions the batch model in five more places, and every one of them is already correct: theHookConditionErrornote ("thebefore*phase was dispatched ONCE for the whole batch") and the retired-helpers block are past-tense history, which is correct as history; "The batch-scopedhookContextstill exists insideupdate()/delete(), but it is never handed totriggerHooks" is a true present-tense statement; and theunevaluableConditionErrornote already says "a predicate write dispatchesbefore*once per matched row now". Only the two cited docblocks were defective, so the card's count is exact for this file.Noted, not filed. The detached docblock shape at site 1 — a block describing
pickRecordPayloadleft sitting abovehookRecordState, which the dts bundler then publishes as if it were part ofhookRecordState's own documentation — is an observation about layout, not a defect in any of the three filing classes: the text is accurate after this change and the emit is TypeScript behaving as documented. Successor that would meet it: any PR re-documenting these helpers. Recording it because it is why site 1 ships at all, which was not obvious from the source.Out of surface, untouched:
packages/objectql/CHANGELOG.mdstill carries the retired phrasing in released entries. That is a dated record of what was true then and is correctly left alone.Generated by Claude Code