Uh oh!
There was an error while loading. Please reload this page.
fix(trigger-record-change): wire the hydration schema gate to the real getObject accessor - #8548
Conversation
…l getObject accessor (#8482) objectHasFormulaField gated the computed-field hydration re-read (a findOne on every afterInsert/afterUpdate dispatch) on an optional getObjectConfig accessor the concrete ObjectQL engine never implemented, so the gate always took its "re-read unconditionally" fallback in production, even for objects declaring no formula field where the re-read adds nothing. Points the gate at getObject instead -- the accessor the trigger already uses elsewhere (the unknown-object probe in start(), and buildContext's declared-field materialization since #4953) and the one the real ObjectQL engine actually implements. Retires the now-unreferenced getObjectConfig interface member and its doc comment. Perf-only; no output change. Re-proves the gate against a REAL engine (record-change-integration.test.ts, ObjectQL + driver-sql on better-sqlite3 :memory:) rather than trusting the trigger's own hand-mocked fakeEngine tests, which passed regardless of whether the gate could ever engage in production -- exactly the vacuity this card exists to close. Measured findOne delta on a real afterUpdate dispatch: 1 -> 0 for an object with no formula field, unchanged (1) for one that declares a formula field. Existing fakeEngine hydration-guard tests updated to mock getObject instead of the retired getObjectConfig, so they test the accessor the trigger actually reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARidKDYSCD56LaygrvDPnk
…mula-gate-getobject
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) reference the affected code. These are read-only:
|
os-zhuang
commented
Aug 13, 2026
PM review — ⭐ The decisive measurement was taken, and it decides cleanlyThe dispatch made one thing the whole verdict: measure the real-engine
Full elimination on the dominant case, provably unchanged on the case that still needs the read. ⭐ That second row is what makes this safe to land rather than merely fast: it shows the gate was wired without narrowing the path where hydration is load-bearing. ⭐ The vacuity trap was closed, not sidesteppedThe dispatch warned that a replacement test which simply mocks The unit tests were updated (necessary) and a real-engine pin added on PR #8483's kernel harness, spying on the engine's public ⭐ And this line is the one that makes the spy count trustworthy rather than plausible:
Without that, the count could have been contaminated by the engine's own prior-row read and the whole measurement would be worthless. Establishing what the instrument is actually counting before quoting a number is the difference between a measurement and a number. ⭐ Premise re-measured, as instructed, and the TEST_DEBT relay paid offAll three premise claims re-verified on today's And the trap I relayed from PR #8483 caught something real: this package's ⭐ The fix is exactly the right scope: corrected in the new tests only, leaving the 6 pre-existing sites alone. ⛔ Don't inherit someone else's debt into your PR, ⛔ and don't add to it either. Re-measured 9/9, net zero. ScopeRetiring the now-unreachable Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8482
What changed
RecordChangeTrigger.hydrateComputedFieldsre-reads the just-written record viafindOneon everyafterInsert/afterUpdatedispatch, to surface read-timeformulavirtual fields the raw lifecycle-hook row never carries. A schema gate (objectHasFormulaField) exists to skip that re-read for objects that declare noformulafield — the only thing it adds — but it gated on an optionalgetObjectConfigaccessor that the concrete ObjectQL engine never implemented. So on every real deployment the gate always fell through to itstruefallback and the re-read ran unconditionally, even for the common case of an object with no formula field at all.objectHasFormulaFieldnow reads the object's field map throughgetObject— the accessor the trigger already uses elsewhere (the unknown-object probe instart(), andbuildContext's declared-field materialization since #4953) and the one the real engine actually implements (packages/objectql/src/engine.ts:10608). The now-unreachablegetObjectConfiginterface member and its doc comment are retired.This is a perf-only change — no output changes, per the card's own framing.
Premise re-measured on today's
mainPer the unblock comment, re-verified rather than trusted (the card's own grep evidence was written from inside the #8483 worktree and is several merges old):
grep -rn "getObjectConfig" packages/onorigin/main(before this fix) hits onlyrecord-change-trigger.tsitself, its own test file, and historicalCHANGELOG.mdprose — no other package, no real engine.getObjectConfig. It hasgetObject(name): ServiceObject | undefinedatpackages/objectql/src/engine.ts:10608, andServiceObject.fieldscarries.type, which is exactly whatobjectHasFormulaFieldneeds.RecordChangeTriggerPlugin.resolveDataEngine(plugin.ts) still hands the trigger the rawctx.getService('objectql')(falling back to'data') with no adapter layer where agetObjectConfigshim could attach — confirmed unchanged.All three claims hold on
mainat the time of this PR.The vacuity trap this closes
The trigger's own unit tests (
record-change-trigger.test.ts, "computed-field hydration guards") only ever proved the gate against a hand-attachedgetObjectConfigmock (Object.assign(engine, { getObjectConfig })) — a true statement about the trigger's own logic that said nothing about the real engine, which is exactly how a gate that never engaged in production kept a green suite this long.Those tests are updated to mock
getObject(the real, now-used accessor) instead — necessary, but per the card's own warning, not sufficient: a replacement test that only swaps which method a fake mocks has reproduced the bug, not fixed it.The closing pin is a new describe block in
record-change-integration.test.ts(reusing PR #8483's real-kernel harness: ObjectQL +service-automation+ this trigger +@objectstack/driver-sqlon better-sqlite3:memory:) that spies on the engine's own publicfindOneacross a realafterUpdatedispatch:formulafield →findOnespy: not calledformulafield →findOnespy: called once, with the expectedwhere(The engine's own by-id-update prior-row fetch reads through
driver.findOnedirectly, never the public engine method, so this spy counts exactly the trigger's hydration re-reads and nothing else.)Reverse verification
Predicted before running: reverting only the implementation (tests kept) would turn exactly the two gate-specific tests RED — the fakeEngine "skips the re-read…" test and the new real-engine "does NOT re-read…" test — while the other 75 tests stay green (they test memoization / materialization / dispatch, not the schema gate itself).
Actual readings,
pnpm --filter @objectstack/trigger-record-change test -- --maxWorkers=2 --run:git checkout f1da948d8 -- .../record-change-trigger.ts, tests as in this PR)2 failed, 75 passed (77), exactly the two predicted tests77 passed (77)The real-engine failure on pre-fix code, verbatim:
Measured findOne delta (real engine, the card's decisive measurement)
Same real-kernel harness, one
afterUpdatedispatch, spying on the engine's publicfindOne:formulafieldfindOnecallfindOnecallsformulafieldfindOnecallfindOnecall (unchanged)Worth landing on this evidence: the no-formula case is the dominant one (the issue's own framing — "most objects have none"), and the delta is a full elimination of the re-read for that case, not a marginal reduction. The formula-field case is provably unaffected (same 1 read, same shape), so correctness is preserved on the path that still needs the read.
Tests
pnpm --workspace-concurrency=2 --filter '@objectstack/trigger-record-change^...' build— clean.pnpm --filter @objectstack/trigger-record-change typecheck— clean (note: this package'stsconfig.jsonexcludes**/*.test.ts, so this alone is not evidence about the new/edited test files — see the TEST_DEBT re-measure below).pnpm --filter @objectstack/trigger-record-change test -- --maxWorkers=2 --run— 6 test files, 77/77 passing (75 pre-existing/updated + 2 new real-engine tests).scripts/check-type-check-coverage.mjs'sremeasureProjectgenerates (same method PR fix(trigger-record-change): materialize declared fields on the seeded flow record #8483 used) — this package's frozen ledger entry is@objectstack/trigger-record-change: { errors: 9, note: 'TS2353 x9 ...' }. First pass came back at 11 (my two new tests used this file's pre-existing{ logLevel: 'silent' }shape, which isn't a realObjectKernelConfigfield — the same known TS2353 the frozen 9 already carry, just 2 more instances). Fixed by using{ logger: { level: 'silent' } }in my own new tests only (matching PR fix(trigger-record-change): materialize declared fields on the seeded flow record #8483's own precedent for its new tests, leaving the 6 pre-existing sites alone). Re-measured: 9/9, matching the frozen ledger exactly — net zero new debt.Gates
Derived via
node scripts/pm/dispatch-gates.mjs <changed paths>, plus the ones named on judgment:pnpm check:changeset-gate-self-tests— cleanpnpm check:objectui-changeset— cleanpnpm check:test-source-alias— clean, no new sitespnpm check:type-source-resolution— clean, no new sitesnode scripts/check-adr-0087-registration.mjs— cleannode scripts/check-changeset-no-major.mjs— clean (patch bump)node scripts/check-empty-changeset.mjs— cleanpnpm check:query-options-erasure— clean, ratchet holds, no new sitespnpm check:type-check-coverage(structural) — cleanpnpm check:nul-bytes— clean (repo-wide + explicit self-scan of changed files)pnpm check:engine-double-contract— clean (no new fake engine added; new tests drive a real engine)node scripts/check-slot-lookup-ratchet.mjs— clean, no new untypedgetServicesites (reused this file's existingTestObjectQLEngine/IDataEnginetyped pattern from PR fix(trigger-record-change): materialize declared fields on the seeded flow record #8483)Changeset
.changeset/trigger-record-change-formula-gate-getobject.md—@objectstack/trigger-record-changepatch.Generated by Claude Code