Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): discriminate a failed sys_organization probe from a genuinely empty one - #9817
Conversation
…nuinely empty one (#9261) `probeInstallOrganizations` answered every read failure with `[]`, which `resolveSystemWriteOrganization` reads as `no-organization-yet` — so one transient failure silently skipped both halves of the #8844 ruling (the single-organization stamp and the multi-organization refusal), and the memo cached the invented answer past the outage. Only the measured benign cause — an unprovisioned `sys_organization` table, asked through the shared `isMissingTableError` predicate — still answers the empty probe. Everything else propagates, envelope intact, and is not memoised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
…ss (#9261) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
…anization-probe-discriminate
…e erasure ratchet at 240 (#9261) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d670f6915e304f155fcf98bf25049c72f214e564 && git checkout d670f6915e304f155fcf98bf25049c72f214e564
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 593c4bf3f53d8b28899d33526108024d8ee12453 fccdc8e19cdd3cb81c4afe2fbd5f9de58c71a002 && git checkout -B drift-repro 593c4bf3f53d8b28899d33526108024d8ee12453 && git merge --no-ff fccdc8e19cdd3cb81c4afe2fbd5f9de58c71a002
node scripts/docs-audit/affected-docs.mjs --json 593c4bf3f53d8b28899d33526108024d8ee12453 |
…anization-probe-discriminate
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9261
probeInstallOrganizationsanswers the one question the #8844 system-writeorganization resolution decides on. Its read sat behind a bare
} catch { ids = [] }, so every failure was answered with the count that meansnone — and the answer was memoised, so the outage's consequence outlived the
outage.
The consumer consequence, stated as the mapping
resolveSystemWriteOrganizationmaps the probe's answer:no-organization-yetderivedrefuseERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED/ 500Before: a failed probe produced the 0 row. On a
single-posture installthat really holds one organization, hook / cron /
runAs: systeminserts landeduntenanted — filed under the
__global__pseudo-tenant, forking exactly theper-organization autonumber counter and partitioned unique index the ruling
exists to protect. On a multi-organization install the mandated refusal was
silently skipped — fail-open on a guard that must be loud.
After: a failed probe produces none of the three rows. The probe no
longer answers at all: the failure propagates with its envelope intact, so
resolveSystemWriteOrganizationis never reached, the write fails loudly, andnothing is written. And nothing is memoised — the next write re-probes rather
than inheriting the guess.
What is benign — measured, and it is ONE cause, not two
The dispatch expected two distinct benign errors and the card named
"
sys_organizationmay not be registered at all (a lean embedding, abare-kernel test)". Measured on this seam, the benign case is one, and it is
not the one the comment names. Every measurement below has its positive
control, and all of them are pinned in
packages/objectql/src/engine-organization-probe-outage.test.ts.sys_organizationroutes but its TABLE wasnever provisioned. It cannot hold a row, so zero really is the count, and
first boot still proceeds unstamped. Asked through the shared
isMissingTableErrorpredicate (@objectstack/metadata/errors, [metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825) — thesame call
resolveFileReferencesandcascadeDeleteRelationsalready make inthis file, never a hand-rolled code test.
the REGISTRY does not fail the read on a driver that tolerates an unknown
table. Measured:
findreturns[]through the normal path. A strict driversurfaces that same install as the missing table above. Pinned, with the
control that the read really did happen, so a later author does not write a
predicate for a case that cannot occur.
route
sys_organizationraises a bare[ObjectQL] No driver available for object 'sys_organization', whichisMissingTableErroranswersfalsefor, so keying on that predicate alonecould have been a fail-CLOSED regression. It is not, and the reason is
structural rather than lucky:
getDriveranswers every object from thedefault driver, which the first
registerDriveralways sets(
isDefault || drivers.size === 1) and which nothing ever clears — no driveris ever removed. So the only engine whose routing fails for
sys_organizationis one with no drivers at all, and there the write that would have asked
already failed on its own object, one frame earlier. Positive control for
that zero-hit claim: the test asserts the unroutable read really does raise
that error, then asserts the system insert stops on
dispatch_orderinstead.An earlier revision of this change carried a structural pre-check
(
resolveDatasourceBinding) for that third case. It was removed once themeasurement showed the case unreachable: a guard whose comment claims to protect
a real deployment shape, standing in front of a shape that cannot occur, is
worse than no guard.
Both halves pinned
packages/objectql/src/engine-organization-probe-outage.test.ts(7 cases):control that the same install with a working probe still reaches
ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED/ 500 /ambiguous-organization;recovery gets the stamp the ruling requires — asserted on the probe count
(2 probes; a memoised failure shows 1);
memoised, still one probe across many writes.
Reverse verification
Predicted before running: three RED (the two fail-open halves and the memo), the
benign and control cases GREEN. Observed exactly that against the pre-fix
engine.tsat the same commit —3 failed | 24 passed (27), the three failuresbeing precisely the propagation, refusal and memo cases; restoring the fix
returns
27 passed. No rebuild is involved in either leg and none is masked:this suite imports the subject as
./engine.js, a relative source specifiervitest transforms directly, which the failure stack traces confirm by pointing
at
packages/objectql/src/engine.tsrather than anydist/.Gate readings — POST-merge of
origin/mainRe-derived with
node scripts/pm/dispatch-gates.mjs(no paths — the scripttakes the change set from the merge base itself), then run at
fccdc8e19— the final commit of this branch, and the tree every reading belowwas taken on. That commit is the merge of
origin/mainincluding sibling#9719's landing (
1258dcaee, PR #9797), which touched this same file; themerge was clean (this card's diff against
origin/mainis still exactly thethree files above), and the whole union below was re-run on the merged tree
rather than inherited from the pre-merge reading.
check:durability-log-level— PASS, and ⛔ deliberately not offered asevidence. Its read-seam rule judges the expression a catch RETURNS; this catch
returns nothing and lets the accumulator above the
tryanswer, which is thecheck-durability-degradation-log-level: the read-seam invention rule keys on
return, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845-measured blind spot and the reason no gate caught this. The readingafter check-durability-degradation-log-level:
loggerLevelcannot see the(logger.error ?? logger.warn)(…)fallback, so a loud catch reads assilent-swallow— and the spelling it CAN see prints nothing #9657's matcher change is unchanged. The#8901disposition is notsettled here; this seam did not need it.
check:cross-package-test-inputs,check:error-code-casing,scripts/check-engine-split-ratio.mjs,scripts/docs-audit/check-affected-docs.mjs.check:changeset-gate-self-tests,check:objectui-changeset,check:stack-collection-maps,scripts/check-adr-0087-registration.mjs,scripts/check-changeset-no-major.mjs,scripts/check-empty-changeset.mjs,and the convention-triggered
check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher. Pluscheck:nul-bytes.check:query-options-erasurefailed first and was repaired at the source,not at the baseline: the new test's probe read used
as any, growing the testsurface 240 → 241. It is now typed
EngineQueryOptions, and the ratchet reads240 again — baseline untouched, no file added to its key set.
check:type-check-debt(check-type-check-coverage.mjs --re-measure) —MEASURED and green, not skipped. It refuses outright on an unbuilt
worktree and that refusal would be NOT MEASURED rather than not-applicable, so
the workspace closure was built first exactly as
lint.ymldoes(
turbo run build --filter=./packages/* --filter=./packages/*/*— 70/70successful). Reading on the merged tree:
33 ledger entr(ies) re-measured in 319.7s, 1926 raw tsc error(s) total, none above its recorded numberandsurplus: none. Itsstructural half,
check:type-check-coverage, passes — that is the half a new test filemoves, and it passes because objectql type-checks its own tests (
tsc --noEmitclean over the new file), so the file sits inside a tsc program rather than in
a TEST_DEBT entry. ⛔ No baseline was raised anywhere in this change.
Region discipline
This file has a concurrent second editor this round (#9719, the
delete()region). The declared region for this card was
probeInstallOrganizationsonly,and the diff honours it: the sole change to
engine.tsis inside that methodand its docblock.
origin/mainwas merged before this PR was opened, andmerged again after #9719 landed — discipline #3 of the region exemption. The
merge queue arbitrates from here; nothing was hand-ordered and nothing was
rebased over the sibling.
Generated by Claude Code