Skip to content

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

Merged
os-zhuang merged 9 commits into
mainfrom
claude/issue-16130-open-org-scoping
Sep 7, 2026
Merged

os-zhuang merged 9 commits into
mainfrom
claude/issue-16130-open-org-scoping

Conversation

@hotlong

@hotlong hotlong commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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-scoping registrar, 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 in plugin-auth, and the requiresService: 'org-scoping' Setup gates. The registrar was the one missing piece, and without it an open install that set OS_TENANCY_POSTURE=isolated could not enforce it: its only route forward was OS_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's ORGANIZATIONS_RUNTIME_PKG already carries, so the loader needs zero changes. Two readings behind that:

  • npm. npm view @objectstack/organizations returns E404 — never published, so the name is free. (For contrast, the historical @objectstack/plugin-org-scoping is on npm at 12.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.)
  • Live config carrying the old name. 24 files mention 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 inside scripts/check-published-readme-exports.mjs whose 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/organizations

Raised in review as possibly belonging at top level (packages/organizations/), mirroring cloud's layout. Kept under packages/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:

  • One CI step would stop covering it. .github/workflows/ci.yml"Verify capability packages ship a runtime entry (no dts-only / half-built)" — enumerates exactly packages/triggers/* packages/services/* packages/drivers/* packages/plugins/*, and it is the only such enumeration in that file; top-level packages/* 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-built dist means the capability silently fails to load. That is this package exactly — serve reaches it through importFromHost, 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 (build script, main: dist/index.js).
  • The record still points here. AGENTS.md's Monorepo Layout lists plugins/ # Official plugins & drivers, and its Context Routing table routes packages/plugins/**Plugin Developer · Implements spec contracts. A repo-wide search finds no recorded retirement of that directory.
  • A maintainer-approved ruling names it as a live family five days ago. [finding] No plugin package has a tsc program that compiles its tests — plugin-approvals alone hides 345 type errors, and 0 of 14 are in the test-typecheck ledger #14062's director ruling of 2026-09-01 (maintainer verbatim: 「同意」) onboarded "the packages/plugins/** family" into the check:test-typecheck instrument family. This package's tsconfig.test.json follows that family's shape and says so.
  • The de facto convention agrees. Measured by first-commit date of each package manifest: the most recent new package anywhere in this repo before this one went into packages/plugins/ (plugin-pinyin-search, 2026-07-16) — later than the last new top-level package (sdui-parser, 2026-06-29).
  • It is a Plugin. It implements the Plugin interface and is mounted with kernel.use(). Three of the fifteen entries under packages/plugins/ already carry no plugin- prefix (knowledge-memory, knowledge-ragflow, embedder-openai), so organizations@objectstack/organizations matches the directory-basename convention there.
  • The symmetry argument is satisfied by the name, not the path. The maintainer's instruction was 「可以直接用cloud 下的 organizations 包名吗」 — 包名, the package name, which this PR takes verbatim. That is also the only thing the subclass coupling reads. Cloud's tree is flat throughout (packages/organizations, packages/service-cloud, packages/security-enterprise), so mirroring its layout would mean adopting a convention this repo does not use.

⚠️ If the retirement of 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:

EE mount path how it resolves class mounted
objectstack serve importFromHost('@objectstack/organizations')createRequire(join(hostRoot,'package.json')), gated on the served app's own declaration (#4719) cloud's gated subclass
artifact-kernel-factory.ts a bare import('@objectstack/organizations') from inside packages/objectos-runtime, which declares it cloud's gated subclass
service-cloud's control-plane preset lazyPlugin('com.objectstack.organizations', ...) reaching control-plane-organizations.js, in a package that declares it cloud's gated subclass

All 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 to link: 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 own name, security-enterprise's objectstack.preflightAlsoReads array entry, and a paths alias in that package's tsconfig.test.json. (Independently re-verified in review, which reached the same six and the same exceptions.) pnpm's workspace: 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 at workspace:* — never a semver range.

What could break it, and is therefore forbidden here: a framework package taking @objectstack/organizations as its own dependency. Cloud consumes the framework by link:, 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.ts holds 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

  1. multi-org-license-gate.ts stays in cloud. Not moved, not reimplemented, and no hook left for it. Its 306 lines and 12 exported symbols are untouched.
  2. The multi-node gate carrier stays. MULTI_NODE_GATE_CARRIER_PACKAGES is unedited and the open package acquires neither the obligation nor the security-enterprise import that discharges it. ⚠️ One diagnostic changes for an open install and is recorded in that file's header: the carrier import used to fail (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.
  3. No licence check of any kind. The constructor's 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, and new 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

  • ~128-130 (ADR-0105 D12's "belongs here, in the commercial runtime"): reworded to record that the open package entitles both walled postures by construction. The declaration ['group','isolated'] moves as-is.
  • ~134-136 ("gating it behind a licence flag … becomes a one-line edit HERE"): reworded, as required. It is replaced by a ⛔ that says the opposite in the same place — that this file and this package carry no licence check, offer no hook for one, and take no entitlement callback.

The service name

org-scoping is unchanged, and so is the plugin id com.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

  • ADR-0132 (new) — the multi-organization runtime is open core; only the entitlement stays commercial. Carries a Provenance section naming cloud ADR-0081 and its date, the D3 resolution mechanism, and the boundaries the code comments cite by number.
  • ADR-0105 D12 — amended in place: the code-vs-activation split stands, the "closed-source" sentences read as history, and D12's argument for putting supportedPostures in the commercial runtime does not carry to the open package. Its status line records the amendment.
  • The cloud record's one-line pointer back to ADR-0132 is owed by the cloud PR, not this one.

#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:

file classified changed to why
claim-orphan-org-rows.test.ts, claim-org-seed-ownership.test.ts move as-is move with changes check:engine-double-contract — the fakes' update() now opens with assertEngineUpdateDispatch
org-creation-no-cross-org-copy.test.ts grant-strip only + the same assertion same gate
walled-default-org-self-registrant.pin.test.ts grant-strip only + the WHERE double REFUSES a combinator check:where-matcher — it read $and/$or as a field name, so a filter it cannot express answered "no rows", which every negative assertion in that file would have read as evidence
organizations-plugin.ts 480 as-is 3 more lines changed check:slot-lookup — the objectql / metadata lookups are typed structurally instead of erased to any; that ratchet's baseline never grows
organizations-plugin.ts, membership-policy-gate.ts prose lines listed + 4 runtime strings check:doc-authoring — tracker ids leave a rendered boot refusal and a log line, and move to adjacent comments
every ADR-0081 citation not flagged cloud ADR-0081 a bare ADR-0081 resolves against this repo's ADR-0081 (the React page tier) — the exact collision ADR-0105's own citation note documents

The three Apache-2.0 files move byte-identical — verified by git hash-object against cloud 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 the unavailableloaded-without-gate diagnostic change, with a ⛔ against "repairing" it.

Plus the gate-driven bookkeeping a new package owes: the isSystem census row for the organization_id auto-stamp's elevation short-circuit (row 62, with the page's counts moved), the boot-refusal row for WALLED_MEMBERSHIP_POLICY_UNDECLARED, the check:cross-package-test-inputs radius plus its turbo.json task for the D3 pin, llms.txt 68 → 69, and the changeset fixed group.

Not edited, deliberately: packages/verify/src/harness.ts's "requires the enterprise @objectstack/organizations package" message and packages/cli's serve / doctor resolution. 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=isolated and OS_ALLOW_DEGRADED_TENANCY unset, booting with the wall ACTIVE and enforcing the matrix, is not measured here and is not claimed. objectstack serve still 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-written posture: 'isolated' stub", not "build a walled showcase app".

Verification

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh could not take the shared verify lock on this host: no usable flock. The shared verify lock is declared Linux-only (flock is 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 at 11ca403f6 — the final commit, after the SECOND origin/main merge — 125 green. (Prior heads for comparison: 6d7d0e4c9 124 green, 494cb77f7 124 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:

gate exit verdict
check:dual-build-cjs-loads 3 PREREQUISITE NOT MET — its own words. 11 packages have no dist/ in this tree.
check:i18n 3 PREREQUISITE NOT MET — needs the CLI's extract-config build closure.
check:type-check-debt 3 PREREQUISITE NOT MET — nothing measured.
@objectstack/spec check:skill-examples 1 PREREQUISITEpackages/client-react/dist holds no .d.ts; the gate refuses rather than compute a false green.
check:merge-driver 1 PRE-EXISTING, proven. check-regen-pending.mjs --self-test fails identically in a clean worktree at origin/main 9f3989771 (case: "a gate whose RUNNER is not installed"). Baseline captured, worktree removed.
check:bash32-floor 1 HOST. Its probes need a bash that cannot parse coproc / ;;& / [[ -v ]]; this host's /bin/bash is 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-closure was 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/main was merged into this branch twice (the PR read mergeable_state: dirty on creation, and a conflicting PR gets no CI at all). The merge brought packages/spec, so per AGENTS.md §10 it was rebuilt and check:generated re-run — All 15 generated artifacts are up to date. The merge driver also deferred content/docs/permissions/system-context.mdx, which is a merge=os-regen artifact git ran no text merge on; .githooks/pre-push refused the push until it was regenerated from the merged tree, and that regeneration is its own commit. It rewrote anchor lines only — 18 file.ts:line citations the incoming commits moved — with row 62, the section extents and every declared count unchanged. The whole gate set, the build closure, typecheck and the 76 tests were then re-run on the merged head.

Second round, same two traps, same handling. main moved again and the PR went dirty; 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. ⭐ main has never touched packages/plugins/organizations, so the classification's premise did not move under this card; the three files both sides touched were system-context.mdx, dispatcher-error-vocabulary.ts and engine-double-contract.pinned.json, and my content survived in all three (row 62, the boot-refusal row, the three pinned fakes). packages/spec came in again, so it was rebuilt and check:generated re-run — All 15 generated artifacts are up to date. The merge=os-regen deferral fired again and .githooks/pre-push refused 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 from 11ca403f6; 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 buildcheck-dts-emitted: 1/1 declared declaration file(s) present.
  • pnpm --filter @objectstack/organizations run typechecktsc --noEmit clean and check:test-typecheck: OK — 0 file(s) / 0 error(s). ⚠️ Those are two different programs: the build config excludes **/*.test.ts, so "typecheck clean" alone would say nothing about the test layer. tsc --listFiles -p tsconfig.test.json puts all 14 files (7 source + 7 test) in the program — measured, not assumed. Zero residue, so the package ships no test-typecheck-debt.json and no gen: script, matching the ten zero-residue plugin packages rather than the four that carry ledgers.
  • pnpm --filter @objectstack/organizations test7 files, 76 tests, all passing.
  • The five CLI pins the classification flagged as at-risk under the shared name — serve-cluster-host-resolution, serve-organizations-message-spelling, doctor-organizations-message-spelling, serve-organizations-host-resolution, test/serve-capability-vocabulary67 tests, all passing, on a built closure. packages/spec's platform-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 / PluginContext and resetPlatformAdminEmailMemo from @objectstack/core; isDefaultOrganizationBootstrapTrigger, ensureDefaultOrganization, EnsureDefaultOrganizationResult, isMembershipPolicy, MEMBERSHIP_POLICIES from @objectstack/plugin-auth (all reached through its export * barrel); resolveTenancyPosture from @objectstack/types; ServiceObject from @objectstack/spec/data. Nothing failed to resolve, and nothing needed a shim.

