Repository navigation
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>
… not cloud-private (objectstack-ai#18759) Fixes objectstack-ai#18542 Clause-②: no objectstack-ai#17379's census (comment 5704455410) left one five-sentence family that is neither "now false" nor "still true". Five measurement narratives in `packages/core/src/security` and `packages/rest/src` call the thing cloud's own app mounted "the real cloud-private `@objectstack/organizations`" (or "`Organizations` plugin"). Under ADR-0132 D3 — one name, two packages — that adjective never says which of the two it means: it is true of the licence-gated subclass a commercial deployment resolves the name to, and false of the package name, which in this workspace resolves to an Apache-2.0 member of it. Triage kept the five on one card so the adjective is chosen once. This is that choice. ## The adjective: `licence-gated` It is the tree's own established spelling for cloud's variant, and it is the property that DISCRIMINATES: the open-core build in this repository is not gated, the subclass cloud resolves the same name to is. Measured on this head: 16 live occurrences of `licence-gated` and zero of `license-gated` — `packages/plugins/plugin-security/README.md`, `packages/plugins/plugin-auth/src/tenancy-service.ts`, `packages/services/service-cluster/src/multi-node-gate-mount.ts`, `packages/spec/src/kernel/platform-capabilities.ts` (a runtime string), `packages/cli/src/commands/serve.ts` and four CHANGELOGs, all reading "a private, licence-gated subclass". Where the old adjective sat on the bare package NAME (the three `packages/rest` sites), the referent noun `subclass` is added with it, so the gate attaches to the variant and never to the Apache-2.0 package — otherwise the repair would trade one false reading ("this package is private") for another ("this package needs a licence"). The two `packages/core` sites already had a noun denoting the mounted instance ("`Organizations` plugin"), so they take the adjective alone. ### Why the alternatives lose * **`enterprise`** — it does not discriminate. `packages/spec/src/security/tenancy-posture.ts` uses "the enterprise `@objectstack/organizations` runtime" for the package required to ACTIVATE a multi-org posture, which since ADR-0132 is the open-core one. Picking it would relabel the ambiguity instead of removing it, and add five lines to the 107-line population the census already flagged as true-but-for-a-stale-reason. * **Keeping `cloud-private`** (the census's option C) — true of what cloud mounted, but it states the package is private: the exact claim ADR-0132 falsified and that a six-PR campaign (objectstack-ai#16215, objectstack-ai#16539, objectstack-ai#16552, objectstack-ai#17371, objectstack-ai#17369, objectstack-ai#17910) removed everywhere else. Five surviving instances re-seed that population. * **Dropping the adjective entirely** — right for an operator-facing message, and already ruled so for one: `packages/cli/src/commands/serve-organizations-message-spelling.test.ts` records "a commercial deployment resolves the same package name to a private licence-gated subclass, so any adjective this message picks is wrong for one of the two installs reading it". That ruling is about a message whose READER is unknown. These five are measurement narratives whose subject is named (`apps/objectos-ee`, cloud#1982): two of them contrast "objectstack#15163 measured it on the framework" with "cloud#1982 reproduced it with the real X", and with no adjective that contrast collapses into the same artifact twice — which is exactly the distinction ADR-0132 D3 exists to keep readable. * **`entitled`** — already carries a different job: `tenancy-posture.ts` uses it for ACTIVATION ("Open code, entitled activation") and doctor's roster pin for "the ENTITLED variant" of a roster row. ## The five sites Taken from objectstack-ai#17379's census, comment 5704455410 — not from the card body, and no fresh sweep was run. Line numbers are readings at this head, not identifiers. | # | file | the adjective as it stood | |--:|:--|:--| | 1 | `packages/core/src/security/resolve-authz-context.ts:483` | "the real cloud-private `Organizations` plugin" | | 2 | `packages/core/src/security/resolve-authz-context.test.ts:1604` | same sentence, the test twin | | 3 | `packages/rest/src/rest-api-plugin.ts:300` | "the real, cloud-private `@objectstack/organizations`" | | 4 | `packages/rest/src/single-kernel-isolated-api-key-matrix.test.ts:21` | "the REAL cloud-private `@objectstack/organizations`" | | 5 | `packages/rest/src/single-kernel-isolated-session-org-claim-matrix.test.ts:11` | same shape | File surface as declared in the claim: `packages/core/src/security/` and `packages/rest/src/`. The diff is those 5 files, both halves of the cross-domain surface, nothing outside it. No other session's edit was found in either file. ## Prose, not pins The three machine assertions the card names are NOT in this diff. `serve-capability-vocabulary.test.ts:92` and `doctor-organizations-message-spelling.test.ts:197` (`edition === 'enterprise'`) and `harness.host-resolution.test.ts:288,319` (operator strings in a regex) are class-3 `enterprise` sites in `packages/cli` and `packages/verify`; this card is the class-1-ambiguous `cloud-private` family in `packages/core` + `packages/rest`. `doctor-organizations-message-spelling.test.ts:188-194` was read before deciding, as the card requires. It argues that the roster row `edition: 'enterprise'` records the ENTITLED variant and is "deliberately not relaxed to match the prose reword". Nothing here disturbs it, and its reasoning is the one this diff applies — one name, two packages, and the roster names the entitled variant. No argument to move it is made. All five edited sites are comment prose. Nothing asserts their text: the three tests that read these two source files as TEXT (`authz-store-unavailable.test.ts`, `execctx-authz-input-seam-reachability.test.ts`, `objectql-slot-consumer-census.test.ts`) assert code shapes only — imports, catch patterns, and field reads whose scanner excludes comment lines by construction. ## Evidence * `pnpm --filter '@objectstack/rest...' --filter '@objectstack/core...' build` — exit 0 (both dependency closures). * `pnpm --filter @objectstack/core test` — 51 files / 1316 tests passed. * `pnpm --filter @objectstack/rest test` — 194 files / 3236 passed, 1 skipped. * `pnpm --filter @objectstack/core typecheck && pnpm --filter @objectstack/rest typecheck` — exit 0, each including its test layer. * `pnpm lint` (repo-wide `eslint . --no-inline-config`) — exit 0 in 86s, so no narrowing was needed and none is declared. * Gate families derived by `scripts/pm/dispatch-gates.mjs --commands` at this head: 53 derived, 51 run green, 2 NOT MEASURED — `check:dual-build-cjs-loads` and `check:type-check-debt` both exit 3, `PREREQUISITE NOT MET`: they read a whole-repo `dist/` this worktree does not have, and CI builds before running them. Reconciled with `--ran`: "53 derived famil(ies) accounted for — 51 run, 2 NOT-MEASURED". ### `skip-changeset`, measured rather than assumed Both packages publish, so the label was measured. After the build, `grep -rn "licence-gated" packages/core/dist packages/rest/dist` returns **zero** hits (exit 1, captured before any pipe). Positive control on the same channel: comment prose DOES reach published output — `cloud#1982` appears at `packages/rest/dist/index.d.ts:421`, out of a JSDoc on an exported declaration in `rest-server.ts` — and the exported symbols (`resolveAuthzContext`, `RestApiPlugin`) hit in `dist` as expected. The channel exists, the control fires on it, and none of the five edited comments is on it: nothing published moves. `Clause-②: no` holds for the same reason — no accept set, no authorable key and no export moves. ## Acceptance notes * **noted, not filed** — `packages/rest/src/single-kernel-isolated-api-key-matrix.test.ts` calls the same mounted runtime "the enterprise plugin" two lines below the edited sentence. That is a class-3 line (true under ADR-0132 D3, stale reason), and objectstack-ai#17379's class-3 population is out of this card's scope by the triage ruling, so it is left exactly as it was. Successor: none queued — the census's class-3 lane table lists no `packages/rest` lane. * **noted, not filed** — two shorter siblings of the same family already carry no adjective at all: `packages/rest/src/rest-server.ts` (the published JSDoc above) and `packages/core/src/security/api-key.ts`. Both are correct as written — they name the package, not a variant — and are unchanged. Recorded so the next reader does not read that absence as drift from this ruling. * The census's own out-of-scope finding about ADR-0132's `Status: Proposed` line is already reported on objectstack-ai#17379 and is not refiled here. --- _Generated by [Claude Code](https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af)_ Co-authored-by: Claude <noreply@anthropic.com>
… carries its own landing PR and commit (objectstack-ai#18711) Fixes objectstack-ai#18532 Clause-②: no ## The defect ADR-0132 and ADR-0133 both landed on `main` on 2026-09-07, and both still carried a `**Status**` line reading `Proposed (2026-09-06) — awaiting the maintainer's hand-merge, which is (itself) the acceptance act for a governed surface (Prime Directive objectstack-ai#14)`. Read by the record's own rule, the acceptance act it names had occurred; only the line was never updated — and the Status field is the one machine-readable statement of a governance record's force, so anyone judging "can I cite this as settled law?" from it got the wrong answer while the body below it told them nothing here is settled. The failure direction is silent: both files are on `main`, every gate is green, and nothing reds. The positive controls are in the same convention and one number apart — ADR-0130 and ADR-0131 each moved their line to `Accepted` on landing and pointed back at the PR and commit that landed them. This PR does the same for 0132 and 0133, with each record's own landing facts, and changes nothing else. ## The four Status lines, before and after Measured on this branch's base, `origin/main` at `f8eaf6704` (the card's own readings were taken earlier, on `1e496f979`). The two controls are quoted to show the target form; ⛔ neither is touched by this PR. **ADR-0130 :3 — positive control, unchanged** ```text **Status**: Accepted (2026-09-01) — accepted by the merge that landed it on `main` ([objectstack-ai#14151](objectstack-ai#14151), commit `682d03ba7`), which is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ``` **ADR-0131 :3 — positive control, unchanged** ```text **Status**: Accepted (2026-09-04) — accepted by the merge that landed it on `main` ([objectstack-ai#14976](objectstack-ai#14976), commit `0ed271574`), which is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14). Execution is paused under objectstack-ai#15193 and the maintainer's standing instruction; that pause is carried there, not by this status line. ``` **ADR-0132 :3 — BEFORE (three physical lines, wrapped under a list dash)** ```text - **Status**: Proposed (2026-09-06) — awaiting the maintainer's hand-merge, which is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ⛔ Nothing below is settled until this record merges. ``` **ADR-0132 :3 — AFTER** ```text - **Status**: Accepted (2026-09-07) — accepted by the merge that landed it on `main` ([objectstack-ai#16215](objectstack-ai#16215), commit `c677cda816`), which is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ``` **ADR-0133 :3 — BEFORE (one unwrapped physical line)** ```text - **Status**: Proposed (2026-09-06) — awaiting the maintainer's hand-merge, which is the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ⛔ Nothing in this file is a new decision; see [Provenance](#provenance--read-this-before-citing-this-file). ``` **ADR-0133 :3 — AFTER** ```text - **Status**: Accepted (2026-09-07) — accepted by the merge that landed it on `main` ([objectstack-ai#16267](objectstack-ai#16267), commit `13c08356ab`), which is the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ⛔ Nothing in this file is a new decision; see [Provenance](#provenance--read-this-before-citing-this-file). ``` Three shape decisions, each read off the controls rather than invented: 1. **Each file keeps its own local form.** 0132 :3 is a wrapped list item under a dash and stays one (three physical lines, wrapped at the same column its sibling bullets use); 0133 :3 is a single unwrapped list line and stays one. ⛔ Neither file is reformatted toward the other, and ⛔ neither is reformatted toward 0130 / 0131, which carry no list dash at all. 2. **0132 keeps `which is itself the acceptance act`; 0133 keeps `which is the acceptance act`.** That word differs between the two files today, and the controls do not settle it — 0130 and 0131 both say `itself`. Each file keeps the wording it already had. 3. **The trailing sentence is treated the way the controls treated theirs.** Neither 0130 :3 nor 0131 :3 carries a "nothing is settled until this merges" clause, so 0132's is dropped rather than turned past tense. 0133's trailing ⛔ sentence is a *different* sentence — it says this file records no new decision and points at Provenance, which is as true after the merge as before — so it is preserved byte-for-byte. ## Landing facts, and how each was verified | record | landing PR | squash commit | merged | merged by | |:--|:--|:--|:--|:--| | ADR-0132 | [objectstack-ai#16215](objectstack-ai#16215) | `c677cda816f70c3e46fa1e3ad451a6d43f50dd07` | 2026-09-07T06:15:26Z (commit date 2026-09-07T05:30:13Z) | os-zhuang | | ADR-0133 | [objectstack-ai#16267](objectstack-ai#16267) | `13c08356abc61f13acd5db0932f99d482c134588` | 2026-09-07T07:37:31Z (commit date 2026-09-07T07:19:25Z) | os-zhuang | Verification, per record, all four legs: - **Single-parent squash.** `git log -1 --format='%P'` returns exactly one parent for each (`acabd24879…` for 0132, `8976ea1120…` for 0133). - **The subject names the PR.** `…(ADR-0132) (objectstack-ai#16215)` and `…mirroring cloud ADR-0081 D1/D3/D4 with Provenance (objectstack-ai#16267)`. - **The commit introduced the file.** `git show --stat SHA -- docs/adr/0132-…md` → `1 file changed, 225 insertions(+)`; the same for 0133 → `273 insertions(+)`. Both are file creations, so each sha is the landing of its own record and not a later edit. - **The sha is an ancestor of `main`** — `git merge-base --is-ancestor SHA origin/main`, exit 0 for both.⚠️ **That ancestry reading needed a control leg, and the first reading was wrong.** This container's checkout is shallow. On the shallow window as fetched (boundary `5de93728a`, 2026-09-09T21:18Z) `--is-ancestor` answered **exit 1 — "not an ancestor"** for both shas, which are dated 2026-09-07 and therefore *outside* the window: a confident false negative, because the missing objects withhold the ancestry path without fabricating one. After `git fetch --shallow-since=2026-08-25 origin main` (694 → 2867 commits) both test legs answer exit 0, and two control commits at least as deep as the tested pair — `682d03ba7` (ADR-0130's own landing, 2026-09-01) and `0ed271574` (ADR-0131's, 2026-09-04) — also answer exit 0, so the negative is retired rather than merely re-rolled. All four exit codes are recorded. The `merged_at` / `merged_by` values come from one REST read each of `/repos/objectstack-ai/objectstack/pulls/16215` and `/pulls/16267`; both report `merged: true`, `base.ref: main`, and a `merge_commit_sha` byte-equal to the sha in the table. This also answers the card's explicitly unmeasured worry about ADR-0133 — whether its landing constituted the hand-merge its own line named. It did: `merged_by` is os-zhuang, the authorized approver. So both records are "the text went stale", ⛔ not "an unaccepted governance record is being cited as law", and one line each is the whole repair. **The date in `Accepted (…)` is the landing date**, as 0130 and 0131 do — 2026-09-07 for both, which is the date on both the squash commit and the merge event. ## Consumers — a reading, no edit `git grep -n 'ADR-0132\|ADR-0133\|0132-multi-organization\|0133-org-management'` at `a937609f6c`: **110 hits across 51 files**. ⛔ None is edited by this PR. By area: | area | files | |:--|--:| | `packages/plugins/**` (organizations, plugin-auth, plugin-dev, plugin-security) | 18 | | `packages/qa/dogfood/**` | 7 | | `packages/cli/**` | 5 | | `packages/verify/**` | 4 | | `packages/spec/**` | 3 | | `docs/adr/**` (0105, 0132, 0133 themselves) | 3 | | `skills/objectstack-data/**` | 2 | | `packages/services/service-cluster/**` | 2 | | `.changeset/**` | 3 | | `content/docs`, `docs/qa`, `packages/types`, `scripts/` | 4 | The card names the census on [objectstack-ai#17379](objectstack-ai#17379) as one reader built on ADR-0132; the grep shows the reliance is much wider than one card — `packages/plugins/organizations` alone cites it in its plugin, its README and nine tests, including pin tests that name the record as the authority for the wall they hold. ## Verification Derived — ⛔ no hand-fed path list — with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree; the tool took the change set itself (three-dot vs the merge base) and printed the two ADR paths. **18 families derived, 18 run, all exit 0**, at head `a937609f6c`: ```text node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-adr-symbol-anchors.mjs :: exit 0 node scripts/check-adr-symbol-anchors.mjs --self-test :: exit 0 node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:adr-anchors :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` Reconciled: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE` → exit 0, `18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of them is 3)`. Beyond the derived set: ```text node scripts/pm/check-governed-merges.mjs --test docs/adr/0132-….md docs/adr/0133-….md :: exit 3 pnpm lint (repo-wide, `eslint . --no-inline-config`, ⛔ not narrowed) :: exit 0 ``` **Exit 3 on the governed predicate is the verdict, ⛔ not a failure**: `GOVERNED — a human merge is the review record for this PR (objectstack-ai#9495 regime)`, `docs/adr/** ×2`. This PR is draft and stays draft; ⛔ no seat flips it ready, enqueues it or arms auto-merge. Exit codes were captured with redirect-then-`$?`, ⛔ never across a pipe.⚠️ **One gate refused before it measured anything, and was re-run rather than recorded as a pass.** On the first sweep `pnpm --filter @objectstack/lint run check:doc-formula-expressions` exited **3** — `PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built`, the script's own wording being `Nothing was measured: this gate exited before running a single check`. That is NOT MEASURED, neither green nor red. After `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` (through `scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0`) it exits 0. Only the exit-0 run is reported above.⚠️ **What this list does not cover** — the derivation says so itself, and it is repeated here so the green is not read wider than it is: 53 artifact-roster families (their `silent` verdict is a fact about a tracked list, ⛔ not a clearance), 11 families whose declared population is too wide to place, 2 families whose argv takes a value only CI supplies, 1 CI job scheduled by these paths whose 5 steps have no local invocation, and 14 families that apply only once a changeset exists — ⛔ not applicable here, see below. Those are CI's to answer on this PR. **Tree state.** `origin/main` moved three commits (`72dd95fa5a`) while this ran, and the derivation flagged the tree stale for one file it reads (`scripts/pm/post-stamped.mjs`). `origin/main` was merged in (5 files, all disjoint from this diff, no `os-regen` deferral), `@objectstack/formula` and `@objectstack/lint` were force-rebuilt because the merge moved `packages/formula/src/validate.ts`, and the whole sweep above was re-derived and re-run at the merged head `a937609f6c`. The re-derivation produced a byte-identical 18-command list and no staleness warning. Every number quoted here is from that head. ## Changeset `skip-changeset`. `docs/adr/**` is on the no-publish fast track: no package's `files[]` ships an ADR, and the diff is four lines of prose in two governance records. `Clause-②: no` — ⛔ nothing an author can write moves. ## Diff ```text docs/adr/0132-multi-organization-runtime-is-open-core.md | 6 +++--- docs/adr/0133-org-management-open-basics.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) ``` Two files, one logical Status line each. 0132 counts as three physical lines because its line is a wrapped bullet, before and after. ## Acceptance notes ⛔ None of the following is filed as a card; each is recorded here for the reviewing seat. 1. **ADR-0133 :85–:97 carries a pre-merge frame that survived its own merge, and is deliberately ⛔ not touched.** The block is headed `⚠️ **A note for the hand-merge, stated rather than decided.**` and ends `**If the maintainer prefers one combined record, that is still a merge-time call** — the two files are disjoint, so folding them is a move, not a rewrite.` It addresses a reader who has not merged the file yet. Quoted here because the dispatch required any such sentence be quoted before touching it: ⛔ it is not touched, because it asserts nothing about this record's acceptance state — it offers a structural option about folding 0133 into 0132 — and scope excludes every other line in both files. The same block already reads `That obstacle is gone — the record has landed`, about ADR-0132. 2. **Two premises in the dispatch text did not survive measurement on this tree; neither changed the deliverable.** (a) The dispatch said the `acceptance act` phrase occurs in the *bodies* of 0132 (twice) and 0133 (once), to be left untouched. Measured: `grep -c` gives **1** in each file, and in both cases that single occurrence **is** the Status line this PR edits — there is no body occurrence in either file to leave alone. The 2× / 1× counts belong to 0130 / 0131, which is what the card itself says. (b) The dispatch described 0133's Status line as carrying the `Nothing … is settled until this record merges` sentence. It does not; its trailing ⛔ sentence is the unrelated `Nothing in this file is a new decision` one, which is preserved. The card's own table quoted 0133 correctly. 3. **Short-sha spelling.** 0130 and 0131 use 9 hex digits, which is what `git rev-parse --short` emitted at their time. On this tree, after deepening, the default abbreviation is 10 — and 10 is also the spelling the card body and the claim comment both recorded (`c677cda816`, `13c08356ab`). This PR uses 10 so that the file, the card and the claim agree; the full 40-hex shas are in the landing table above. The sentence *form* is the controls' form exactly. 4. **⛔ ADR-0134 and the other 17 `Proposed` records are untouched**, per the card. 0134's `Proposed` mirrors cloud ADR-0071's own state and is self-explanatory; the other 17 were never measured by the card and a batch edit would treat an unmeasured population as a measured one. ## 维护者速读(草稿) **改了什么** —— 两份治理记录(ADR-0132、ADR-0133)的 `**Status**` 行,各改一行:从「Proposed —— 等待维护者手工合并,而那次合并本身就是接受行为」改成「Accepted (2026-09-07) —— 由把它落到 `main` 上的那次合并接受」,并各自回指自己的落地 PR 与提交(0132 → objectstack-ai#16215 / `c677cda816`;0133 → objectstack-ai#16267 / `13c08356ab`)。两份文件的其余内容、另外 17 份 `Proposed` 的 ADR、以及看着同形但必须排除的 0134,全部未动。整个 diff 是 2 个文件、4 行增、4 行删。 **为什么改** —— 这两份 ADR 早在 2026-09-07 就已经合并进 `main`,而且都是由授权批准人 os-zhuang 合的 —— 也就是说,它们自己那行字所说的「接受行为」**确实已经发生了**,只是没有人回去改那一行。ADR 的 Status 字段是这份记录唯一一处机器可读的效力声明:今天任何人(或任何工具)按它判断「这条能不能当既定法引用」,得到的都是错的答案,而正文还在要求读者「在这份记录合并之前,下面的一切都未定」。实测有 **51 个文件、110 处**引用这两份 ADR,其中 `packages/plugins/organizations` 的插件、README 和九个测试都把它当权威引用。⚠️ 这个失效是**沉默的**:两份都在 `main` 上,CI 全绿,没有任何门禁会因此变红 —— 所以它只能靠人读出来。同一套约定的 ADR-0130 / 0131 落地后都改成了 `Accepted` 并回指落地 PR,本 PR 照抄它们的拼法,⛔ 没有发明第三种。 **风险与代价(含回滚)** —— 风险极低:改的是四行散文,不改任何决策文本、任何标题、任何代码,不发布任何 npm 包内容(故 `skip-changeset`)。本地推导出的 18 个门禁族全部 exit 0,仓库级 `pnpm lint` 也 exit 0。⚠️ 唯一需要您知道的判断:**这两份记录被判定为「文字过时」,而不是「未被接受却已被当法引用」** —— 依据是两份的合并人都是 os-zhuang(授权批准人),卡片里原本存疑的正是 ADR-0133 这一条,现已查实。若您认为 ADR-0133 那次合并**不**构成它所说的接受行为,那么正确的修法与本 PR **相反**(要补一次接受行为,而不是改一行字),此时请直接关掉本 PR —— 回滚成本就是关掉它,没有任何其他产物需要撤。 **席位意见** —— **你要做的** —— ① 确认上面那条判断:ADR-0133 由 os-zhuang 合并,是否算作它那行字所说的「维护者手工合并 = 接受行为」。② 若认可,本 PR 是受管面(`docs/adr/**`),按 Prime Directive objectstack-ai#14 需要您本人的手工合并或一次授权批准 —— ⛔ 本 PR 恒为 draft,任何 AI 席位都不会把它翻成 ready、不会入队、不会开 auto-merge。③ 若您希望连带把另外 17 份 `Proposed` 的 ADR 一起盘一遍,那是另一张卡 —— 本卡刻意没量它们。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ Co-authored-by: Claude <noreply@anthropic.com>
…ors (objectstack-ai#19200) Third round of the `scripts/**` unresolvable-citation migration (PRs objectstack-ai#15806 and objectstack-ai#16338 were rounds one and two), **devx-lane slice only**. `scripts/check-scripts-symbol-anchors.mjs` registers `scripts/**` `.mjs` comment prose as a symbol-anchor corpus with `judgeUntrackedLineAnchors: false`, so a citation naming no tracked file is seen, counted and enumerated — and judged by nothing. This PR migrates **six** of those, by the gate's own repair form (its header, under `[line-anchor]`): > ANCHOR FORM: name the file as a file-level anchor and put the number beside it as data Not one digit is repaired or repointed. Every number that was part of an anchor is still on its page, as data. ## The six, and why each target is unambiguous | site | citation | resolves to | |---|---|---| | `scripts/check-changeset-no-major.mjs` | `date-range-presets.ts:101` | `packages/spec/src/data/date-range-presets.ts` | | `scripts/cross-package-test-inputs.mjs` | `:102`, `:92`, `:484-485` (continuations) | `packages/spec/src/shared/retired-key-migrate-sentence.test.ts` | | `scripts/ts-parse.mjs` | `tenant-audit-census.mjs:459`, `:723` | `scripts/tenant-audit-census.mjs` | - **`date-range-presets.ts`** — one tracked file of that basename (`date-range-presets.test.ts` is a different basename), and line 101 is inside the TSDoc block the surrounding prose describes. - **`retired-key-migrate-sentence.test.ts`** — one tracked file, and all three numbers still land on what the prose says they do: `PUBLISHED_SKILLS_ROOT` on line 102, the "plus every `.md` file under `skills/`" sentence on line 92, and the `skills:`-prefixed `files.push` on lines 484-485. - **`tenant-audit-census.mjs`** — one tracked file of that basename (`check-tenant-audit-census.mjs` is a different basename). It carries both cited constructs, the `.replace(/\s+/g, ' ')` collapse and the synthetic `type CensusReceiver = ...` alias; both have drifted off the cited lines, which is precisely why those lines are kept as dated data rather than repointed. ## The gate's own dated record is corrected in the same edit Both prior rounds did this, and it had gone stale in a way that matters: the header claimed a residual of **8** and named holders (PRs objectstack-ai#16215, objectstack-ai#16202) that have since merged, while the live command printed **18**. That regrowth — 8 back up to 18 between rounds, because gate-header prose written in the interval added citations faster than round two removed them — is the most load-bearing reading here, and the header now says so. **This is not a burn-down; it is a quantity that grows.** ## Acceptance, measured on this branch ``` node scripts/check-scripts-symbol-anchors.mjs --list-unresolvable EXIT 0 18 -> 12 node scripts/check-scripts-symbol-anchors.mjs --self-test EXIT 0 node scripts/check-scripts-symbol-anchors.mjs EXIT 0 ``` Row-by-row `diff` of the before and after listings: the **only** lines removed are the six above, plus the summary line changing from `18 ... 10 bare-filename, 5 continuation, 3 directory-qualified` to `12 ... 8 bare-filename, 1 continuation, 3 directory-qualified`. The other twelve rows are byte-identical and still listed. ## What is deliberately NOT here - **`judgeUntrackedLineAnchors` stays `false`.** Whether it should flip is the open question this card exists to put in front of triage, and it is now a sharper question than a counting one — see below. - **The remaining twelve rows are untouched**, every one of them on another seat's surface: `scripts/pm/check-governed-merges.mjs`, `scripts/pm/check-skill-line-ratchet.mjs`, `scripts/pm/check-prior-rulings.mjs`, `scripts/pm/check-half-states.mjs`, `scripts/pm/check-widening-tells.mjs` (governed-surface and `pm` patrol instruments) and `scripts/check-spec-docblock-symbol-anchors.mjs` (SUBJECT is `packages/spec` docblocks). No card is filed for them here. ## Open question for triage A flag that flips at residual zero flips into a gate that reds on the **next** author's first bare citation — and the 8 -> 18 regrowth measured above says there will be a next author, soon. So what the flip needs is a **remedy for an unbindable abbreviation** (a bare `engine.ts` a resolver cannot bind to one of several files of that name), not a smaller number. Reaching zero and flipping the same day would hand the repo the permanently-red gate its own header says it retired. Part of objectstack-ai#15809 --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ 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.