feat(organizations): bring the multi-organization runtime back to open core — the org-scoping registrar ships open, the licence gate stays in cloud (ADR-0132) - #16215
Conversation
…ore (#16130) Creates packages/plugins/organizations — @objectstack/organizations — carrying the 1300 lines cloud's packages/organizations classified as moving. The licence gate (306 lines) stays in the commercial repo, which will subclass this class and call it in its own constructor. WIP: ADR-0132 and the changeset still to come. Claude-Session: https://claude.ai/code/session_6679d191-11f4-465b-b322-0e0409d76793 Co-authored-by: Claude <noreply@anthropic.com>
…6130) Amends ADR-0105 D12 in place, corrects the three open-tree claims the move falsifies (plugin-security README, the spec roster row, the multi-node carrier header), and adds the changeset. Claude-Session: https://claude.ai/code/session_6679d191-11f4-465b-b322-0e0409d76793 Co-authored-by: Claude <noreply@anthropic.com>
…never met (#16130) The moved code arrives in a repo with gates cloud's does not run. Each finding was a real one, fixed rather than baselined: - slot-lookup: the `objectql` / `metadata` lookups are typed structurally instead of erased to `any` — that ratchet's baseline never grows. - engine-double-contract: the three moved test fakes open `update()` with `assertEngineUpdateDispatch`, pinning them to the producer's rejection. - doc-authoring: tracker ids leave the rendered boot refusal and the log line; an operator cannot resolve `#NNNN`. - dispatcher-error-vocabulary: `WALLED_MEMBERSHIP_POLICY_UNDECLARED` is recorded as a boot-refusal, the class its neighbours are ruled by. - system-context census: the `organization_id` auto-stamp's elevation short-circuit gets its row (62) and the page's counts move with it. - cross-package-test-inputs + turbo: the ADR-0132 D3 pin declares its radius. - test-typecheck: measured zero, so the package carries no ledger and no generator, matching the ten zero-residue plugin packages. - llms.txt: 68 → 69 published packages. Claude-Session: https://claude.ai/code/session_6679d191-11f4-465b-b322-0e0409d76793 Co-authored-by: Claude <noreply@anthropic.com>
… last slot erasure (#16130) - tsconfig `paths` + a vitest alias so every verdict in this package is about the source in the checkout, not the last build. That covers the test-only `@objectstack/metadata-core` too, which the moved fakes open `update()` with. - `packages/qa/dogfood` gains the same rule for `@objectstack/organizations`: the name became a workspace package with this PR, and that suite's doc comment spells a literal dynamic import of it. - the moved `walled-default-org-self-registrant` fake REFUSES a WHERE combinator instead of reading it as a field name — silently wrong is the shape that would let a negative assertion read "no rows" as evidence. - the last `getService(...) as any` in the moved suite goes through the harness's own handle. - engine-double-contract ledger learns the three newly pinned fakes. - census self-test literal moves with the page it asserts against. Claude-Session: https://claude.ai/code/session_6679d191-11f4-465b-b322-0e0409d76793 Co-authored-by: Claude <noreply@anthropic.com>
#16130) The merge driver deferred `content/docs/permissions/system-context.mdx` — it is a `merge=os-regen` artifact and git ran no text merge on it. This discharges that deferral from the merged tree, as `.githooks/pre-push` requires. Anchor lines only: 18 rewritten, all of them `<file>.ts:<line>` citations the incoming commits moved. Row 62, the section extents and every declared count are unchanged, and `check:system-context-census` is green on the merged tree. Claude-Session: https://claude.ai/code/session_6679d191-11f4-465b-b322-0e0409d76793 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 6 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 140 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 b76c34ce079ca8042450276374b2eb07b5b3254d && git checkout b76c34ce079ca8042450276374b2eb07b5b3254d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6c546ab9d0b4842bf363caac2a9ff852277680b5 11ca403f6b12361daa1082891ac1cbe97113b654 && git checkout -B drift-repro 6c546ab9d0b4842bf363caac2a9ff852277680b5 && git merge --no-ff 11ca403f6b12361daa1082891ac1cbe97113b654
node scripts/docs-audit/affected-docs.mjs --json 6c546ab9d0b4842bf363caac2a9ff852277680b5
|
… residual The rest of the population #15809 measured, one repair per shape: - TRACKED TARGETS the surrounding prose already names become file-level anchors, and where the prose names the symbol too they become symbol anchors the gate now resolves — `packages/objectql/src/plugin.ts#runGovernanceInventory`, `packages/plugins/plugin-audit/src/audit-writers.ts#resolveWriteLocale`, `scripts/check-system-context-census.mjs#UNENFORCED_TEXT_COUNTS`. A bare `engine.ts` was never ambiguous where the prose said which package. - THIRD-PARTY AND DEPENDENCY SOURCES (`@oclif/core`, `@changesets/pre`) name the package, the file and the pinned line as prose. Nothing in this tree can resolve them, and a pinned upstream line is provenance, not a pointer. - ILLUSTRATIONS take the angle-bracket placeholder spelling. ⛔ Again, no number is repaired or repointed: every digit is still on its page. The residual is 15 citations across six files, and every one of them is a file another lane holds OPEN — three by PR #16215, one by PR #16202, three by the in-flight #15776, one by #12511. NOT ONE is a genuinely ambiguous abbreviation.⚠️ `judgeUntrackedLineAnchors` therefore stays `false`. The fence this work was dispatched under flips it only when the residual is ZERO and a self-test pins the flip; flipping it over 15 citations in files this PR may not touch would make the gate permanently red for the length of somebody else's pull request — the exact shape the card argued against. `--list-unresolvable` now prints that residual, so the next author inherits a worklist rather than a count. Part of #15809. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
#16130) Second merge round, same deferral: `content/docs/permissions/system-context.mdx` is a `merge=os-regen` artifact, git ran no text merge on it, and the driver recorded it in `os-regen-pending`. This discharges that deferral from the merged tree, as `.githooks/pre-push` requires. Anchor lines only: 15 rewritten, every one a `file.ts:line` citation the incoming commits moved. Proven rather than asserted — diffing the page against its pre-regeneration copy and filtering out anchor-bearing lines leaves nothing, row 62 is byte-identical, and the whole declared-counts table is byte-identical (106 sites / 20 packages / 45 files, rows 1-62 behaviour, rows 63-66 carry-onward).
…vel anchors Second round of the scripts/** unresolvable-citation migration PR #16301 started. Two of the six held files freed since that PR landed -- check-docs-section-name.mjs and docs-audit/affected-docs.mjs (PR #16300), check-type-check-coverage.mjs (PR #16295) -- so their 7 citations migrate now by the same method: the file named as a file-level anchor, the number kept beside it as data, no digit repaired or repointed. - check-docs-section-name.mjs: concept.mdx:426, doc-pages.mdx:257 and forms.mdx:183 each resolve to exactly one tracked file under content/docs/** (forms.mdx is ambiguous by basename alone -- two files share it -- but only content/docs/ui/forms.mdx has a line 183 carrying the cited "sections": [/* ... */] shape). - check-type-check-coverage.mjs: engine.test.ts:2547/2577 resolves to packages/services/service-automation/src/engine.test.ts, named by the surrounding prose ("service-automation's note"). - docs-audit/affected-docs.mjs: three rest-server.ts citations resolve to packages/rest/src/rest-server.ts, the only tracked file of that name. The corpus residual (scripts/check-scripts-symbol-anchors.mjs --list-unresolvable) drops from 15 to 8, all 8 still held by open PRs (#16215, #16202) -- none an ambiguity. judgeUntrackedLineAnchors stays false per the #15809 fence (flips only at residual zero). Header prose updated to record both dated readings. Part of #15809. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
The landing that killed the old premise has a decision record; name it where the premise is restated, per Prime Directive #13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
…ed file to file-level anchors (objectstack-ai#15809) (objectstack-ai#16301) * feat(devx): the scripts/** corpus enumerates the citations it declines, not just their count `judgeUntrackedLineAnchors: false` waives a citation that names no tracked file, and until now the only trace was a number on the green line. A number says a residual exists without saying where: it cannot be worked down, and a residual that MOVED reads exactly like one that shrank. `sweepCorpus` now returns `declined` beside `findings` and `counts` — one row per waived citation, carrying the file, the line, the citation text and a shape (`bare-filename` / `directory-qualified` / `continuation` / `tilde`). `declinedShape` classifies by how the citation was WRITTEN, never by the path it borrowed: a continuation and a tilde form inherit whatever path preceded them on the line, so classifying by path first would file them under a file their author never wrote. `check-scripts-symbol-anchors.mjs --list-unresolvable` prints those rows and a tally by shape. It is a listing, never a verdict — it exits 0 whatever it prints, exactly as `--list` does, and `runCheck()` remains the only failing arm. Pinned: three cases hold the enumeration equal to the counter, to the file/line/text a residual list needs, and to the shape classification (floor 26 → 29); four more hold each arm of `declinedShape` (floor 63 → 67). Part of objectstack-ai#15809. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 * chore(devx): migrate the scripts/** line citations in the four heaviest carriers `check-durability-degradation-log-level` (23), `symbol-anchors` (15), `doc-line-anchors` (11) and `pr-labels` (8) carried more than half the citations that name no tracked file, and each shape gets the repair its own kind admits: - DATED CENSUS ROWS name the file as a file-level anchor and keep the number beside it as data, losing no digit — the `objectstack-ai#15806` method. The durability gate's two seam tables gain a `try line` column of their own, and `objectql engine.ts` / `metadata-protocol protocol.ts` become the full tracked paths the gate's own output actually prints. One of them binds tighter still: `packages/objectql/src/engine.ts#cascadeDeleteRelations` is now a resolved symbol anchor. - ILLUSTRATIONS take the angle-bracket placeholder spelling `ANCHOR_GRAMMAR` already uses. `symbol-anchors.mjs` and `doc-line-anchors.mjs` are the two files that DEFINE what a line citation looks like, so every example in them was a citation of its own; they now exhibit the shape without writing one, and say why in a⚠️ line so a later author does not "fix" it back. - THIRD-PARTY SOURCES (`codelytv/pr-size-labeler`, `actions/labeler`) name the repo, the file and the pinned line as prose. No in-repo resolver could ever check them, and a pinned upstream line is provenance rather than a pointer. ⛔ No number is repaired or repointed anywhere in this commit: every digit that was in an anchor is still on the page, as data. Corpus reading: 96 → 39 unresolvable citations. Part of objectstack-ai#15809. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 * chore(devx): migrate the remaining free line citations and record the residual The rest of the population objectstack-ai#15809 measured, one repair per shape: - TRACKED TARGETS the surrounding prose already names become file-level anchors, and where the prose names the symbol too they become symbol anchors the gate now resolves — `packages/objectql/src/plugin.ts#runGovernanceInventory`, `packages/plugins/plugin-audit/src/audit-writers.ts#resolveWriteLocale`, `scripts/check-system-context-census.mjs#UNENFORCED_TEXT_COUNTS`. A bare `engine.ts` was never ambiguous where the prose said which package. - THIRD-PARTY AND DEPENDENCY SOURCES (`@oclif/core`, `@changesets/pre`) name the package, the file and the pinned line as prose. Nothing in this tree can resolve them, and a pinned upstream line is provenance, not a pointer. - ILLUSTRATIONS take the angle-bracket placeholder spelling. ⛔ Again, no number is repaired or repointed: every digit is still on its page. The residual is 15 citations across six files, and every one of them is a file another lane holds OPEN — three by PR objectstack-ai#16215, one by PR objectstack-ai#16202, three by the in-flight objectstack-ai#15776, one by objectstack-ai#12511. NOT ONE is a genuinely ambiguous abbreviation.⚠️ `judgeUntrackedLineAnchors` therefore stays `false`. The fence this work was dispatched under flips it only when the residual is ZERO and a self-test pins the flip; flipping it over 15 citations in files this PR may not touch would make the gate permanently red for the length of somebody else's pull request — the exact shape the card argued against. `--list-unresolvable` now prints that residual, so the next author inherits a worklist rather than a count. Part of objectstack-ai#15809. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 * docs(devx): the corpus header states its residual as a dated reading `--list-unresolvable` is the live instrument; a count written in prose is a dated record, and this file is the one arguing that a number in prose outlives the measurement it quoted. Part of objectstack-ai#15809. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 --------- Co-authored-by: Claude <noreply@anthropic.com>
…vel anchors (objectstack-ai#16338) Second round of the scripts/** unresolvable-citation migration PR objectstack-ai#16301 started. Two of the six held files freed since that PR landed -- check-docs-section-name.mjs and docs-audit/affected-docs.mjs (PR objectstack-ai#16300), check-type-check-coverage.mjs (PR objectstack-ai#16295) -- so their 7 citations migrate now by the same method: the file named as a file-level anchor, the number kept beside it as data, no digit repaired or repointed. - check-docs-section-name.mjs: concept.mdx:426, doc-pages.mdx:257 and forms.mdx:183 each resolve to exactly one tracked file under content/docs/** (forms.mdx is ambiguous by basename alone -- two files share it -- but only content/docs/ui/forms.mdx has a line 183 carrying the cited "sections": [/* ... */] shape). - check-type-check-coverage.mjs: engine.test.ts:2547/2577 resolves to packages/services/service-automation/src/engine.test.ts, named by the surrounding prose ("service-automation's note"). - docs-audit/affected-docs.mjs: three rest-server.ts citations resolve to packages/rest/src/rest-server.ts, the only tracked file of that name. The corpus residual (scripts/check-scripts-symbol-anchors.mjs --list-unresolvable) drops from 15 to 8, all 8 still held by open PRs (objectstack-ai#16215, objectstack-ai#16202) -- none an ambiguity. judgeUntrackedLineAnchors stays false per the objectstack-ai#15809 fence (flips only at residual zero). Header prose updated to record both dated readings. Part of objectstack-ai#15809. Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 Co-authored-by: Claude <noreply@anthropic.com>
…y & access architecture (objectstack-ai#16478) * docs(adr): ADR-0132 mirrors the open half of cloud ADR-0024 — identity & access architecture Records, in this repository, the mechanism half of `objectstack-ai/cloud` ADR-0024 (Accepted there 2026-06-25) whose code lives here: per-environment identity (D1), the two user populations (D2), the managed-vs-env-native source-of-truth marking (D4), identity-from-the-IdP with authorization decided in the environment (D5) and the SSO-era local user-management surface (D5.2), per-environment SSO with opt-in domain verification (D6), preserved portability and self-host (D7), and env-local users with better-auth-backed organization membership (D9). Every clause carries a symbol anchor into this tree. The commercial half — D3, D5.1, D8, D10 and the V1 roadmap — stays in cloud and is still cited as `cloud ADR-0024`; a table in the record says so, because the re-pointing of today's bare citations is per-site and semantic, not a search-and-replace. No decision is added or widened. Files are not moved and numbers are not reassigned, per the maintainer ruling of 2026-09-02. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y * docs(adr): renumber the cloud ADR-0024 mirror 0132 → 0135 (0132 is claimed by an open PR) `0132` is free on `main` but claimed on the branch of an open PR — objectstack-ai#16215 adds `docs/adr/0132-multi-organization-runtime-is-open-core.md`. Verified here by a full `git diff --name-only origin/main...<pr-head> -- docs/adr/` over all 27 open PRs, not by titles: 0132 is objectstack-ai#16215's, 0133 is objectstack-ai#16267's, 0134 is objectstack-ai#16476's, and 0135 is unclaimed. File renamed with `git mv`, slug unchanged. The record's one self-citation (its title line) moves with it; the `ADR-0105` and `ADR-0108` references are landed records and are untouched. No line-number anchors were introduced — the file still carries none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --------- Co-authored-by: Claude <noreply@anthropic.com>
…orkspace can never supply (objectstack-ai#16907) * test(dogfood): make the enterprise-organizations control hermetic (objectstack-ai#16539) WIP: fixture-only subject + premise legs. Verification pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * test(dogfood): cite ADR-0132 beside objectstack-ai#16215 in the rewritten premises The landing that killed the old premise has a decision record; name it where the premise is restated, per Prime Directive objectstack-ai#13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 --------- Co-authored-by: Claude <noreply@anthropic.com>
…he card's pattern could not see (#16718) The card's measurement is a case-sensitive regex over `enterprise (@objectstack/organizations|multi-org|organizations)` plus two `closed-source`/`cloud-private` forms. Re-running it case-INSENSITIVELY, and widening to the neighbouring `cloud-private` / `enterprise package` spellings inside the two files the card names as its worst sites, turns up nine more population-1 statements. All nine are in files the previous commit already corrects; every one is a comment. - `cli/src/commands/serve.ts` (×5) · the host-importer docblock used `@objectstack/organizations` as THE example of "a cloud-private package the framework cannot see" — the same reasoning #16539 already applied to the dogfood probe: it is no longer a name this repo can use as an example of something a host root does not have. The example is now framed on what actually decides resolution (the host app's declaration), with ADR-0132's two-packages fact named. · the cloud#1013 narrative said the load "never found the cloud-private package" — true of 2026-07, asserted in the present tense. Now "then-cloud-private": the history is kept, the present claim is dropped. · the live Stage-1 mount comment said "This package is cloud-private" in the boot path itself. Now APP-DECLARED, citing ADR-0132 D3 — which build of the name a deployment gets is the host manifest's call, not `serve`'s. · the ADR-0093 D5 fail-fast comment said "the enterprise package can't provide tenant isolation". - `plugin-dev/src/dev-plugin.ts` — `ENTERPRISE @objectstack/organizations`, in capitals. The card's pattern is case-sensitive, so this hit was invisible to the 65-occurrence count while sitting two lines above one that was counted. - `plugin-auth/src/auth-plugin.ts` — "so the enterprise package needs no change to light it up", on the `org-scoping` presence probe. - `qa/dogfood/test/enterprise-organizations.ts` — the doc heading on `ORGANIZATIONS_PKG` still opened "The enterprise multi-org package", directly contradicted by the⚠️ #16539 note two lines below it. - `qa/dogfood/test/rls-multitenant.dogfood.test.ts` — "moved to the ENTERPRISE package ... not part of this open workspace". Both halves false since #16215. The skip is unchanged and its real reason is now stated: the gate needs the runtime resolvable FROM THE APP UNDER TEST, which is what `organizationsAvailable` probes. ⛔ No behaviour change: every hunk is a comment. Populations 2 and 3 are untouched — see the PR body for the per-site accounting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…se/closed-source (objectstack-ai#16718) (objectstack-ai#17371) * docs(organizations): stop calling the open multi-org runtime enterprise/closed-source (objectstack-ai#16718) ADR-0132 moved the `org-scoping` registrar into open core: `packages/plugins/organizations` is Apache-2.0, carries no licence check of any kind, and declares `['group','isolated']` as its own constant. objectstack-ai#16137's acceptance proved an open-only composition raises the `isolated` wall with no licence check reachable. The prose did not follow — 65 occurrences across 45 files still described that package as enterprise / closed-source / cloud-private. Prose only. Every non-comment hunk is a string literal (an operator message, a flag description, a conformance-matrix note, a test expectation); no control flow, no identifier and no behaviour changes anywhere. Corrected (population 1), led by the two sites the card names: - `plugin-auth/src/tenancy-service.ts` — open core's own authority for resolving the posture. Its header said both walled postures require the *enterprise* package and that multi-org operation is *a commercial capability*; `isolationActive` argued "ENABLING a multi-organization posture is an entitlement". Rewritten around ADR-0132 D2/D3/D4: the mechanism (both postures probe the same registrar) is unchanged, the answer to "can an open install run isolated?" is now yes, and the `probeEntitledPostures` seam is explained as existing for the commercial subclass rather than as a tier. - `cli/src/commands/serve.ts` — the mount block no longer says the runtime "lives in the closed-source package", and the operator-facing install remedy no longer ends "this runtime is closed-source and is NOT on the public npm registry ... Without one this bullet is not followable". That sentence told an open-source operator the remedy they had just been given was unusable. - `cli/src/utils/tenancy-posture-hints.ts` — the `isolated` hint both `os serve` and `os doctor` render. Not in the card's count: its regex cannot see `the enterprise ${ORGANIZATIONS_RUNTIME_PKG}`. - `plugin-dev/src/dev-plugin.ts` — two more operator-facing strings (the degraded-tenancy warning and the stage-2 mount refusal). Because one name now denotes two packages (ADR-0132 D3), every rewritten site names which one it means; where a site was load-bearing on ADR-0105 D12 the citation is repointed at D12-as-amended rather than deleted. Pin tests updated deliberately, with the reason recorded in each file: `serve-organizations-message-spelling.test.ts` (5 hits) and `doctor-organizations-message-spelling.test.ts`. The pins were not relaxed — they still compare whole rendered lines built from `Serve.ORGANIZATIONS_RUNTIME_PKG`, and both CONTROL blocks still differ from their expectation only in spacing. The doctor roster assertion still pins `edition: 'enterprise'`, which stays true: the roster records the ENTITLED variant. Left alone on purpose, and why, in the PR body: 2 hits that describe the genuinely commercial same-named package, and 15 that state the package is absent/not installable in this workspace as the reason a fixture fakes or skips a proof — the live capability question the card dispatches as report-only. Refs ADR-0132, ADR-0105 D12 (as amended), objectstack-ai#16130, objectstack-ai#16137, PR objectstack-ai#16215. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(organizations): correct the enterprise/closed-source spellings the card's pattern could not see (objectstack-ai#16718) The card's measurement is a case-sensitive regex over `enterprise (@objectstack/organizations|multi-org|organizations)` plus two `closed-source`/`cloud-private` forms. Re-running it case-INSENSITIVELY, and widening to the neighbouring `cloud-private` / `enterprise package` spellings inside the two files the card names as its worst sites, turns up nine more population-1 statements. All nine are in files the previous commit already corrects; every one is a comment. - `cli/src/commands/serve.ts` (×5) · the host-importer docblock used `@objectstack/organizations` as THE example of "a cloud-private package the framework cannot see" — the same reasoning objectstack-ai#16539 already applied to the dogfood probe: it is no longer a name this repo can use as an example of something a host root does not have. The example is now framed on what actually decides resolution (the host app's declaration), with ADR-0132's two-packages fact named. · the cloud#1013 narrative said the load "never found the cloud-private package" — true of 2026-07, asserted in the present tense. Now "then-cloud-private": the history is kept, the present claim is dropped. · the live Stage-1 mount comment said "This package is cloud-private" in the boot path itself. Now APP-DECLARED, citing ADR-0132 D3 — which build of the name a deployment gets is the host manifest's call, not `serve`'s. · the ADR-0093 D5 fail-fast comment said "the enterprise package can't provide tenant isolation". - `plugin-dev/src/dev-plugin.ts` — `ENTERPRISE @objectstack/organizations`, in capitals. The card's pattern is case-sensitive, so this hit was invisible to the 65-occurrence count while sitting two lines above one that was counted. - `plugin-auth/src/auth-plugin.ts` — "so the enterprise package needs no change to light it up", on the `org-scoping` presence probe. - `qa/dogfood/test/enterprise-organizations.ts` — the doc heading on `ORGANIZATIONS_PKG` still opened "The enterprise multi-org package", directly contradicted by the⚠️ objectstack-ai#16539 note two lines below it. - `qa/dogfood/test/rls-multitenant.dogfood.test.ts` — "moved to the ENTERPRISE package ... not part of this open workspace". Both halves false since objectstack-ai#16215. The skip is unchanged and its real reason is now stated: the gate needs the runtime resolvable FROM THE APP UNDER TEST, which is what `organizationsAvailable` probes. ⛔ No behaviour change: every hunk is a comment. Populations 2 and 3 are untouched — see the PR body for the per-site accounting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): operator-facing organizations text is no longer enterprise-only (objectstack-ai#16718) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…son the cross-tenant proofs stand in (objectstack-ai#17910) Fixes objectstack-ai#17369 Clause-②: no > **Re-written by the `domain:cli` seat (objectstack-ai#6024) at 2026-09-13T01:17Z after a body rewrite dropped it.** The declaration is the seat's, ⛔ not the implementer's, and it is **re-measured on this head** `7990da9233af387b15acd8c5e8e748855087d450` (unchanged since 00:38Z) against merge base `a9c64779046facc1b0b2e74f71a9441b0f5b7fb8`: added exported declarations in `packages/verify/src/harness.ts` — the diff's only non-test source file — **0**, control lit (`export` occurs **5** times there); **non-comment added lines in that file: 0**, so 「comment-only」 is measured rather than asserted; whole-diff added `export const|function|class|interface|type|default`: **0**; `package.json` files touched: **0**, so no dependency moved and ADR-0132's entitlement-boundary pin keeps its premise. ⇒ the axis stands down and `@objectstack/verify: patch` stands. >⚠️ **Please leave these four lines in place.** `scripts/check-changeset-no-major.mjs` reads this declaration out of the PR body; removing it re-reds `Check Changeset` with no push involved, which is what happened at 01:15:13Z. The seat's process fault, owned on the card at the record below: the gate was cleared at 00:40Z and the author was never told, so a later body rewrite clobbered it. Option 3, as ruled (director batch objectstack-ai#114 item 3, carried by the maintainer's 「其他同意」 at `5629544916`): **the prose moves, the behaviour does not.** Zero dependency changes, zero behaviour changes, `no-framework-dependents.pin.test.ts` untouched and green. Acceptance is triage's rewritten one (`5649534177` §③), not the ruling's execution line: **class (i) reaches zero**, never "a phrase reaches zero". > 4. ⛔ **永远不要求 `git grep -nF "not installable"` 归零** That grep still reads 5 hits / 5 files, and it is supposed to. ## The enumeration — method, then membership Subject + cause, **with context**, exactly as triage prescribed after breaking the single-line form on this very card: ``` git grep -nE -C2 "cloud-private|not installable|closed-source" ``` On the merge base `a9c64779046facc1b0b2e74f71a9441b0f5b7fb8` (non-shallow): **60 hits / 36 files**, minus published `CHANGELOG.md` and `.changeset/**` ⇒ **47 hits / 30 files**. Every hit was then read in context and classified by hand — ⛔ no single-line `| grep -i organizations` filter, which is what hides a site whose subject and cause sit on different lines (`harness.posture.test.ts` is one: the subject is on line 9, the cause on line 10). **Membership proof against the claim's 30-file set** (`5649601883`) — ⛔ not a count agreeing with a count: | check | reading | |:--|--:| | files in my enumeration | 30 | | files in the seat's table | 30 | | in mine, not in the seat's | **0** | | in the seat's, not in mine | **0** | | symmetric difference | **0** | | intersection (the instrument firing) | **30** | The two sets are equal by membership, and the intersection is non-empty, so the comparison instrument is live rather than silently matching nothing. ## Per-site classification — all 47 in-scope hits **(i) still states the stale reason as currently true = MUST-FIX · (ii) already past tense / already recorded as "no longer cloud-private" = not touched · (iii) the subject is another package = not touched** Counts: **(i) 21 hits / 13 files — all fixed · (ii) 19 hits / 13 files · (iii) 7 hits / 5 files**. ### Class (i) — fixed (21 hits, 13 files) | site | what it said | why (i) | |:--|:--|:--| | `packages/verify/src/harness.ts:45` | "Stand-in for the **cloud-private** `@objectstack/organizations` runtime" | the epithet is the justification for the stand-in, present tense | | `packages/verify/src/harness.ts:159` | "instead of requiring the **cloud-private** enterprise package" | same, in the `BootOptions.multiTenant` TSDoc | | `packages/verify/src/harness.ts:513` | "is **cloud-private** and only ever lives in the host app's `node_modules`, **so** the import could never succeed" | explicit causal `so` | | `packages/verify/src/harness.ts:534` | "is **cloud-private** and resolves from nowhere in the framework workspace" | present-tense cause | | `packages/verify/src/harness.posture.test.ts:10` | "The enterprise package is **not installable in this workspace**, **so** a fake stands in for it" | explicit causal `so` — the card's headline site | | `packages/verify/src/harness.posture-only.test.ts:4` | "without the **cloud-private** enterprise runtime" | epithet justifying the stand-in | | `packages/verify/src/harness.posture-only.test.ts:12` | "is **not installable in this workspace**, **so** the fixture needed an honest way…" | explicit causal `so` | | `packages/verify/src/harness.host-resolution.test.ts:9` | "is **cloud-private** and only ever lives in the verified app's `node_modules`, **so** the import could never succeed" | explicit causal `so` | | `packages/verify/src/harness.host-resolution.test.ts:24` | "(it is **not installable in this workspace** — that is the whole point)" | states the stale cause as the point of the fixture | | `packages/cli/test/serve-organizations-host-resolution.e2e.test.ts:9` | "is **cloud-private** and only ever lives in the served app's `node_modules`, **so** the import could never succeed" | explicit causal `so` | | `packages/cli/test/serve-organizations-host-resolution.e2e.test.ts:24` | "(it is **not installable in this workspace** — that is the whole point)" | as above | | `packages/types/src/node.test.ts:10` | "while the package **is cloud-private** and only ever exists in the host app's `node_modules`. It could **therefore** never resolve" | present-tense cause inside a past-defect narrative | | `packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts:41` | "runtime **is cloud-private and genuinely absent from this workspace**" | both halves false — the package IS a workspace member now | | `packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts:78` | "the single honest walled harness is `multiTenant: true` with the **cloud-private** `@objectstack/organizations`" | the epithet is why feasibility is in doubt | | `packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts:22` | "is a **cloud-private** enterprise package **genuinely absent from this workspace**, **so** the dynamic import genuinely fails" | explicit causal `so`, and "absent from this workspace" is false | | `packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts:23` | "off the **genuinely-absent cloud-private** package" / "the thing the open-source workspace cannot have" | same premise, restated | | `packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:23` | "is a **cloud-private** enterprise package that is **genuinely absent from this workspace**, **so** the dynamic import genuinely fails" | explicit causal `so` | | `docs/qa/platform-checklist/areas/attachments-storage.json:189` | MARKER: "`@objectstack/organizations` is **cloud-private**, **so** the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(...)`" | explicit causal `so` | | `docs/qa/platform-checklist/areas/attachments-storage.json:277` | same MARKER string, second carried copy | as above | | `docs/qa/platform-checklist/areas/attachments-storage.json:358` | same MARKER string, third carried copy | as above | | `content/docs/plugins/packages.mdx:331` | "Ships as a **separate, closed-source enterprise package** — it is **not part of the open framework repo**" | ⭐ named by neither the card, the objection nor triage; the most customer-visible site in the set, and flatly false | ⭐ **The `attachments-storage.json` cluster is the one the order singled out, and the effect survives verbatim.** The three copies carry the same MARKER string; only the cause clause changed. The sentences that carry the *effect* — "skips BY DESIGN here. That is `blocked(dependency)`, never a pass and never a defect. A run that is supposed to ship the package sets `OS_TEST_MULTI_ORG_ENABLED=1`, which turns the skip into a hard failure instead of a silent one." — are byte-identical to before, and all three copies stay identical to each other (verified: 3 occurrences before, 3 after, and the file still parses as JSON). ### Class (ii) — already past tense or already self-correcting (19 hits, not touched) | site | why (ii) | |:--|:--| | `packages/cli/src/commands/serve.ts:1107` | already past-tensed: "the **then**-cloud-private package".⚠️ triage cited this as `:820`; on `a9c64779` it sits at **`:1107`** — the only §1/§3 figure that moved | | `packages/rest/src/rest-api-plugin.ts:300` | record of a **cloud-side** measurement ("cloud#1982 reproduced it … mounted") | | `packages/rest/src/single-kernel-isolated-api-key-matrix.test.ts:21` | same: "cloud#1982 reproduced it on `apps/objectos-ee`" | | `packages/rest/src/single-kernel-isolated-session-org-claim-matrix.test.ts:11` | same: "On a real `objectstack serve` of **cloud's** `apps/objectos-ee`" | | `packages/core/src/security/resolve-authz-context.ts:483` | same incident, past tense ("**measured on** a live `isolated` boot") | | `packages/core/src/security/resolve-authz-context.test.ts:1604` | under the heading "## What **was** measured, before the guard existed" | | `packages/qa/dogfood/test/enterprise-organizations.ts:26` | "while the package **was** cloud-private and **lived** in the host app's `node_modules`" | | `packages/qa/dogfood/test/enterprise-organizations.ts:59` | quotes prose it says the file "**used to**" carry | | `packages/qa/dogfood/test/enterprise-organizations.ts:101` | "⚠️ objectstack-ai#16539: **no longer cloud-private** — ADR-0132 / objectstack-ai#16215 brought it back to open core" | | `packages/qa/dogfood/test/enterprise-organizations.ts:152` | "the clause that **used to** close this comment … **died with** objectstack-ai#16215" | | `packages/qa/dogfood/test/enterprise-organizations.test.ts:7` | "The **old** probe answered 'unavailable' … because it resolved a cloud-private package" | | `packages/qa/dogfood/test/enterprise-organizations.test.ts:20` | "`@objectstack/organizations` **was** cloud-private" | | `packages/qa/dogfood/test/enterprise-organizations.test.ts:53` | "(… **was** cloud-private **when these cases were written**)" | | `packages/types/src/node.test.ts:50` | "a real cloud-private package **at the time**", then records objectstack-ai#16215 explicitly | | `packages/plugins/organizations/src/open-only-wall-acceptance.test.ts:20` | "**Until** ADR-0132 moved this package into the open core that **was** unavoidable — the only registrar **was** cloud-private" | | `packages/services/service-cluster/src/multi-node-gate-mount.ts:61` | the same doc block corrects itself eight lines later: "⚠️ `@objectstack/organizations` is ONE NAME over TWO packages **since ADR-0132**" | | `docs/adr/0132-…:35` | history: records what cloud ADR-0081 D2 did. ⛔ ADRs are a maintainer floor and out of scope | | `docs/adr/0132-…:218` | ADR-0132 stating its own consequence. ⛔ out of scope | | `docs/adr/0105-…:357` | already carries an editorial note saying those sentences 「read as history」. ⛔ out of scope |⚠️ **The five cloud-measurement rows are the one judgement call in this table a reviewer might reasonably take the other way**, so the reasoning is stated rather than implied. Each records a measurement performed in the **commercial** repository, on cloud's `apps/objectos-ee`, with the copy that app declares. ADR-0132 split one package name across two packages, and this repository says so in its own words at `multi-node-gate-mount.ts`: "the framework publishes an Apache-2.0 package of that name and **the commercial repo keeps a private licence-gated subclass of it**". The copy those five sentences name is therefore still cloud-private, and the sentences are still true. `serve.ts:1107` — which triage excluded by name — is the same shape with an explicit `then-` marker; these five are the same shape without one. ### Class (iii) — the subject is another package (7 hits, not touched) | site | subject | |:--|:--| | `content/docs/kernel/services-checklist.mdx:74` | **`@objectstack/service-ai`** — and true as written. This is the hit that makes the ruling's grep-to-zero a wrong edit | | `packages/types/src/node.ts:34` | the epithet attaches to **`@objectstack/service-ai-studio`**, not to organizations. The claim it does make about organizations — host-supplied, invisible to a bare import — is still true (measured below) | | `packages/types/src/node.test.ts:74` | "none of them can see **a cloud-private package**" — the class of such packages generically, and true | | `packages/spec/src/kernel/platform-capabilities.ts:204` | closed-source `@objectstack/` packages **in general** | | `packages/spec/src/kernel/platform-capabilities.ts:252` | the roster row for **`@objectstack/security-enterprise`** (ADR-0057) | | `packages/spec/src/api/error-code-ledger.zod.ts:43` | a closed-source **product's** error states | | `content/docs/references/api/error-code-ledger.mdx:47` | generated from the row above — and `content/docs/references/` is ⛔ never hand-edited | ⇒ The order's `packages/spec` reading is confirmed independently: **neither spec hit names `@objectstack/organizations`**, so nothing here routes to the `domain:spec` seat and no file under `packages/spec/**` is touched. ## The two controls, printed | control | before | after | expected | |:--|--:|--:|:--| | **must FIRE** — `entitlement boundary` | 2 files | **15 files** | fires; it grew because every corrected site now cites the real cause | | **must say NO** — `not installable in this galaxy` | 0 | **0** | still says no, so a zero here is a reading and not a dead instrument | | **must NOT be driven to zero** — `git grep -nF "not installable"` | 5 hits / 5 files | 5 hits / 5 files | unchanged on purpose (`services-checklist.mdx:74` is true) | Class (i) after the change: **0**. Remaining in-scope hits: 32 — every one of them class (ii) or (iii), including five new ones created by the corrections themselves, which say in so many words that being closed-source is *not* the reason. ## The pin, and the boundary it defends ⛔ **Zero dependency changes.** `git grep -l '"@objectstack/organizations"' -- '*package.json'` returns **exactly one** file before and after — `packages/plugins/organizations/package.json`, the package's own manifest. `no-framework-dependents.pin.test.ts` is not in this diff and is green. ## Measured while classifying — the `plugin-dev` premise holds, and is not a function of build state Three `plugin-dev` sites said the dynamic import fails because the package is "genuinely absent from this workspace". Since ADR-0132 it is a workspace member, so before rewriting the cause I had to know whether the **effect** is still true — the order's STOP rule covers exactly this ("if a class-(i) site cannot be made to tell the truth without a behaviour change — STOP and report"). It is true, and for the entitlement-boundary reason: - A bare ESM `import('@objectstack/organizations')` from `packages/plugins/plugin-dev`, run on this file's own vitest runner, answers `ERR_MODULE_NOT_FOUND` **"Cannot find package"** — the package directory is never found. - **Ablation, with `packages/plugins/organizations/dist` stubbed present**: same answer, `ERR_MODULE_NOT_FOUND` "Cannot find package". So the signal is **not** a function of build state. - **Discriminating control, same moment, same tree**: a `require`-shaped resolve with the pnpm bin shim's `NODE_PATH` *did* resolve, to `packages/plugins/organizations/dist/index.js` — so the hoisted store copy is genuinely reachable that way, and the instrument can say "found". Node's ESM resolver does not consult `NODE_PATH`, which is why the two differ. - The stub was removed and the removal proven by `git status --porcelain` on that path being empty; the throwaway probe test was deleted before the first commit. ⇒ No behaviour change is needed anywhere in class (i), so the STOP rule did not fire. That measurement is recorded in `dev-plugin-tenancy-failfast.test.ts` itself, because it is the reason that file may keep reading a real failure rather than a stubbed one — and it is the same build-state trap objectstack-ai#16539 had to move a `require`-shaped probe off a workspace name to escape. ## Changeset The gate's own words, from `pr-automation.yml`'s "Require a changeset (or the `skip-changeset` label)" step: > 2. It releases nothing (`.github/`, `.claude/`, `skills/`, `docs/`, `content/`, `examples/`, tests-only, and the like) > -> apply the 'skip-changeset' label. (the workflow marks this route PREFERRED) This PR is **not** tests-only, so route 2 does not apply and the label would be wrong. Measured rather than assumed: `packages/verify/src/harness.ts` is source, its TSDoc is carried into `dist/index.d.ts`, and `dist` is in that package's `files[]`. After a real build of `@objectstack/verify` and its closure, the corrected sentence is present in `packages/verify/dist/index.d.ts`, with a **positive control** (untouched TSDoc prose from the same block: HIT) and a **stale-cache probe** (`cloud-private` in that `.d.ts`: absent, so the artifact is fresh rather than a turbo cache hit).⚠️ The first attempt at that measurement read **zero for the subject and zero for the control** — a single-line `grep` for a phrase the formatter had wrapped across two lines. A zero against a dead instrument is not a reading; the figures above come from the whitespace-normalised re-measurement. ⇒ Route 1, **`patch`, `@objectstack/verify` only**. The other three touched published packages (`cli`, `plugin-dev`, `types`) changed only `*.test.ts` files, which the shared tsup entry (`entry: ['src/index.ts']`) never reaches and which CI's "No compiled test files in any dist" step forbids in `dist` anyway; `packages/qa/dogfood` is `private: true`. ⛔ No package is named whose published bytes do not move, and no level is raised: no behaviour and no public surface moves, so `patch`. ## Verification Everything below was run on this branch. Exit codes captured **before any pipe**; every heavy run went through `scripts/pm/os-verify-lock.sh` (slot `issue-17369-cli-seat`). | what | command | reading | |:--|:--|:--| | derived gate set | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | **92** families (85 before the changeset existed; the changeset added 7) | | all 92 | each run individually | **92 green** | | reconciliation | `node scripts/pm/dispatch-gates.mjs --ran … --repo …` | `92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN` — a **derived** zero: every family carries a recorded exit code and none is 3 | | repo lint | `pnpm lint` | **exit 0**, whole repo, unnarrowed (`eslint . --no-inline-config`) | | ⭐ the pin | `pnpm --filter @objectstack/organizations test` | **exit 0** — `no-framework-dependents.pin.test.ts` green, and not in this diff | | the pin's package typecheck | `pnpm --filter @objectstack/organizations typecheck` | **exit 0** | | plugin-dev | `test` · `typecheck` | **exit 0** · **exit 0** | | types | `test` · `typecheck` | **exit 0** · **exit 0** | | verify typecheck | `pnpm --filter @objectstack/verify typecheck` | **exit 0** | | the touched cli test | `OS_TEST_TIERS=nightly … vitest run test/serve-organizations-host-resolution.e2e.test.ts` | **3 passed (3)** | | the touched dogfood tests | `… vitest run test/automation-toggle-tenant-scope.dogfood.test.ts test/showcase-external-autoconnect.dogfood.test.ts` | **2 files, 11 passed (11)** | | full package build | `turbo run build --filter='./packages/*' --filter='./packages/*/*'` | **72 successful, 72 total** | **Three gates first read non-zero and are recorded twice on purpose**, because a prerequisite failure is neither a pass nor a finding: | gate | first reading | after the prerequisite existed | |:--|:--|:--| | `pnpm --filter @objectstack/spec run check:skill-examples` | exit 1 — its own text: "`packages/client-react/dist` holds no `.d.ts` declarations — the package is not built" | **exit 0**, 258 prose examples type-check | | `pnpm check:dual-build-cjs-loads` | **exit 3** — "PREREQUISITE NOT MET … Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured." | **exit 0** | | `pnpm check:type-check-debt` | **exit 3** — PREREQUISITE NOT MET (unbuilt closure), then exit 3 again on a tsc OOM under my own `--max-old-space-size` cap, which the gate names as the binding constraint | **exit 0** — "5 ledger entr(ies) re-measured in 72.1s, 55 raw tsc error(s) total, none above its recorded number" |⚠️ **`pnpm --filter @objectstack/cli exec vitest run --project integration …` returned exit 1 reading "No test files found" — recorded as NOT MEASURED, ⛔ not as a red.** I had assumed "spawns the CLI ⇒ integration tier" and that was wrong twice over: `packages/cli/vitest-tiers.ts` classifies `*.e2e.test.*` as a **nightly** tier, so with `OS_TEST_TIERS` unset the file is outside **both** projects' population by design. The passing reading in the table above is the one taken under the switch the file actually runs in. `test/vitest-tiers-partition.test.ts` is green (22 passed), so the partition itself is intact. ### ⛔ One pre-existing failure in `packages/verify`, proven not to be this diff — filed as objectstack-ai#17911 `pnpm --filter @objectstack/verify test` reads **`Tests 1 failed | 102 passed (103)`**. The failure is `harness.host-resolution.test.ts` → "CONTROL — the `declared-unresolvable` remedy is unchanged: declared, not installed (objectstack-ai#4719)", and it is **objectstack-ai#16539's defect in a file that card's fix did not reach**: - **My delta to that file is comment-only** — the failing assertion is at line 299, untouched; the diff is a doc-block header. - **Ablation, same built tree**: the file was restored from its `origin/main` blob `c89a1c1b1` and re-run — **same single failure, same case**. Restore proven by `git diff HEAD` empty, `git status --porcelain` empty, and the blob back to HEAD's `e4ea9ed7e`. - **Cause, two-legged on one tree**: `packages/plugins/organizations/dist` present → `1 failed | 6 passed (7)`; the same directory moved aside → **`7 passed (7)`**. So the control's verdict is a function of whether a sibling package has been built. ⇒ Not fixed here — it is a behaviour change to a test fixture, which this prose-only card may not make. Filed unassigned and bare as **objectstack-ai#17911**. ## Acceptance notes Measured in passing, in scope for nothing here: - **`packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts:35-37`** said `@objectstack/organizations` "is really absent" — the same falsified premise as the class-(i) hit eleven lines above it, but carrying none of the three enumerated phrases, so the ruled instrument cannot see it. Corrected in place to "it really does fail to resolve from here", because leaving it would have made the file contradict its own corrected paragraph. ⛔ Same file, same sentence, same defect — no new behaviour and no new verification surface. - **`content/docs/plugins/packages.mdx:329`** still heads the section `### @objectstack/organizations (enterprise)`. "enterprise" reads as a product tier rather than a licensing claim, and the heading is a doc anchor other pages may link to, so it is left alone. noted, not filed: renaming it is a docs decision, not a defect. - **`packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:29`** says a successfully mounted plugin is "unobservable in open-source CI". That is still true for `plugin-dev` (it may not declare the package), so it is not a class-(i) site — but the phrase "open-source CI" now carries the old framing. noted, not filed: no false statement, and no PR or person is queued to touch this file. - ⛔ **No `packages/spec/**` and no `docs/adr/**` file is touched**, and both readings that would have forced one were checked independently rather than inherited: the two spec hits name other subjects, and all three ADR hits are history. - **A pre-existing, build-state-dependent failure in `packages/verify/src/harness.host-resolution.test.ts`** — filed as **objectstack-ai#17911** after a targeted duplicate search (REST `/search` is 403 for this seat by design, so one targeted MCP `search_issues`, declared here as the channel switch; it returned 2 results, so the instrument fired rather than being silently empty). objectstack-ai#16539 is the same defect in `packages/qa/dogfood` and is **closed**; objectstack-ai#16552 applied the same remedy to `packages/types`. `packages/verify` was never covered. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… the workspace can never supply (objectstack-ai#17979) Fixes objectstack-ai#17911 Clause-②: no The host-resolution `declared-unresolvable` CONTROL builds a temp host app that DECLARES the enterprise multi-org package and deliberately does not install it, then pins the `declared-but-not-installed` remedy wording. Since ADR-0132 / objectstack-ai#16215 made that package a tracked workspace member, pnpm's hoisted store carries it and vitest's own `pnpm exec` bin shim exports a `NODE_PATH` that reaches that store — so on a built tree the CJS-shaped host resolve SUCCEEDS, the organization wall really comes up, and boot is refused several steps later by the membership-policy gate. **That is the half that matters: the control never reached the wording it exists to pin.** Red on any tree with a full local build, green on CI, and proving nothing in either state. This ports the repair objectstack-ai#16539 (dogfood) and objectstack-ai#16552 (`packages/types/src/node.test.ts`) already landed, to its third site. ## What changed - `bootStack` takes the subject as an option, `BootOptions.organizationsPackage`, defaulting to the real package (`ORGANIZATIONS_PKG`). Production callers never pass it, and the operator-facing sentence still names `@objectstack/organizations` literally — only the specifier moves. - The CONTROL — the one case whose entire content is an ABSENCE — hands in `@fixture/host-organizations`, a name this workspace can never contain. - A `PREMISE` block proves that absence instead of assuming it, including the leg that would have caught this card: the fixture subject fails on the BARE SPECIFIER, which an unbuilt workspace package can never satisfy. - ⛔ The other four cases deliberately stay on the REAL subject. That is what keeps the default pinned behaviourally: each is decided by something the ambient workspace cannot supply — an app-local `node_modules` copy, which wins over `NODE_PATH`, or the UNDECLARED arm, whose fallback is this module's own ESM `import()`, and Node's ESM resolver does not consult `NODE_PATH`. ⛔ No manifest gained `@objectstack/organizations` (ADR-0132's entitlement boundary; `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` holds it). ⛔ `packages/plugins/organizations/**` is untouched. ⛔ The ESM/CJS asymmetry is left exactly as measured — it is why this bites the host-resolution control and not the `plugin-dev` fail-fast tests. ## Two-leg ablation, redone here The card's ablation was the filer's testimony; triage could not re-run it. It was re-measured on this branch. One thing moves: whether `packages/plugins/organizations/dist` exists. Same tree, same command (`pnpm --filter @objectstack/verify exec vitest run src/harness.host-resolution.test.ts --maxWorkers=2`), exit codes captured before any pipe. | `organizations/dist` | before the fix (`9ccc4179e`) | after the fix (`250e87413`) | |:--|:--|:--| | **present** | exit **1** — `Tests 1 failed \| 6 passed (7)` | exit **0** — `Tests 11 passed (11)` | | **absent** | exit **0** — `Tests 7 passed (7)` | exit **0** — `Tests 11 passed (11)` | The before/present failure is the card's, verbatim — the assertion at the CONTROL receives the membership-policy gate instead of the remedy: ``` AssertionError: expected '✖ FATAL: tenancy posture \'isolated\'…' to contain 'It IS declared in /tmp/os-verify-org-…' + ✖ FATAL: tenancy posture 'isolated' is walled, but this deployment never declared a membership policy. … + This is NOT a licensing failure and NOT a missing package: the multi-org runtime is + present and about to enforce the wall. Do not chase either. ``` Restore discipline, both legs: `dist` backed up with `cp -a` outside the repo, restored, and the restore PROVED — `files_before=6 files_after=6`, `index.js=present`, `git status --porcelain -- packages/plugins/organizations` empty. A trap on EXIT/INT/TERM with absolute paths covered the case where the container's foreground cap killed the run mid-leg. No moved-aside build directory was left behind. ## The control demonstrably REACHES its wording now ⛔ Both-legs-green alone cannot distinguish "fixed" from "did not trigger", so the assertion was driven to failure on purpose: the EXPECTED tail was replaced with a sentinel the real remedy cannot contain, the mutation was proved on disk (anchor occurrences 1 to 0, sentinel occurrences 1), and the run's Received side is what the control actually got: ``` - It IS declared in /tmp/os-verify-org-host-not-installed-bLyCCs's package.json, so the declaration is not the problem — OS17911-SENTINEL-THE-CONTROL-REACHED-ITS-ASSERTION. + verify: multiTenant=true requires the enterprise @objectstack/organizations package (migrated from plugin-org-scoping, ADR-0105 D12). It IS declared in /tmp/os-verify-org-host-not-installed-bLyCCs's package.json, so the declaration is not the problem — repair the install there (`pnpm install`, un-prune, rebuild its dist). (Cannot find module '@fixture/host-organizations': the host app DECLARES it (dependencies: "*") but it could not be resolved. ``` Exactly one test failed under that mutation (the CONTROL), and the importer's own message names the BARE specifier — the failure shape leg 2 of the PREMISE pins. Restored from `HEAD` afterwards; restore proved by blob hash equality plus an empty `git diff HEAD`. ## Reverse control — the CONTROL still controls what it is for The fixture subject was deliberately pointed at a REAL workspace package name. Both legs go RED, and their failure modes differ, which is itself the mechanism: | subject | control verdict | what it received | |:--|:--|:--| | `@objectstack/organizations` (the historical name) | **RED**, exit 1 — `3 failed \| 8 passed` | the membership-policy gate — the original defect, reproduced | | `@objectstack/types` (a different real workspace package) | **RED**, exit 1 — `3 failed \| 8 passed` | `mod.OrganizationsPlugin is not a constructor` | In both, the PREMISE leg `is absent from every ambient store the runner exposes` also fires — that is the guard which would have caught this card in the first place. Each mutation was proved on disk before its run and restored from `HEAD` after it, with blob-hash equality and an empty `git diff HEAD` as the proof. ## Why CI has stayed green — measured, not suspected The card flagged this as the thing to measure first and did not assert it. It suspected the shard's dependency closure. The measurement says something stronger and simpler. Turbo's `test` task declares `dependsOn: ["^build"]` — a package's DEPENDENCIES are built, never the package itself. Nothing in the workspace may depend on `@objectstack/organizations` (ADR-0132, held by `no-framework-dependents.pin.test.ts`), so it is in no package's `^build` closure. Measured on this tree: ``` turbo run test --filter=@objectstack/verify --dry=json total tasks: 34 · @objectstack/organizations tasks: 0 CONTROL — @objectstack/spec#build present: true turbo run test test:repo --dry=json # the ENTIRE workspace total tasks: 228 · build tasks scheduled: 68 CONTROL — is @objectstack/organizations#build anywhere: false CONTROL — is @objectstack/organizations#test anywhere: true ``` ⇒ Not merely "this shard's closure need not include it": **no `turbo run test` task graph in this workspace can build it at all**, on any of the six Test Core shards, including the shard that runs the package's own suite. Its `dist/` never exists on a Test Core runner, so the host resolve never reaches the store and the control has been green for the wrong reason on every CI run. The contrast confirms it is job isolation rather than luck — `Build Core` runs `turbo run build --filter=!@objectstack/docs`, whose plan is **79 tasks and DOES include `@objectstack/organizations#build`** — on its own runner, whose `node_modules` no Test Core shard ever sees. The state that reproduces this is a developer's full local build, which is exactly the state someone verifying a change is in. Same shape objectstack-ai#16539 recorded for the dogfood copy. ## Verification - `pnpm --filter @objectstack/verify test` — exit **0**, `Test Files 14 passed (14)` / `Tests 107 passed (107)` (103 before; the 4 new ones are the PREMISE block). - `pnpm --filter @objectstack/verify typecheck` — exit **0**; `check:test-typecheck` holds at `0 file(s) / 0 error(s)`, so the shrink-only TEST_DEBT ledger did not move. - `pnpm --filter @objectstack/verify build` — exit **0**. - Dependency closure built with `turbo run build --filter='@objectstack/verify...' --filter='@objectstack/organizations...'` — 34/34 successful. - Gate families derived from the real change set with `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (not a hand-written list) and run individually with each exit code captured BEFORE any pipe: **52 commands, 50 exit 0**. ## Acceptance notes - The two remaining gate commands answered **exit 3 — `PREREQUISITE NOT MET`**, the code both scripts use for "nothing was measured", explicitly distinct from a finding's exit 1: `check:dual-build-cjs-loads` and `check:type-check-debt` both read built output for the WHOLE workspace. Their readings after a whole-workspace build are in the verification comment on the card. - **Scope note, declared rather than buried:** the claim comment's declared file face named only the test file and the changeset. The fix needs one more file — `packages/verify/src/harness.ts` — because the specifier is hard-coded in the seam under test, so the fixture cannot be renamed without the harness accepting the name. This is the same shape the precedent had: objectstack-ai#16539 changed `packages/qa/dogfood/test/enterprise-organizations.ts` (a non-test helper) to give `probeOrganizations` its optional `pkg` parameter. - **One judgement worth a reviewer's eye:** that precedent's seam lives in a PRIVATE package, so its parameter published nothing. `BootOptions` is exported from `@objectstack/verify`'s entry point, so `organizationsPackage` becomes published API and the changeset is `minor` rather than the `patch` a test-only fixture rename would take. It is additive and optional, and this interface already carries options that exist so a test can isolate one belt (`analytics`, `multiTenant: 'posture-only'`). The alternative — reading an option the type never declares — was rejected as a surface that lies. Say the word and it can be reshaped. - Noted, not filed: the CONTROL's docblock still carries a `⚠️ ` note about `./harness` being imported without its `.js` extension and the TS7006 that would add to this package's frozen TEST_DEBT entry. That remains true and untouched here; the one-line fix belongs to whoever takes that card. --- _Generated by [Claude Code](https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ord's own landing PR and commit Both records landed on `main` on 2026-09-07 (0132 via #16215, squash `c677cda816`; 0133 via #16267, squash `13c08356ab`, both merged by the authorized approver), while their `**Status**` lines still read `Proposed — awaiting the maintainer's hand-merge, which is (itself) the acceptance act`. Read by their own rule the acceptance act occurred; only the line was never updated. Anyone judging citability from the Status field got the wrong answer, and no gate reds on it. Each line now takes the form the positive controls 0130 :3 / 0131 :3 already use — `Accepted (date) — accepted by the merge that landed it on `main` ([#PR](url), commit `sha`), which is (itself) the acceptance act for a governed surface (Prime Directive #14).` — with that record's own landing facts, keeping each file's local form (0132 wraps under a list dash, 0133 is one unwrapped list line). 0132's trailing `Nothing below is settled until this record merges` sentence is dropped, which is what 0130 and 0131 did with theirs; 0133's trailing sentence says something else entirely (this file records no new decision) and is kept byte-for-byte. Nothing else moves: no decision text, no heading, no other line, and ADR-0134 is untouched — its `Proposed` mirrors cloud ADR-0071's own state and is self-explanatory. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
Part of #16130 — phase 2, the objectstack half only. The cloud half (bump the framework pin, strip what moved, subclass, keep the gate) is a separate dispatch and is not touched here.
⛔ DRAFT, and it stays draft. The diff touches
docs/adr/**, a governed surface: the maintainer hand-merges. Do not flip ready, do not arm auto-merge, do not queue.What this delivers
The
org-scopingregistrar, in open core. Everything else the maintainer's goal needs was already open — the Layer 0 wall, the three postures, the posture knob, the organization and invitation objects, better-auth's organization plugin inplugin-auth, and therequiresService: 'org-scoping'Setup gates. The registrar was the one missing piece, and without it an open install that setOS_TENANCY_POSTURE=isolatedcould not enforce it: its only route forward wasOS_ALLOW_DEGRADED_TENANCY=1, the wall configured but not enforced.New package
packages/plugins/organizations—@objectstack/organizations, Apache-2.0, carrying the 1300 lines comment 5556477998 classified as moving. The 306-line licence gate stays in cloud.Package name — measured, not assumed
The name is
@objectstack/organizations, the spelling the open CLI'sORGANIZATIONS_RUNTIME_PKGalready carries, so the loader needs zero changes. Two readings behind that:npm view @objectstack/organizationsreturns E404 — never published, so the name is free. (For contrast, the historical@objectstack/plugin-org-scopingis on npm at12.6.0, owned by the org, not deprecated and with no per-version deprecations — recorded here because the earlier proposal would have resumed it at 17.x with a gap.)plugin-org-scoping; all but four are CHANGELOGs. The four are ADR-0056 / ADR-0057 consumer lists,plugin-security's README (corrected below), and three fixtures insidescripts/check-published-readme-exports.mjswhose workspace maps are hand-supplied, so none of them resolves against the real tree. Nothing live points at the old name.Where the package sits, and why —
packages/plugins/organizationsRaised in review as possibly belonging at top level (
packages/organizations/), mirroring cloud's layout. Kept underpackages/plugins/on the record and on measurement. Stating the evidence so the maintainer can overrule it at hand-merge with all of it in view:.github/workflows/ci.yml— "Verify capability packages ship a runtime entry (no dts-only / half-built)" — enumerates exactlypackages/triggers/* packages/services/* packages/drivers/* packages/plugins/*, and it is the only such enumeration in that file; top-levelpackages/*has no equivalent. Its own comment says why it exists: "Capability packages (services / triggers / plugins) are loaded by the multi-tenant runtime via a DYNAMIC import of their published entry", and a half-builtdistmeans the capability silently fails to load. That is this package exactly —servereaches it throughimportFromHost, a dynamic import of the published entry — and a silent failure to load is precisely the "wall configured but not enforced" outcome this whole card exists to end. This package qualifies for that step today (buildscript,main: dist/index.js).plugins/ # Official plugins & drivers, and its Context Routing table routespackages/plugins/**→ Plugin Developer · Implements spec contracts. A repo-wide search finds no recorded retirement of that directory.packages/plugins/**family" into thecheck:test-typecheckinstrument family. This package'stsconfig.test.jsonfollows that family's shape and says so.packages/plugins/(plugin-pinyin-search, 2026-07-16) — later than the last new top-level package (sdui-parser, 2026-06-29).Plugin. It implements thePlugininterface and is mounted withkernel.use(). Three of the fifteen entries underpackages/plugins/already carry noplugin-prefix (knowledge-memory,knowledge-ragflow,embedder-openai), soorganizations→@objectstack/organizationsmatches the directory-basename convention there.packages/organizations,packages/service-cloud,packages/security-enterprise), so mirroring its layout would mean adopting a convention this repo does not use.packages/plugins/is a real maintainer ruling rather than an inference, this is a one-command move — but it should land with the ci.yml enumeration widened in the same change, or the runtime-entry guard is lost for the one package whose whole contract is being dynamically imported.⭐ The shared name, and how the entitlement stays mounted on EE
The commercial repo keeps a private package of the same name. That is the mechanism, not a collision — but it is also the sharpest hazard in this card, so it was measured rather than reasoned:
objectstack serveimportFromHost('@objectstack/organizations')—createRequire(join(hostRoot,'package.json')), gated on the served app's own declaration (#4719)artifact-kernel-factory.tsimport('@objectstack/organizations')from insidepackages/objectos-runtime, which declares itservice-cloud's control-plane presetlazyPlugin('com.objectstack.organizations', ...)reachingcontrol-plane-organizations.js, in a package that declares itAll six cloud manifests that declare the name declare it
workspace:*—apps/cloud,apps/ee-group-showcase,apps/ee-tenant-crm-showcase,apps/objectos-ee,packages/objectos-runtime,packages/service-cloud— and cloud's lockfile resolves every one tolink:its local package. The name appears three more times in cloud outside a dependency field, and none of the three is a dependency resolution: the package's ownname,security-enterprise'sobjectstack.preflightAlsoReadsarray entry, and apathsalias in that package'stsconfig.test.json. (Independently re-verified in review, which reached the same six and the same exceptions.) pnpm'sworkspace:protocol resolves only to the local workspace package — it cannot fall through to the registry, and a missing one fails the install rather than substituting silently. So the class an EE deployment mounts is decided by the manifest that declares the name, on all three paths, with no change to the open package's shape and no change to the loader.What the cloud half must preserve (stated now because it constrains that PR): keep the package name, keep it
private: true, and keep every host declaration atworkspace:*— never a semver range.What could break it, and is therefore forbidden here: a framework package taking
@objectstack/organizationsas its own dependency. Cloud consumes the framework bylink:, so that would put the ungated copy inside the tree an EE app links, reachable by a bare import that never consults the app's manifest — the entitlement bypassed by resolution rather than by any defect in the gate. Apps declare this package; packages do not.src/no-framework-dependents.pin.test.tsholds it mechanically, with an anti-vacuity control on the population and a control proving the detector can find a planted declaration.The three hard boundaries
multi-org-license-gate.tsstays in cloud. Not moved, not reimplemented, and no hook left for it. Its 306 lines and 12 exported symbols are untouched.MULTI_NODE_GATE_CARRIER_PACKAGESis unedited and the open package acquires neither the obligation nor thesecurity-enterpriseimport that discharges it.unavailable) and now succeeds while registering nothing (loaded-without-gate). Both leave no gate registered, so the fail-closed default refuses a multi-node verdict exactly as before.assertMultiOrgEntitled()and its import are the 18 deleted lines. Per Q1 → A, cloud subclasses and calls its gate in its own constructor; the open class takes no entitlement callback, hook, option or protected seam, andnew X.OrganizationsPlugin()keeps working for cloud's two call sites. A ⛔ comment sits where the call was, naming what may not be added there.Q2 — the two comment blocks
['group','isolated']moves as-is.The service name
org-scopingis unchanged, and so is the plugin idcom.objectstack.organizations. Both are anchored by open consumers and by cloud fixtures; the "kept on purpose" comment moves with the registration.The ADR amendment is in this PR
Provenancesection naming cloud ADR-0081 and its date, the D3 resolution mechanism, and the boundaries the code comments cite by number.supportedPosturesin the commercial runtime does not carry to the open package. Its status line records the amendment.#14508 as found: still
pm:queue, unassigned, no branch, no PR, last touched 2026-09-04 by triage. I am the first writer. Its scope is cloud ADR-0081 D1/D3/D4 (the open basics half) and its Shape section says D2 "stays in cloud" — ADR-0132 is what makes that one line stale. ADR-0132 says so explicitly and asks #14508's writer to cite this number rather than open a competing record; a comment on #14508 says the same.Dispositions changed from the phase-1 classification
Comment 5556477998 is the source of truth for what moves. Six changes, every one forced by a gate the open tree runs and cloud's does not — none of them changes what the code does:
claim-orphan-org-rows.test.ts,claim-org-seed-ownership.test.tscheck:engine-double-contract— the fakes'update()now opens withassertEngineUpdateDispatchorg-creation-no-cross-org-copy.test.tswalled-default-org-self-registrant.pin.test.tscheck:where-matcher— it read$and/$oras a field name, so a filter it cannot express answered "no rows", which every negative assertion in that file would have read as evidenceorganizations-plugin.tscheck:slot-lookup— theobjectql/metadatalookups are typed structurally instead of erased toany; that ratchet's baseline never growsorganizations-plugin.ts,membership-policy-gate.tscheck:doc-authoring— tracker ids leave a rendered boot refusal and a log line, and move to adjacent commentsADR-0081citationcloud ADR-0081ADR-0081resolves against this repo's ADR-0081 (the React page tier) — the exact collision ADR-0105's own citation note documentsThe three Apache-2.0 files move byte-identical — verified by
git hash-objectagainstcloud origin/main:packages/organizations/src/*(08eb3641…,3b8d03f6…,da45ee8f…and the two as-is test files), not by an insertion count.Consequence edits — claims this PR falsifies
Three places in the open tree assert something that stops being true on merge. Each is corrected here rather than deferred, and none changes behaviour:
packages/plugins/plugin-security/README.md— said "No such package exists — not on npm, and in no directory of this repo." It is a published README (the page npm renders). Now states the package is Apache-2.0 and in this repository, and how an EE deployment resolves the same name to a gated subclass.packages/spec/src/kernel/platform-capabilities.ts— the roster row's note said "closed-source … not on the public npm registry". Corrected. ⛔edition: 'enterprise'and the row key are untouched: the row records the entitled variant, and the carrier list's drift test reads that field.packages/services/service-cluster/src/multi-node-gate-mount.ts— the carrier header now records the one-name-two-packages fact and theunavailable→loaded-without-gatediagnostic change, with a ⛔ against "repairing" it.Plus the gate-driven bookkeeping a new package owes: the
isSystemcensus row for theorganization_idauto-stamp's elevation short-circuit (row 62, with the page's counts moved), the boot-refusal row forWALLED_MEMBERSHIP_POLICY_UNDECLARED, thecheck:cross-package-test-inputsradius plus itsturbo.jsontask for the D3 pin,llms.txt68 → 69, and the changesetfixedgroup.⛔ Not edited, deliberately:
packages/verify/src/harness.ts's "requires the enterprise @objectstack/organizations package" message andpackages/cli'sserve/doctorresolution. Teaching those about an open registrar is #16137's, and doing it here would be the wiring this PR explicitly does not claim.⛔ This PR does not claim the acceptance criterion
An open-only install with
OS_TENANCY_POSTURE=isolatedandOS_ALLOW_DEGRADED_TENANCYunset, booting with the wall ACTIVE and enforcing the matrix, is not measured here and is not claimed.objectstack servestill resolves the runtime from the served app's own declaration and is not wired to mount this package off the posture. That acceptance is discharged by #16137, which is blocked on this — per the PM's Q3 answer it is "run the existing open matrices (single-kernel-isolated-api-key-matrix.test.ts,single-kernel-isolated-session-org-claim-matrix.test.ts) against the real registrar instead of their hand-writtenposture: 'isolated'stub", not "build a walled showcase app".Verification
Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usableflock. The shared verify lock is declared Linux-only (flockis util-linux, and a stock macOS does not ship it), so every command below was run through that entry point without the lock — a declared narrowing, not a silent one. No serialization guarantee held for these runs, nor for any sibling agent in this container while they ran.Gate set derived mechanically —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, the machine-readable mode, never the human-readable output. 131 commands, run at11ca403f6— the final commit, after the SECONDorigin/mainmerge — 125 green. (Prior heads for comparison:6d7d0e4c9124 green,494cb77f7124 green, same split each time.) The six that are not green are all NOT MEASURED, none is a finding, and each is named with its reason:check:dual-build-cjs-loadsdist/in this tree.check:i18ncheck:type-check-debt@objectstack/spec check:skill-examplespackages/client-react/distholds no.d.ts; the gate refuses rather than compute a false green.check:merge-drivercheck-regen-pending.mjs --self-testfails identically in a clean worktree atorigin/main9f3989771(case: "a gate whose RUNNER is not installed"). Baseline captured, worktree removed.check:bash32-floorcoproc/;;&/[[ -v ]]; this host's/bin/bashis 3.2 and cannot even parse the probe text. Zero shell files in this diff.CI builds fresh on Linux, so all six are measured there. ⭐
check:dts-closurewas the seventh on the previous head and is green at this one: it had named@objectstack/embedder-openai, and building the dependency closure supplied its declarations — which confirms it was a prerequisite of this checkout rather than a finding about the diff.origin/mainwas merged into this branch twice (the PR readmergeable_state: dirtyon creation, and a conflicting PR gets no CI at all). The merge broughtpackages/spec, so per AGENTS.md §10 it was rebuilt andcheck:generatedre-run — All 15 generated artifacts are up to date. The merge driver also deferredcontent/docs/permissions/system-context.mdx, which is amerge=os-regenartifact git ran no text merge on;.githooks/pre-pushrefused the push until it was regenerated from the merged tree, and that regeneration is its own commit. It rewrote anchor lines only — 18file.ts:linecitations the incoming commits moved — with row 62, the section extents and every declared count unchanged. The whole gate set, the build closure,typecheckand the 76 tests were then re-run on the merged head.Second round, same two traps, same handling.
mainmoved again and the PR wentdirty; a conflicting PR gets no fresh CI, so a green badge on it is a false read. The merge was append-only — no rebase, no force-push, no squash — and clean, with zero conflicts. ⭐mainhas never touchedpackages/plugins/organizations, so the classification's premise did not move under this card; the three files both sides touched weresystem-context.mdx,dispatcher-error-vocabulary.tsandengine-double-contract.pinned.json, and my content survived in all three (row 62, the boot-refusal row, the three pinned fakes).packages/speccame in again, so it was rebuilt andcheck:generatedre-run — All 15 generated artifacts are up to date. Themerge=os-regendeferral fired again and.githooks/pre-pushrefused until the census was regenerated from the merged tree; that regeneration is its own commit and rewrote anchor lines only — 15 this time, proven rather than asserted: diffing the page against its pre-regeneration copy and filtering out anchor-bearing lines leaves nothing, row 62 is byte-identical, and the entire declared-counts table is byte-identical (106 sites / 20 packages / 45 files; rows 1–62 behaviour, rows 63–66 carry-onward). Every number in this section is from11ca403f6; none is carried forward.Package-level, all green at
11ca403f6:pnpm --filter '@objectstack/organizations^...' build— dependency closure first, in the new worktree.pnpm --filter @objectstack/organizations build—check-dts-emitted: 1/1 declared declaration file(s) present.pnpm --filter @objectstack/organizations run typecheck—tsc --noEmitclean andcheck:test-typecheck: OK — 0 file(s) / 0 error(s).**/*.test.ts, so "typecheck clean" alone would say nothing about the test layer.tsc --listFiles -p tsconfig.test.jsonputs all 14 files (7 source + 7 test) in the program — measured, not assumed. Zero residue, so the package ships notest-typecheck-debt.jsonand nogen:script, matching the ten zero-residue plugin packages rather than the four that carry ledgers.pnpm --filter @objectstack/organizations test— 7 files, 76 tests, all passing.serve-cluster-host-resolution,serve-organizations-message-spelling,doctor-organizations-message-spelling,serve-organizations-host-resolution,test/serve-capability-vocabulary— 67 tests, all passing, on a built closure.packages/spec'splatform-capabilities.test.ts: 27 passing.Imports re-verified rather than trusted. Every non-local specifier the moving code names was resolved against this tree:
Plugin/PluginContextandresetPlatformAdminEmailMemofrom@objectstack/core;isDefaultOrganizationBootstrapTrigger,ensureDefaultOrganization,EnsureDefaultOrganizationResult,isMembershipPolicy,MEMBERSHIP_POLICIESfrom@objectstack/plugin-auth(all reached through itsexport *barrel);resolveTenancyPosturefrom@objectstack/types;ServiceObjectfrom@objectstack/spec/data. Nothing failed to resolve, and nothing needed a shim.And the verdicts are about the source, not the last build.
tsconfig.jsoncarriespathsfor all five workspace dependencies andvitest.config.tscarries anchored-regex aliases for the three the tests reach as values — socheck:type-source-resolutionandcheck:test-source-aliasboth pass with this package resolving source, rather than the package being registered in either gate's shrink-only unaliased ledger.