Filed by the domain:spec execution seat (session_01MkQhmuuJAVDjmeWNixwDDH), 2026-09-10T14:52Z, out of PR #17334 (card #16659). ⛔ Unlabelled and ungraded — routing, grading and type are triage's.
⚠️ Read the disposition section FIRST. A dedup search found a standing maintainer ruling that almost certainly decides this card without anyone re-deciding anything, and it is the reason this was not filed into the decision box.
The measurement
The at-tier review of PR #17334 found the changeset banner's claim — that a single-organization install "loses nothing at run time" — false on @objectstack/driver-memory. The implementing round verified it by running the driver's own exported predicate, assertCallNotTenantScoped (packages/drivers/driver-memory/src/memory-tenancy-guard.ts:303), over all twelve doors it guards:
| the call the engine hands the driver |
result |
scoped — tenantId present (the shape a DECLARED flow produces) |
12 of 12 REFUSED, MEMORY_MULTI_TENANT_UNSUPPORTED |
unscoped — no tenantId (the shape a job tick produced before the change) |
0 of 12 refused, 12 served |
The guard has no organization-count limb at all — its only early return is if (tenantId === undefined && tenantIds.length === 0) return; — so a single-organization install is not exempt. Engine side confirmed at packages/objectql/src/engine.ts:4058: hasTenant = execCtx.tenantId !== undefined && !isTenancyDisabled(objectSchema) && !isFederated, then opts.tenantId = execCtx.tenantId.
⭐ Why this is a functional gap and not a documentation defect
On driver-memory, a time-triggered flow that touches genuinely per-organization data has no legal configuration:
The one configuration that is served — declaring the swept objects platform-global (tenancy: { enabled: false }, ADR-0066) — is exactly what the guard's own refusal message says must not be reached for on data that really is per-organization.
⚠️ ⛔ PR #17334 does not create this limitation — the driver has never had row-level tenant isolation (#16589). What it does is move a time-triggered flow from the served case into the refused one. That is the honest framing and it is the whole claim here.
⛔ PR #17334 correctly stopped at this boundary: it fixed the false sentence (the banner is now driver-conditional and names the refusal) and named the gap in the migration entry's acceptance criteria, without touching the gap itself. ⭐ Reported rather than widened — the right call, and the reason this card exists.
⭐ Disposition — the maintainer has already ruled on this family
#5499 — 「[裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点)」 — is a standing cross-card anchor, and it speaks to this directly:
driver-memory 是早期为测试便利设计的组件,方向已定为 sqlite 内存模式替代
and it sets a standing triage rule for exactly this situation:
新单分诊规则(常设):driver-memory / driver-mongodb 族新单照常按落点打 domain:engine,但直接挂 pm:on-hold 引用本单,不入 pm:queue。
⇒ ⛔ This is not a decision-box card and I did not file it as one. The implementing round offered the maintainer three options (A: declare driver-memory unsupported for time-triggered data flows · B: an explicit exemption serving such runs unscoped with a loud warning · C: narrow the guard to installs holding more than one sys_organization) and recommended A. ⭐ #5499 already IS option A, for the whole component — the direction is replacement by sqlite :memory:, not extension. Putting this in the decision box would ask the maintainer to re-decide something decided five weeks ago.
⚠️ I also checked the ruling's exception escalation channel, because it would override the freeze:
例外升级通道:若 driver-memory 的缺陷影响 CI 判绿的正确性(单测后端语义错造成测试假绿/假红),按 restore-invariant 处理
It does not apply. PR #17334's own dogfood test (packages/qa/dogfood/test/schedule-sweep-organization-scope.dogfood.test.ts) runs the memory leg and asserts the refusal, with a paired control proving the refusal is about the scope and not about the fixture. So the semantics are pinned, not silently wrong: no false green, no false red. ⛔ No escalation is owed.
⇒ Suggested (⛔ not applied — triage's act): domain:engine, pm:on-hold referencing #5499, per that ruling's standing rule. If triage disagrees and thinks the flow-kind × driver support matrix is a separate axis from the component freeze, then it belongs in the box — but that is triage's call to make, not this seat's.
Restart condition, if it is held
#5499's own: when the sqlite :memory: replacement is complete on the test surface, the cards hung under it are closed not planned as a batch. ⭐ That would dispose of this one correctly too — a flow kind that cannot be served by a component being retired needs no fix, only the retirement.
⚠️ One thing worth carrying to whoever closes that batch: this card is not a driver-memory defect. It is a statement about which flow kinds a driver can serve. If the sqlite :memory: replacement lands with the same absence of row-level tenant isolation, the gap moves with it rather than closing — so the batch close should check the successor's guard, not just the component's removal.
Re-check
git -C /home/user/objectstack show origin/main:packages/drivers/driver-memory/src/memory-tenancy-guard.ts | grep -n 'tenantIds.length === 0'
git -C /home/user/objectstack show origin/main:packages/objectql/src/engine.ts | sed -n '4050,4062p'
⛔ Re-run the twelve-door probe through the driver's own exported assertCallNotTenantScoped rather than re-implementing the predicate, and keep the unscoped leg as the control — without it, "12 refused" does not distinguish a tenancy guard from a broken fixture.
Refs: PR #17334 · card #16659 · the at-tier review 5619271717 (M2) · the implementing round's report 5620579622 · #5499 (the governing ruling) · #16589 (the driver's absent isolation) · ADR-0066 (tenancy.enabled: false)
Filed by the
domain:specexecution seat (session_01MkQhmuuJAVDjmeWNixwDDH), 2026-09-10T14:52Z, out of PR #17334 (card #16659). ⛔ Unlabelled and ungraded — routing, grading and type are triage's.The measurement
The at-tier review of PR #17334 found the changeset banner's claim — that a single-organization install "loses nothing at run time" — false on
@objectstack/driver-memory. The implementing round verified it by running the driver's own exported predicate,assertCallNotTenantScoped(packages/drivers/driver-memory/src/memory-tenancy-guard.ts:303), over all twelve doors it guards:tenantIdpresent (the shape a DECLARED flow produces)MEMORY_MULTI_TENANT_UNSUPPORTEDtenantId(the shape a job tick produced before the change)The guard has no organization-count limb at all — its only early return is
if (tenantId === undefined && tenantIds.length === 0) return;— so a single-organization install is not exempt. Engine side confirmed atpackages/objectql/src/engine.ts:4058:hasTenant = execCtx.tenantId !== undefined && !isTenancyDisabled(objectSchema) && !isFederated, thenopts.tenantId = execCtx.tenantId.⭐ Why this is a functional gap and not a documentation defect
On
driver-memory, a time-triggered flow that touches genuinely per-organization data has no legal configuration:tenantId⇒ refused, per call;The one configuration that is served — declaring the swept objects platform-global (
tenancy: { enabled: false }, ADR-0066) — is exactly what the guard's own refusal message says must not be reached for on data that really is per-organization.⛔ PR #17334 correctly stopped at this boundary: it fixed the false sentence (the banner is now driver-conditional and names the refusal) and named the gap in the migration entry's acceptance criteria, without touching the gap itself. ⭐ Reported rather than widened — the right call, and the reason this card exists.
⭐ Disposition — the maintainer has already ruled on this family
#5499 — 「[裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点)」 — is a standing cross-card anchor, and it speaks to this directly:
and it sets a standing triage rule for exactly this situation:
⇒ ⛔ This is not a decision-box card and I did not file it as one. The implementing round offered the maintainer three options (A: declare
driver-memoryunsupported for time-triggered data flows · B: an explicit exemption serving such runs unscoped with a loud warning · C: narrow the guard to installs holding more than onesys_organization) and recommended A. ⭐ #5499 already IS option A, for the whole component — the direction is replacement by sqlite:memory:, not extension. Putting this in the decision box would ask the maintainer to re-decide something decided five weeks ago.It does not apply. PR #17334's own dogfood test (
packages/qa/dogfood/test/schedule-sweep-organization-scope.dogfood.test.ts) runs thememoryleg and asserts the refusal, with a paired control proving the refusal is about the scope and not about the fixture. So the semantics are pinned, not silently wrong: no false green, no false red. ⛔ No escalation is owed.⇒ Suggested (⛔ not applied — triage's act):
domain:engine,pm:on-holdreferencing #5499, per that ruling's standing rule. If triage disagrees and thinks the flow-kind × driver support matrix is a separate axis from the component freeze, then it belongs in the box — but that is triage's call to make, not this seat's.Restart condition, if it is held
#5499's own: when the sqlite
:memory:replacement is complete on the test surface, the cards hung under it are closed not planned as a batch. ⭐ That would dispose of this one correctly too — a flow kind that cannot be served by a component being retired needs no fix, only the retirement.:memory:replacement lands with the same absence of row-level tenant isolation, the gap moves with it rather than closing — so the batch close should check the successor's guard, not just the component's removal.Re-check
⛔ Re-run the twelve-door probe through the driver's own exported
assertCallNotTenantScopedrather than re-implementing the predicate, and keep the unscoped leg as the control — without it, "12 refused" does not distinguish a tenancy guard from a broken fixture.Refs: PR #17334 · card #16659 · the at-tier review
5619271717(M2) · the implementing round's report5620579622· #5499 (the governing ruling) · #16589 (the driver's absent isolation) · ADR-0066 (tenancy.enabled: false)