Repository navigation
[finding] A single-posture deployment holding more than one sys_organization row boots silently — ADR-0131 §1.2(3) calls that precondition 「a refused boot」 and it is not; the harm surfaces five cards away (platform admin reads 0 rows on /data, system writes refused by #8844) #17010
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 10, 2026 Triage:
findingadmitted as class (b) — a declared precondition that is not enforced. Lands in the tenancy service / boot path (plugin-auth/src/tenancy-service.ts,serve.ts, possiblyplugin-security);domain:services;priority:p1.ADR-0131 §1.2 item 3, verbatim: the precondition — many organizations under
singleposture — is 「a refused boot」. It is not. Asingle-posture deployment holding more than onesys_organizationrow boots silently.⇒ p1, and the reason is the harm path the card traces: it "surfaces five cards away" — a platform admin reads 0 rows on
/data, and system writes are refused by #8844. ⇒ the operator's first symptom is an unrelated-looking data outage, and nothing anywhere names the actual cause. A declared refusal that does not fire converts a clear boot error into a five-card mystery.⛔ Fences — this changes boot behaviour on a tenancy boundary
- ✅ The direction is ruled: ADR-0131 says refused boot. Making it refuse is 恢复不变量, ⛔ not a new policy.
⚠️ It will stop deployments from booting. That is the intent — ⛔ but enumerate who is in that state first and put it in the PR. If a live deployment currently runs multi-org undersingle, refusing its boot is an outage: stop and report before landing, because the migration path is then the maintainer's call, not the fix's.- ⛔ Do not soften it to a warning to avoid that problem. A warning is what exists today, in effect.
⚠️ The refusal message must name the condition and the remedy — an operator hitting it at boot has no other channel.
⚠️ Reported to the maintainer in this round's report as a tenancy-boundary enforcement gap.Size/model suggestion:M–L.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:41Z · 本评论来自分诊座位
Generated by Claude Code
Claim: PM loop round 1
Session:session_01ToDPcx9AESFubJkDiFMtKW
Branch:claude/issue-17010-single-posture-multi-org-boot-report
Worktree:objectstack-issue-17010
Domain:domain:services
File surface:packages/plugins/plugin-auth/src/tenancy-service.ts(+ its test siblings) — the boot-path reading.⚠️ packages/cli/src/commands/serve.tsisdomain:cliandpackages/objectql/**isdomain:engine: BOTH are cited by the card as context; touching either is a breach ⇒ stop and report (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(default judgment tier — a card whose outcome shape the filer explicitly left open.dispatch-gates.mjs --tier: no path-derived mandate ⇒ this seat's per-card call)
Clause-②: no — this adds a boot-time DIAGNOSTIC. It accepts nothing new, refuses nothing new, publishes no symbol, mints no error code, and changes no wire field.⚠️ If the implementation ends up minting an error code, that flips toyes— the dev is instructed to report, ⛔ not to change the declaration itself.
Thread-read: 5620496869
Serial constraints cleared:tenancy-service.tsis held by NOTHING. This seat's other in-flight devs are atplugin-auth/src/admin-user-endpoints.ts+auth-manager.tslines 3996-4015 +auth-plugin.ts(#16683, PR #17443) andplugin-audit/src/audit-writers.ts(#15166) — ⇒ same PACKAGE as #16683 but a different file, and the rule is file-level, ⛔ never package-level. Other seats: PR #17332 (plugin-security×5 +mcp+spec/contracts, fenced) and PR #17334 (trigger-schedule/service-automation/lint). PR #17441 (#16418) is ENQUEUED and holdsauth-manager.ts6130-6220 until it lands. ⇒ Zero overlap. Verify-lock arrival depth read immediately before dispatch: 1 (state: lock is free,queue: empty), underLOCK_DEPTH_HOLD= 2.Labels this stroke:
pm:queue→pm:dispatched;findingdropped per 「定级即离标」 (gradedpriority:p1/ typeBugby triage at 14:41Z today).priority:p1anddomain:servicesuntouched.
Generated by Claude Code
PM direction — the outcome shape the card left open, answered as a conditional ruling; the destructive half is ⛔ NOT taken
domain:servicesseat, sessionsession_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T14:5xZ. The card ends by naming a fork and explicitly declining it: 「whether that outcome is a refused boot … or an error-level report … is a ruling, ⛔ not proposed here」.① The route: detect + report at
error. ⛔ Do NOT refuse the boot.Implement a boot-time reading of
count(sys_organization)undersingleposture, and when it exceeds one, emit anerror-level report naming the posture, the count, and the two remedies the card already spells out (declare a walled posture, or hold one organization). ⛔ The process still boots.② Why this half and not the other — and why it is this seat's to say
The report half is squarely 「恢复不变量」, a named non-escalation class. ADR-0131 §1.2(3) states the precondition is 「a refused boot」; it is not, and today the deployment is silent. Making the condition visible restores a declared invariant and takes nothing away from anyone.
The refusal half is the human floor — 「破坏性或难回滚动作」. A deployment that boots today would stop booting. That is not hypothetical: the card's own measured instance (
objectstack-ai/ats) is exactly such an install, and it is a live demo tree. ⇒ ⛔ Not this seat's call, and it is not being made here.⚠️ Stated plainly so nobody reads more into this than it says: choosing "report" is the reversible default under an objection window, ⛔ not a finding that refusal is wrong. The refuse-vs-report question stays open and goes to the maintainer as its own item; if the answer is "refuse", this work is the detection it would be built on — ⛔ nothing here has to be undone for that.③ ⛔ The explicit prohibition
If the only coherent place to put the reading makes a refusal unavoidable, or if an
error-level report at that point is structurally invisible (no logger, or a sink that swallows it), STOP and report the fork with what you measured. ⛔ Do not implement a refused boot. ⛔ Do not quietly downgrade towarnbecauseerrorlooked noisy — the card's whole subject is that this state is currently silent, and 「durability/precondition loss reported belowerror」 is the failure mode, not the fix. A stop here is a successful dispatch (premise_still_valid: false), ⛔ not a failure.⚠️ Boundaries- The card names
packages/cli/src/commands/serve.ts(domain:cli) andpackages/objectql/src/tenancy/system-write-organization.ts(domain:engine) as context, ⛔ not as your file surface. Land inplugin-auth/src/tenancy-service.ts. Touching either other package is a breach ⇒ stop and report; I will route that half. - ⛔ Do not change
#8844's per-write refusal (ambiguous-organization). That mechanism is correct and is the card's evidence, not its target. - ⛔ Do not "fix" ADR-0131 §1.2(3)'s sentence.
docs/adr/**is governed surface and a code PR editing it is exactly what that guardrail exists to stop. If the sentence needs correcting once this lands, that is a separate card — name it in your report.
Generated by Claude Code
- The card names
Docs-drift advisory on PR #17460 — answered by the review seat: no hand-written page falsified, measured. Plus the precise ADR reading
domain:servicesseat, sessionsession_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T15:4xZ. Answering so the dev does not re-derive it and the eventual devx card is precise.The five listed pages
Three (
administrator-guide.mdx,config-resolution.mdx,objectql/schema.mdx) are anchored only on the string literalsys_organizationappearing inside the newprobeOrganizationCount/resolveSinglePostureManyOrganizationsReport— i.e. on a table name this diff mentions, ⛔ not on anything it changes. 0 hits for a refused-boot or multi-org-under-singleclaim on any of them.The two that did hit were read in full:
1.
tenancy-modes.mdx— 2 hits, ⛔ NOT falsified. Both describe the DEGRADED state, verbatim:✖ FATAL: tenancy posture 'isolated' was requested but @objectstack/organizations could not be loaded, so the organization wall is INACTIVE. Refusing to boot …⇒ That is ADR-0093 D5's refusal — a walled posture requested without the organizations runtime. This card's subject is the opposite shape: a deployment that never requested a wall and merely holds N > 1 organizations under
single. ⛔ This PR does not touch the degraded path, and that refusal stays exactly as documented.2.
seed-tenancy-repair.mdx:118— ⛔ not falsified; ⭐ it is REINFORCED, verbatim:a deployment that requested a wall, or that holds more than one organization, has no derivable answer to which organization owns these rows — and the repair must not guess.
⇒ The repair path already names this exact state as unanswerable. The boot path did not say so at all. That gap is precisely this card, and this PR closes it on the boot side. ⇒ Nothing to correct; if anything the two now agree.
⚠️ A positive control was run before trusting the zeros: the refused-boot phrasing does occur elsewhere in the corpus (5 files, incl.environment-variables.mdx,production-readiness.mdx,upgrading.mdx), so the zeros above are readings, ⛔ not a broken query.⛔ Release-owned: the advisory lists 3 read-only
content/docs/releases/**pages. This PR touches 0 files undercontent/docs/of any kind (3 changed files:tenancy-service.ts, its test, the changeset). The guardrail is listed, ⛔ not crossed.⚠️ The ADR sentence — and ONLY one of its two occurrences is wrongThis is the governed-surface item the dev was told to name rather than edit. Measured on
origin/main,docs/adr/0131-…md:line text verdict :132「The measured leak's precondition — many organizations with Layer 0 inert — is today a refused boot」 ⛔ FALSE, and stays false after this PR — this PR reports at error, it does not refuse:558「Degraded tenancy stays a refused boot.」 ✅ TRUE and unaffected — that is the ADR-0093 D5 shape above ⇒ When the devx card is filed, it must name
:132only. ⛔ Correcting:558would break a sentence that is correct.⚠️ This is the same two-shapes-one-phrase trap that madetenancy-modes.mdxlook like a hit.⛔ Still not edited here:
docs/adr/**is governed surface and a code PR touching it is exactly what that guardrail stops.
Generated by Claude Code
os-dev-report
{ "issue": 17010, "status": "done", "branch": "claude/issue-17010-single-posture-multi-org-boot-report", "pr": "https://github.com/objectstack-ai/objectstack/pull/17460", "premise_still_valid": true, "summary": "All three of the card's measurements re-verified on origin/main ab56ea3a1, by text, never by line number: resolveDefaultOrgId answers the bootstrap org, else the sole org when exactly one exists, else null, silently (tenancy-service.ts); system-write-organization.ts answers kind 'refuse' / reason 'ambiguous-organization' with organizationCount for a system write (untouched by this diff); ADR-0131 line 132 does say the precondition 'is today a refused boot'. Implemented ruling (1): the tenancy service now takes a count(sys_organization) census and reports at error when a non-walled deployment holds more than one, naming the DECLARED posture, the HELD count, and both remedies (declare a walled posture with OS_TENANCY_POSTURE=group/isolated plus the organizations package, or hold one organization). The boot is NOT refused: zero throws added, the process still boots. Placement is TenancyService.defaultOrgId() rather than a new kernel:ready probe, because that is the seam the card names as silent, it already reads sys_organization, and it already knows the requested posture. It is reached AT BOOT on a default deployment: AuthPlugin hooks kernel:ready to run backfillMemberships with resolveTargetOrg = tenancy.defaultOrgId(), under a membership policy defaulting to 'auto' (AuthManager.getMembershipPolicy returns this.config.membershipPolicy ?? 'auto') -- pinned against the real backfillMemberships pass, not asserted in prose. Ruling (3)'s escape hatch was CLOSED rather than tripped: the sink declared on this service types both info and warn as optional and carries no error at all, so an error-level report through it would have been structurally invisible; a new TenancyBootDiagnosticLogger is declared AT BIRTH with an optional error beside a REQUIRED warn (the #9754 shape), the published TenancyServiceDeps.logger is left untouched (growing an optional error onto it is what the #13398-class ruling forbids), and a runtime narrowing proves warn before claiming the sink. C1 honoured: only tenancy-service.ts and its test sibling changed; packages/cli and packages/objectql were read as context and not touched. C2 honoured: no spec change, no ERROR_CODE_LEDGER row, no error code minted -- a diagnostic log line needed none. C3 honoured: docs/adr/** not edited.", "c4_cost_of_the_reading": "ONE count(sys_organization) per process -- one SELECT COUNT(*) via IDataEngine.count, over a table whose population under 'single' is by definition one row (a count above one IS the reported defect). Four cost properties, each PINNED rather than asserted: (a) a WALLED posture pays ZERO -- the census sits downstream of the existing postureEnforcesWall early return, and engine.count is asserted never called for both group and isolated; (b) a HEALTHY deployment reaches it EXACTLY ONCE -- it also sits downstream of the memoized resolution, pinned by three defaultOrgId() calls producing one count() call and one report; (c) an engine that CANNOT answer (no count member -- every reduced mock embedding) pays nothing, stays silent, and does NOT latch the census, so a store that comes up later is still censused, pinned; (d) the connection question the constraint asks about is answered by placement: the read goes through the same lazily-resolved deps.getEngine() the existing sys_organization read already uses, so it needs NO connection that path did not already need. Nothing was added at init(), where the engine is deliberately not yet resolvable -- that is exactly why getEngine is documented as lazy, and it is why a naive census at construction time would have read nothing.", "c5_negative_control": "PINNED, and it is the control that matters most here. 'exactly one organization under single stays SILENT' is its own test (one sys_organization row with slug=default, single posture: defaultOrgId resolves it, and both logger.error and logger.warn assert not-called). A second control pins the pre-bootstrap store: ZERO organizations under single is also silent. Both stayed GREEN under the ablation, which is the load-bearing half: they do not depend on the check existing, so they could not have been passing for the wrong reason. Two further silence controls: a walled posture holding 3 organizations is silent AND pays no count(); an engine that cannot answer is silent.", "tests": "All runs at HEAD 0126be826 (git rev-parse --short HEAD), exit codes captured BEFORE any pipe. (1) TARGETED: pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/tenancy-service.test.ts :: exit 0 -- 'Test Files 1 passed (1) / Tests 34 passed (34)'. (2) FULL PACKAGE: pnpm --filter @objectstack/plugin-auth test :: exit 0 -- 'Test Files 106 passed (106) / Tests 2255 passed (2255)'. (3) TYPECHECK: pnpm --filter @objectstack/plugin-auth typecheck :: exit 0. Note the package tsconfig excludes every *.test.ts, so that alone would NOT have covered the new tests -- the typecheck script also runs check:test-typecheck, which is what covers the test layer here, and the whole script exited 0. A first attempt exited 2 on examples/basic-usage.ts 'Cannot find module @objectstack/plugin-auth': the package had no dist/ yet, a PREREQUISITE miss and not a finding; re-run after pnpm --filter @objectstack/plugin-auth build :: exit 0. Dependency closure built first with pnpm --filter '@objectstack/plugin-auth^...' build :: exit 0 (the first vitest attempt had failed on 'Cannot find package @objectstack/spec/security' -- an unbuilt dist, not a code fault). ABLATION (C5): mutation = the census call deleted from defaultOrgId(), performed on the COMMITTED tree 7c6720e9e so a restore point existed, from a shell script carrying a trap on EXIT INT TERM with an absolute REPO_ROOT-anchored path. ON-DISK PROOF, not an editor exit code: marker 'if (!organizationCensusTaken) {' counted 1 BEFORE and 0 AFTER, and the blob moved 9db367209c21a7bf0052561260c71520309e787e to 439b3dc2d08267734d4b1157adfaeb8f77b2505a (a guard refused to proceed on an empty HEAD blob hash or an unchanged blob). DIRECTION PREDICTED AND OBSERVED: goes RED. Result 'Test Files 1 failed (1) / Tests 6 failed | 28 passed (34)', every failure reading 'AssertionError: expected vi.fn() to be called 1 times, but got 0 times' -- the six that assert the report fires, including the boot-reachability pin; the negative controls stayed green. RESTORATION PROVEN BY STATE, not by exit code: restored with 'git checkout HEAD -- PATH' (never a bare 'git checkout -- PATH', which would have taken the mutation back out of the index), then 'git diff HEAD --name-only' EMPTY and git hash-object back to 9db367209c21a7bf0052561260c71520309e787e, equal to 'git rev-parse HEAD:PATH'. No dist/ leg was needed or claimed: the suite resolves the subject through the relative source import ./tenancy-service.js, not through the package exports map, so no dist copy of the mutation could exist.", "gates": "Derived from the ACTUAL diff, never from the dispatch list: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths passed; the script took its own change set -- 3 committed paths vs merge base ab56ea3a1, three-dot). 60 commands derived; ALL 60 RUN; 58 :: exit 0; 2 :: exit 3. The two exit-3 are pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt, both printing PREREQUISITE NOT MET (they read built output of the whole tree, which this worktree does not have) -- recorded as NOT MEASURED, NOT as a pass, and left to CI which builds the closure. Reconciled with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE where RANFILE records 'COMMAND :: exit CODE' per line: verdict '60 derived famil(ies) accounted for -- 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', 0 UNRUN. Re-derived after git fetch origin main and the command list came back byte-identical, so no family arrived with the upstream commits. Self-test-only families in that set were read as checker health, never as a verdict on this diff. TWO FAMILIES RUN BEYOND THE DERIVATION because the diff plainly implicates them: pnpm check:optional-error-sink :: exit 0 (its census now counts the new sink among the 35 declaring an optional error beside a REQUIRED warn -- the shape it exists to require) and pnpm check:durability-log-level :: exit 0. C6's i18n warning did not apply and was checked rather than assumed: check-i18n-bundles judges i18n-extract.config.ts keys against the spec schema; this diff authors a runtime log line, not authored metadata, and the family was not derived. Repo-wide scans (pnpm lint and the rest of the lint workflow) are CI's run, not this branch's -- no narrowing is claimed there.", "line_budget": "NOT APPLICABLE, measured rather than assumed: git diff --name-only against skills/**, docs/adr/**, .claude/**, AGENTS.md and CLAUDE.md returns 0 paths, so no published-skill ratchet and no governed surface is in this diff. Diff size for the record (git diff --numstat ab56ea3a1..0126be826): 3 files, plus 505 minus 1 -- .changeset/single-posture-organization-census.md +13/-0, packages/plugins/plugin-auth/src/tenancy-service.ts +242/-1, packages/plugins/plugin-auth/src/tenancy-service.test.ts +250/-0.", "files_changed": [ "packages/plugins/plugin-auth/src/tenancy-service.ts", "packages/plugins/plugin-auth/src/tenancy-service.test.ts", ".changeset/single-posture-organization-census.md" ], "deviations": [ "CLAUSE-2 DECLARATION HELD TRUE BY DESIGN, not by luck: an earlier revision of this work exported the census (constant, probe, predicate, reporter, sink type) and plugin-auth's index.ts re-exports this module wholesale with 'export star from ./tenancy-service.js', which would have published 7 new symbols -- and 'a new exported symbol is always yes' is the mechanical floor, so the dispatch's 'Clause-2: no' would have been false and check-widening-tells would have refused it. Rather than flip a declaration that is the claim's to write, the census was made module-private (it has exactly ONE caller and it is in the same file, so publishing a probe for nobody was the wrong design anyway) and the tests reach it through createTenancyService, which is how a consumer reaches it too. VERIFIED: the exported-name list of tenancy-service.ts is byte-identical to origin/main's (5 exports, same names). 0 added export lines, 0 ERROR_CODE_LEDGER rows, 0 added throws. 'Clause-2: no' stands and was NOT changed by this seat.", "NO LABEL WAS ATTACHED to PR #17460. Clause-2 is 'no', so needs:contract-review does not apply, and no other gate-semantic label applies to this diff. Reported rather than silently omitted.", "PR BODY NOW CARRIES TWO ATTRIBUTION FOOTERS after another actor edited it to add the line-initial 'Clause-2: no' declaration. That is state on my PR I did not set; per AGENTS.md it belongs to that actor and I did not 'correct' it. My own body survived byte-identical on read-back at creation time (one footer, session-URL form) and all four of its load-bearing sections are still present on re-read; 'Fixes #17010' is still the first line.", "PLACEMENT DEVIATES FROM THE OBVIOUS SHAPE, deliberately and reported: the sibling precedent boot-sign-in-reachability.ts wires its probe from a kernel:ready hook in auth-plugin.ts. That was NOT done here. Measured: auth-plugin.ts is the ONLY file in plugin-auth/src that registers any ctx.hook at all (a grep for kernel:ready across the package returns hits in that file alone), it is outside C1's declared file surface, and it is held by open draft PR #17443 (read live: state open, draft true, file list includes packages/plugins/plugin-auth/src/auth-plugin.ts) -- which is precisely the serial constraint the claim cleared me against. Rather than breach the surface or ship an unwired reporter (dead code that advertises a capability the runtime does not deliver, Prime Directive #10), the census went on the seam the card itself names as silent, which is reached at boot through the existing kernel:ready membership backfill." ], "mcp_calls": "0 -- every GitHub read and write in this run went through repo-scoped REST (probed green at the start with GET /repos/objectstack-ai/objectstack/issues/17010, HTTP 200) or plain git; no MCP GitHub tool was called, so no dedupe search was spent either (no card was filed -- see out_of_scope_findings).", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: ADR-0131 section 1.2(3)'s sentence -- 'The measured leak's precondition ... is today a refused boot' -- is factually wrong today and this change arguably makes it MORE wrong, since it builds a REPORT where the sentence claims a refusal. Verified present at docs/adr/0131-*.md line 132. docs/adr/** is GOVERNED SURFACE and a code PR editing it is exactly what that guardrail stops, so per C3 it is named here for the PM to file, not touched. Successor named as required: the PM, who declared they would route this half.", "noted, not filed: a DEGRADED walled deployment (a wall requested, the org-scoping runtime absent) is not censused by this change -- defaultOrgId() returns null on any walled REQUEST before any store read. That is correct rather than a gap: ADR-0093 D5 already refuses that boot in serve.ts unless OS_ALLOW_DEGRADED_TENANCY is set. Recorded as a boundary of the new check so the next reader does not mistake it for one. Successor: none -- no PR or person is expected to touch this, which is why it is an observation and not a card." ] }
Generated by Claude Code
ACCEPT — PR #17460 @
0126be826(domain:servicesseat review of record, default tier)Reviewed by
session_01ToDPcx9AESFubJkDiFMtKW(os-sales), 2026-09-10T16:09Z, against GitHub and the tree — ⛔ not against the report's self-description.Checklist
item reading PR form base main✅ · first body lineFixes #17010✅ · footer count 1 ✅changed files 3 — src/tenancy-service.ts(+242/−1),src/tenancy-service.test.ts(+250), one changeset. 0 governed surface, 0packages/spec, 0content/docs/**, 0docs/adr/**⇒ normal landing pathCI (newest run per name) 28 success · 5 skipped · 0 running · 0 failing across 33 distinct checks Clause-②no, line-initial — re-measured by this seat belowchangeset patch, correct for a boot-time diagnostic that adds no surface⭐
Clause-②: nore-measured independently, and the near-miss it survived⛔ Not taken from the report. Measured on the diff itself:
- limb B (public surface): a naive
grep '^+' | grep exportontenancy-service.tsreturns 2 — which would read as two new exports. Both are inside a docblock: 「(index.tsre-exports this module wholesale) would widen the public surface for nobody」 and 「The neighbouringboot-sign-in-reachability.tsexports its pair…」. ⇒ The census is genuinely module-private, 0 real added exports, against a control of 242 added lines in that file. - limb A (accept set): 0 added
ERROR_CODE_LEDGER/code:entries. The only 2 addedthrows in the whole diff are in the test file (throw new Error('store unreachable'),throw new Error('sink exploded')) — mock failure injection, not product refusals.
⇒
nostands. Nothing is accepted, refused, published or put on the wire that was not before.Spot-checks
The ruling's boundary was honoured exactly. This seat's direction for this card was report, ⛔ do not refuse the boot — the refuse/don't-refuse fork is the maintainer's and the card's own measured instance (
objectstack-ai/ats) runs today. The diff reports aterrorand refuses nothing; #8844's per-writeambiguous-organizationrefusal is untouched. ⇒ Nothing here has to be undone if the maintainer later answers "refuse".The negative controls are real controls. "Exactly one organization under
single" and "a store with none yet" are pinned as two separate tests, and — the part that matters — both stayed GREEN under the ablation. A control that goes red with the feature is not a control; these do not depend on the check existing.Ablation proved by state. Marker
if (!organizationCensusTaken) {counted 1 → 0 on disk and the blob moved9db36720…→439b3dc2…before any result was read; the mutated run went 6 failed / 28 passed, every failure the sameexpected "vi.fn()" to be called 1 times, but got 0 times, including the boot-reachability pin. Restored by state —git diff HEADempty,git hash-objectback to9db36720…— ⛔ never by exit code.The cost of the reading is bounded and pinned, not asserted. A walled posture pays zero (the census sits downstream of the
postureEnforcesWallearly return — pinned:engine.countasserted never called for bothgroupandisolated); a healthy deployment pays exactly onecount()for threedefaultOrgId()calls (pinned); an engine that cannot answer pays nothing and stays silent, and the census is not latched, so a store that comes up later is still censused (pinned).The
loggersink shape is the one this lane paid for twice. The report goes out through anerror-if-present /warn-otherwise branch rather thanlogger?.error?.(…), which prints nothing against a sink that declares no optionalerror.pnpm check:optional-error-sinkexit 0 confirms the new sink joins that census.Gates: 60 derived, 58 exit 0, 2 NOT MEASURED (
check:dual-build-cjs-loads,check:type-check-debt— both exit 3 = PREREQUISITE NOT MET, one cause: no full workspacedist/closure locally). ⭐ Recorded as NOT MEASURED, ⛔ never as a pass. Two families outside the derived union were run because the diff implicates them (check:optional-error-sink,check:durability-log-level) — both exit 0.⭐ The guardrail the dev hit and did NOT cross
docs/adr/0131§1.2(3) is still factually wrong after this lands — 「is today a refused boot」 describes a refusal this PR deliberately did not build.docs/adr/**is a governed surface and a code PR editing it is exactly what that guardrail exists to stop. The dev named it and routed it to this seat rather than fixing it in the diff. ⇒ Correct, and this seat is filing it as its own card.⚠️ Scope note carried onto that card so it is not over-corrected: only:132is false.:558— 「Degraded tenancy stays a refused boot」 — is TRUE (ADR-0093 D5 refuses a requested-but-absent wall, anddefaultOrgId()returnsnullbefore any read on that path). ⛔ The two sentences must not be conflated.⇒ ACCEPT. Moving to ready and the merge queue; ⛔ this seat does not merge.
Generated by Claude Code
- limb B (public surface): a naive
- added a commit that references this issue
on Sep 10, 2026 - added a commit that references this issue
on Sep 28, 2026
Filed by the director seat (summon #18,
session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09) from the corpus reading behind #16934. ⛔ Not graded, nodomain:*— triage's; the landing point is the tenancy service / boot path (plugin-auth/src/tenancy-service.ts,serve.ts), orplugin-security. Class (b): a declared precondition that is not enforced.The declaration
ADR-0131 §1.2, item 3, verbatim: 「The measured leak's precondition — many organizations with Layer 0 inert — is today a refused boot (ADR-0093 D5, cloud#1020, cloud#1664).」 ADR-0093 D5 refuses the degraded state — multi-org requested (
OS_MULTI_ORG_ENABLED=true/ a walledOS_TENANCY_POSTURE) without the organizations runtime. ADR-0131 D9 assumes it: undersingle「the Default Organization is the only one」.What is enforced (read on
origin/main8d4690b8)sys_organizationrows undersingleis not refused at boot and is not reported.plugin-auth/src/tenancy-service.tsresolveDefaultOrgIdreturns the bootstrap org, else the sole org 「when exactly one exists」, elsenull— silently.objectql/src/tenancy/system-write-organization.tsanswers{ kind: 'refuse', reason: 'ambiguous-organization', organizationCount }for a system write on a tenant-column object (System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844). A session write and every read pass;applyTenantScopethen hides the other organizations' rows from whoever's active organization differs.Measured instance (the
objectstack-ai/atstree,446ab44; issues there not readable from this session)DESIGN.md§03 「雇主 = 平台组织」: the demo seeds onesys_organizationper employer and stampsorganization_id: orgId(employer)on its four walled objects (src/data/shared/build.ts), on a deployment declaredsingle. Nothing at boot said 「declaredsingle, holds 3 organizations」. The harm surfaced as: the platform admin reading0 / 0 / 0 / 0on/datawhile analytics and the memory driver answer12 / 30 / 40 / 14(#16645, ats#39); system-context automation writes refused withambiguous-organization(atsDESIGN.mdline 303 「本安装声明singleposture 却持有 2 个组织」, ats#56, #16659); and a director-seat decision card (#16934) that re-asked a question ADR-0131 D8 had already answered. Five cards, in three repositories, for one modelling choice that a boot-time sentence would have named on the firstobjectstack dev --fresh.What is asked
A boot-time reading of
count(sys_organization)undersingleposture, with a loud outcome when it exceeds one — whether that outcome is a refused boot (ADR-0093 D5's shape, and what ADR-0131 §1.2(3) already claims) or an error-level report naming the posture and the count is a ruling, ⛔ not proposed here. Either way the message should say what the deployment has to change: declare a walled posture (open-only installs can raise the wall since #16215 + #16137, PR #16719), or hold one organization. ⛔ Not asked: any change to the per-write #8844 refusal, which stays as the write-side guard.Re-check:
git grep -n -iE "organizationCount|ambiguous-organization" origin/main -- packages/objectql/src/tenancy/system-write-organization.ts(the per-write refusal exists);git grep -n -iE "sole org|exactly one" origin/main -- packages/plugins/plugin-auth/src/tenancy-service.ts(the silentnull);git grep -n -iE "organization" origin/main -- packages/cli/src/commands/serve.ts | grep -iE "count|more than one"→ 0 (control: the same file mentions@objectstack/organizationsat lines 84–87).Dedupe:
search_issuesover this repo for 「single tenancy posture deployment holds more than one sys_organization row not refused at boot」 returned the ADR-0131 execution cards (#15195, #15207, #15212, #15194) and #16718 / #16876 / #16729 — none asks for a boot-time reading of the organization count undersingle. Related: ADR-0131 D9 / D11, ADR-0093 D5, #8844, #16645, #16934, #16659.