Skip to content

fix(objectql): discriminate a failed sys_organization probe from a genuinely empty one - #9817

Merged
os-elon merged 5 commits into
mainfrom
claude/issue-9261-organization-probe-discriminate
Aug 19, 2026
Merged

fix(objectql): discriminate a failed sys_organization probe from a genuinely empty one#9817
os-elon merged 5 commits into
mainfrom
claude/issue-9261-organization-probe-discriminate

Conversation

@os-elon

@os-elonos-elon commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Fixes#9261

probeInstallOrganizations answers the one question the #8844 system-write
organization resolution decides on. Its read sat behind a bare
} catch { ids = [] }, so every failure was answered with the count that means
none — and the answer was memoised, so the outage's consequence outlived the
outage.

The consumer consequence, stated as the mapping

resolveSystemWriteOrganization maps the probe's answer:

probe answerdecisioneffect on a system-context insert
0 organizationsno-organization-yetproceed unstamped
1 organizationderivedstamp the derived id
2+ organizationsrefuseERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED / 500

Before: a failed probe produced the 0 row. On a single-posture install
that really holds one organization, hook / cron / runAs: system inserts landed
untenanted — filed under the __global__ pseudo-tenant, forking exactly the
per-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
resolveSystemWriteOrganization is never reached, the write fails loudly, and
nothing 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_organization may not be registered at all (a lean embedding, a
bare-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.

  • Benign, and the only one:sys_organization routes but its TABLE was
    never provisioned. It cannot hold a row, so zero really is the count, and
    first boot still proceeds unstamped. Asked through the shared
    isMissingTableError predicate (@objectstack/metadata/errors, [metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825) — the
    same call resolveFileReferences and cascadeDeleteRelations already make in
    this file, never a hand-rolled code test.
  • Not a benign cause — it never reaches the catch: an object missing from
    the REGISTRY does not fail the read on a driver that tolerates an unknown
    table. Measured: find returns [] through the normal path. A strict driver
    surfaces 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.
  • Not a benign cause — it cannot reach the probe: an engine that cannot
    route sys_organization raises a bare
    [ObjectQL] No driver available for object 'sys_organization', which
    isMissingTableError answers false for, so keying on that predicate alone
    could have been a fail-CLOSED regression. It is not, and the reason is
    structural rather than lucky: getDriver answers every object from the
    default driver, which the first registerDriver always sets
    (isDefault || drivers.size === 1) and which nothing ever clears — no driver
    is ever removed. So the only engine whose routing fails for sys_organization
    is 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_order instead.

An earlier revision of this change carried a structural pre-check
(resolveDatasourceBinding) for that third case. It was removed once the
measurement 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):

  • a transient outage propagates and nothing reaches the driver;
  • the multi-organization refusal is not skipped, with the discriminating
    control that the same install with a working probe still reaches
    ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED / 500 / ambiguous-organization;
  • the memo case: a failure is not cached, so the very next write after
    recovery gets the stamp the ruling requires — asserted on the probe count
    (2 probes; a memoised failure shows 1);
  • the benign unprovisioned table still proceeds unstamped;
  • the unregistered-registry and unroutable cases above;
  • a healthy probe is unchanged — one organization is still stamped, still
    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.ts at the same commit — 3 failed | 24 passed (27), the three failures
being 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 specifier
vitest transforms directly, which the failure stack traces confirm by pointing
at packages/objectql/src/engine.ts rather than any dist/.

Gate readings — POST-merge of origin/main

Re-derived with node scripts/pm/dispatch-gates.mjs (no paths — the script
takes the change set from the merge base itself), then run at
fccdc8e19 — the final commit of this branch, and the tree every reading below
was taken on. That commit is the merge of origin/mainincluding sibling
#9719's landing (1258dcaee, PR #9797)
, which touched this same file; the
merge was clean (this card's diff against origin/main is still exactly the
three 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-levelPASS, and ⛔ deliberately not offered as
    evidence. Its read-seam rule judges the expression a catch RETURNS; this catch
    returns nothing and lets the accumulator above the try answer, which is the
    check-durability-degradation-log-level: the read-seam invention rule keys on return, so a catch that degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845-measured blind spot and the reason no gate caught this. The reading
    after check-durability-degradation-log-level: loggerLevel cannot see the (logger.error ?? logger.warn)(…) fallback, so a loud catch reads as silent-swallow — and the spelling it CAN see prints nothing #9657's matcher change is unchanged. The #8901 disposition is not
    settled here; this seam did not need it.
  • Named in the dispatch and run: check:cross-package-test-inputs,
    check:error-code-casing, scripts/check-engine-split-ratio.mjs,
    scripts/docs-audit/check-affected-docs.mjs.
  • Additions the derivation produced that the dispatch did not name, all run:
    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. Plus check: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 test
    surface 240 → 241. It is now typed EngineQueryOptions, and the ratchet reads
    240 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.yml does
    (turbo run build --filter=./packages/* --filter=./packages/*/* — 70/70
    successful). Reading on the merged tree: 33 ledger entr(ies) re-measured in 319.7s, 1926 raw tsc error(s) total, none above its recorded number and
    surplus: none. Its
    structural half,
    check:type-check-coverage, passes — that is the half a new test file
    moves, and it passes because objectql type-checks its own tests (tsc --noEmit
    clean 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 probeInstallOrganizations only,
and the diff honours it: the sole change to engine.ts is inside that method
and its docblock. origin/main was merged before this PR was opened, and
merged 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

os-elonand others added 4 commits August 19, 2026 00:32
…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
…e erasure ratchet at 240 (#9261)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

github-actionsBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 60 pages)

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 593c4bf3f53d8b28899d33526108024d8ee12453packageMentionDocs.

Which tree this was computed on

This run read content/docs from d670f6915e304f155fcf98bf25049c72f214e564 — the merge of head fccdc8e19cdd3cb81c4afe2fbd5f9de58c71a002 into base 593c4bf3f53d8b28899d33526108024d8ee12453, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@os-elon
os-elon marked this pull request as ready for review August 19, 2026 03:01
@os-elon
os-elon added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit 855591fAug 19, 2026
29 checks passed
@os-elon
os-elon deleted the claude/issue-9261-organization-probe-discriminate branch August 19, 2026 03:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

probeInstallOrganizations answers a failed sys_organization read with a memoised empty list, so system-insert organization resolution fails open

2 participants

@os-elon@claude