And the verdicts are about the source, not the last build. tsconfig.json carries paths for all five workspace dependencies and vitest.config.ts carries anchored-regex aliases for the three the tests reach as values — so check:type-source-resolution and check:test-source-alias both pass with this package resolving source, rather than the package being registered in either gate's shrink-only unaliased ledger.

hotlong and others added 5 commits September 6, 2026 13:03
…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>
hotlong and others added 2 commits September 6, 2026 14:11
#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>
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/organizations, @objectstack/plugin-security, @objectstack/dogfood, @objectstack/runtime, @objectstack/service-cluster, @objectstack/spec, touching 53 documentable anchor(s). ⚠️ 9 changed file(s) yielded no anchor (packages/plugins/organizations/LICENSE, packages/plugins/organizations/README.md, packages/plugins/organizations/package.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6c546ab9d0b4842bf363caac2a9ff852277680b5.

2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/plugins/organizations/LICENSE, packages/plugins/organizations/README.md, packages/plugins/organizations/package.json, …) — pages documenting those are invisible to this run
  • 2 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 29 pages), owner_id (literal, 31 pages)
  • 19 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Which tree this was computed on

This run read content/docs from b76c34ce079ca8042450276374b2eb07b5b3254d — the merge of head 11ca403f6b12361daa1082891ac1cbe97113b654 into base 6c546ab9d0b4842bf363caac2a9ff852277680b5, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6c546ab9d0b4842bf363caac2a9ff852277680b5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 6, 2026
baozhoutao pushed a commit that referenced this pull request Sep 6, 2026
… 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
hotlong and others added 2 commits September 6, 2026 21:46
#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).
baozhoutao pushed a commit that referenced this pull request Sep 6, 2026
…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
os-project-manager pushed a commit that referenced this pull request Sep 8, 2026
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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…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>
os-zhuang pushed a commit that referenced this pull request Sep 10, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… 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>
os-justin pushed a commit that referenced this pull request Sep 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants