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 unavailable → loaded-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 PREREQUISITE — packages/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 build — check-dts-emitted: 1/1 declared declaration file(s) present.
  • pnpm --filter @objectstack/organizations run typecheck — tsc --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 test — 7 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-vocabulary — 67 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 6c546ab9d0b4842bf363caac2a9ff852277680b5 → packageMentionDocs.

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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… not cloud-private (objectstack-ai#18759)

Fixes objectstack-ai#18542

Clause-②: no

objectstack-ai#17379's census (comment 5704455410) left one five-sentence family that
is neither "now
false" nor "still true". Five measurement narratives in
`packages/core/src/security` and
`packages/rest/src` call the thing cloud's own app mounted "the real
cloud-private
`@objectstack/organizations`" (or "`Organizations` plugin"). Under
ADR-0132 D3 — one name,
two packages — that adjective never says which of the two it means: it
is true of the
licence-gated subclass a commercial deployment resolves the name to, and
false of the
package name, which in this workspace resolves to an Apache-2.0 member
of it.

Triage kept the five on one card so the adjective is chosen once. This
is that choice.

## The adjective: `licence-gated`

It is the tree's own established spelling for cloud's variant, and it is
the property that
DISCRIMINATES: the open-core build in this repository is not gated, the
subclass cloud
resolves the same name to is. Measured on this head: 16 live occurrences
of `licence-gated`
and zero of `license-gated` —
`packages/plugins/plugin-security/README.md`,
`packages/plugins/plugin-auth/src/tenancy-service.ts`,
`packages/services/service-cluster/src/multi-node-gate-mount.ts`,
`packages/spec/src/kernel/platform-capabilities.ts` (a runtime string),
`packages/cli/src/commands/serve.ts` and four CHANGELOGs, all reading "a
private,
licence-gated subclass".

Where the old adjective sat on the bare package NAME (the three
`packages/rest` sites), the
referent noun `subclass` is added with it, so the gate attaches to the
variant and never to
the Apache-2.0 package — otherwise the repair would trade one false
reading ("this package
is private") for another ("this package needs a licence"). The two
`packages/core` sites
already had a noun denoting the mounted instance ("`Organizations`
plugin"), so they take
the adjective alone.

### Why the alternatives lose

* **`enterprise`** — it does not discriminate.
`packages/spec/src/security/tenancy-posture.ts`
uses "the enterprise `@objectstack/organizations` runtime" for the
package required to
ACTIVATE a multi-org posture, which since ADR-0132 is the open-core one.
Picking it would
relabel the ambiguity instead of removing it, and add five lines to the
107-line population
  the census already flagged as true-but-for-a-stale-reason.
* **Keeping `cloud-private`** (the census's option C) — true of what
cloud mounted, but it
states the package is private: the exact claim ADR-0132 falsified and
that a six-PR campaign
(objectstack-ai#16215, objectstack-ai#16539, objectstack-ai#16552, objectstack-ai#17371, objectstack-ai#17369, objectstack-ai#17910) removed everywhere
else. Five surviving
  instances re-seed that population.
* **Dropping the adjective entirely** — right for an operator-facing
message, and already
ruled so for one:
`packages/cli/src/commands/serve-organizations-message-spelling.test.ts`
records "a commercial deployment resolves the same package name to a
private licence-gated
subclass, so any adjective this message picks is wrong for one of the
two installs reading
it". That ruling is about a message whose READER is unknown. These five
are measurement
narratives whose subject is named (`apps/objectos-ee`, cloud#1982): two
of them contrast
"objectstack#15163 measured it on the framework" with "cloud#1982
reproduced it with the
real X", and with no adjective that contrast collapses into the same
artifact twice — which
  is exactly the distinction ADR-0132 D3 exists to keep readable.
* **`entitled`** — already carries a different job: `tenancy-posture.ts`
uses it for
ACTIVATION ("Open code, entitled activation") and doctor's roster pin
for "the ENTITLED
  variant" of a roster row.

## The five sites

Taken from objectstack-ai#17379's census, comment 5704455410 — not from the card body,
and no fresh sweep
was run. Line numbers are readings at this head, not identifiers.

| # | file | the adjective as it stood |
|--:|:--|:--|
| 1 | `packages/core/src/security/resolve-authz-context.ts:483` | "the
real cloud-private `Organizations` plugin" |
| 2 | `packages/core/src/security/resolve-authz-context.test.ts:1604` |
same sentence, the test twin |
| 3 | `packages/rest/src/rest-api-plugin.ts:300` | "the real,
cloud-private `@objectstack/organizations`" |
| 4 |
`packages/rest/src/single-kernel-isolated-api-key-matrix.test.ts:21` |
"the REAL cloud-private `@objectstack/organizations`" |
| 5 |
`packages/rest/src/single-kernel-isolated-session-org-claim-matrix.test.ts:11`
| same shape |

File surface as declared in the claim: `packages/core/src/security/` and
`packages/rest/src/`.
The diff is those 5 files, both halves of the cross-domain surface,
nothing outside it. No
other session's edit was found in either file.

## Prose, not pins

The three machine assertions the card names are NOT in this diff.
`serve-capability-vocabulary.test.ts:92` and
`doctor-organizations-message-spelling.test.ts:197`
(`edition === 'enterprise'`) and
`harness.host-resolution.test.ts:288,319` (operator strings in
a regex) are class-3 `enterprise` sites in `packages/cli` and
`packages/verify`; this card is
the class-1-ambiguous `cloud-private` family in `packages/core` +
`packages/rest`.

`doctor-organizations-message-spelling.test.ts:188-194` was read before
deciding, as the card
requires. It argues that the roster row `edition: 'enterprise'` records
the ENTITLED variant
and is "deliberately not relaxed to match the prose reword". Nothing
here disturbs it, and its
reasoning is the one this diff applies — one name, two packages, and the
roster names the
entitled variant. No argument to move it is made.

All five edited sites are comment prose. Nothing asserts their text: the
three tests that read
these two source files as TEXT (`authz-store-unavailable.test.ts`,
`execctx-authz-input-seam-reachability.test.ts`,
`objectql-slot-consumer-census.test.ts`)
assert code shapes only — imports, catch patterns, and field reads whose
scanner excludes
comment lines by construction.

## Evidence

* `pnpm --filter '@objectstack/rest...' --filter '@objectstack/core...'
build` — exit 0 (both dependency closures).
* `pnpm --filter @objectstack/core test` — 51 files / 1316 tests passed.
* `pnpm --filter @objectstack/rest test` — 194 files / 3236 passed, 1
skipped.
* `pnpm --filter @objectstack/core typecheck && pnpm --filter
@objectstack/rest typecheck` — exit 0, each including its test layer.
* `pnpm lint` (repo-wide `eslint . --no-inline-config`) — exit 0 in 86s,
so no narrowing was needed and none is declared.
* Gate families derived by `scripts/pm/dispatch-gates.mjs --commands` at
this head: 53 derived, 51 run green, 2 NOT MEASURED —
`check:dual-build-cjs-loads` and `check:type-check-debt` both exit 3,
`PREREQUISITE NOT MET`: they read a whole-repo `dist/` this worktree
does not have, and CI builds before running them. Reconciled with
`--ran`: "53 derived famil(ies) accounted for — 51 run, 2 NOT-MEASURED".

### `skip-changeset`, measured rather than assumed

Both packages publish, so the label was measured. After the build,
`grep -rn "licence-gated" packages/core/dist packages/rest/dist` returns
**zero** hits (exit 1,
captured before any pipe). Positive control on the same channel: comment
prose DOES reach
published output — `cloud#1982` appears at
`packages/rest/dist/index.d.ts:421`, out of a JSDoc
on an exported declaration in `rest-server.ts` — and the exported
symbols (`resolveAuthzContext`,
`RestApiPlugin`) hit in `dist` as expected. The channel exists, the
control fires on it, and
none of the five edited comments is on it: nothing published moves.
`Clause-②: no` holds for
the same reason — no accept set, no authorable key and no export moves.

## Acceptance notes

* **noted, not filed** —
`packages/rest/src/single-kernel-isolated-api-key-matrix.test.ts`
calls the same mounted runtime "the enterprise plugin" two lines below
the edited sentence.
That is a class-3 line (true under ADR-0132 D3, stale reason), and
objectstack-ai#17379's class-3
population is out of this card's scope by the triage ruling, so it is
left exactly as it was.
Successor: none queued — the census's class-3 lane table lists no
`packages/rest` lane.
* **noted, not filed** — two shorter siblings of the same family already
carry no adjective at
all: `packages/rest/src/rest-server.ts` (the published JSDoc above) and
`packages/core/src/security/api-key.ts`. Both are correct as written —
they name the package,
not a variant — and are unchanged. Recorded so the next reader does not
read that absence as
  drift from this ruling.
* The census's own out-of-scope finding about ADR-0132's `Status:
Proposed` line is already
  reported on objectstack-ai#17379 and is not refiled here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… carries its own landing PR and commit (objectstack-ai#18711)

Fixes objectstack-ai#18532

Clause-②: no

## The defect

ADR-0132 and ADR-0133 both landed on `main` on 2026-09-07, and both
still carried a `**Status**` line reading `Proposed (2026-09-06) —
awaiting the maintainer's hand-merge, which is (itself) the acceptance
act for a governed surface (Prime Directive objectstack-ai#14)`. Read by the record's
own rule, the acceptance act it names had occurred; only the line was
never updated — and the Status field is the one machine-readable
statement of a governance record's force, so anyone judging "can I cite
this as settled law?" from it got the wrong answer while the body below
it told them nothing here is settled. The failure direction is silent:
both files are on `main`, every gate is green, and nothing reds. The
positive controls are in the same convention and one number apart —
ADR-0130 and ADR-0131 each moved their line to `Accepted` on landing and
pointed back at the PR and commit that landed them. This PR does the
same for 0132 and 0133, with each record's own landing facts, and
changes nothing else.

## The four Status lines, before and after

Measured on this branch's base, `origin/main` at `f8eaf6704` (the card's
own readings were taken earlier, on `1e496f979`). The two controls are
quoted to show the target form; ⛔ neither is touched by this PR.

**ADR-0130 :3 — positive control, unchanged**

```text
**Status**: Accepted (2026-09-01) — accepted by the merge that landed it on `main` ([objectstack-ai#14151](objectstack-ai#14151), commit `682d03ba7`), which is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14).
```

**ADR-0131 :3 — positive control, unchanged**

```text
**Status**: Accepted (2026-09-04) — accepted by the merge that landed it on `main` ([objectstack-ai#14976](objectstack-ai#14976), commit `0ed271574`), which is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14). Execution is paused under objectstack-ai#15193 and the maintainer's standing instruction; that pause is carried there, not by this status line.
```

**ADR-0132 :3 — BEFORE (three physical lines, wrapped under a list
dash)**

```text
- **Status**: Proposed (2026-09-06) — awaiting the maintainer's hand-merge, which is itself the
  acceptance act for a governed surface (Prime Directive objectstack-ai#14). ⛔ Nothing below is settled until
  this record merges.
```

**ADR-0132 :3 — AFTER**

```text
- **Status**: Accepted (2026-09-07) — accepted by the merge that landed it on `main`
  ([objectstack-ai#16215](objectstack-ai#16215), commit `c677cda816`), which
  is itself the acceptance act for a governed surface (Prime Directive objectstack-ai#14).
```

**ADR-0133 :3 — BEFORE (one unwrapped physical line)**

```text
- **Status**: Proposed (2026-09-06) — awaiting the maintainer's hand-merge, which is the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ⛔ Nothing in this file is a new decision; see [Provenance](#provenance--read-this-before-citing-this-file).
```

**ADR-0133 :3 — AFTER**

```text
- **Status**: Accepted (2026-09-07) — accepted by the merge that landed it on `main` ([objectstack-ai#16267](objectstack-ai#16267), commit `13c08356ab`), which is the acceptance act for a governed surface (Prime Directive objectstack-ai#14). ⛔ Nothing in this file is a new decision; see [Provenance](#provenance--read-this-before-citing-this-file).
```

Three shape decisions, each read off the controls rather than invented:

1. **Each file keeps its own local form.** 0132 :3 is a wrapped list
item under a dash and stays one (three physical lines, wrapped at the
same column its sibling bullets use); 0133 :3 is a single unwrapped list
line and stays one. ⛔ Neither file is reformatted toward the other, and
⛔ neither is reformatted toward 0130 / 0131, which carry no list dash at
all.
2. **0132 keeps `which is itself the acceptance act`; 0133 keeps `which
is the acceptance act`.** That word differs between the two files today,
and the controls do not settle it — 0130 and 0131 both say `itself`.
Each file keeps the wording it already had.
3. **The trailing sentence is treated the way the controls treated
theirs.** Neither 0130 :3 nor 0131 :3 carries a "nothing is settled
until this merges" clause, so 0132's is dropped rather than turned past
tense. 0133's trailing ⛔ sentence is a *different* sentence — it says
this file records no new decision and points at Provenance, which is as
true after the merge as before — so it is preserved byte-for-byte.

## Landing facts, and how each was verified

| record | landing PR | squash commit | merged | merged by |
|:--|:--|:--|:--|:--|
| ADR-0132 |
[objectstack-ai#16215](objectstack-ai#16215) |
`c677cda816f70c3e46fa1e3ad451a6d43f50dd07` | 2026-09-07T06:15:26Z
(commit date 2026-09-07T05:30:13Z) | os-zhuang |
| ADR-0133 |
[objectstack-ai#16267](objectstack-ai#16267) |
`13c08356abc61f13acd5db0932f99d482c134588` | 2026-09-07T07:37:31Z
(commit date 2026-09-07T07:19:25Z) | os-zhuang |

Verification, per record, all four legs:

- **Single-parent squash.** `git log -1 --format='%P'` returns exactly
one parent for each (`acabd24879…` for 0132, `8976ea1120…` for 0133).
- **The subject names the PR.** `…(ADR-0132) (objectstack-ai#16215)` and `…mirroring
cloud ADR-0081 D1/D3/D4 with Provenance (objectstack-ai#16267)`.
- **The commit introduced the file.** `git show --stat SHA --
docs/adr/0132-…md` → `1 file changed, 225 insertions(+)`; the same for
0133 → `273 insertions(+)`. Both are file creations, so each sha is the
landing of its own record and not a later edit.
- **The sha is an ancestor of `main`** — `git merge-base --is-ancestor
SHA origin/main`, exit 0 for both.

⚠️ **That ancestry reading needed a control leg, and the first reading
was wrong.** This container's checkout is shallow. On the shallow window
as fetched (boundary `5de93728a`, 2026-09-09T21:18Z) `--is-ancestor`
answered **exit 1 — "not an ancestor"** for both shas, which are dated
2026-09-07 and therefore *outside* the window: a confident false
negative, because the missing objects withhold the ancestry path without
fabricating one. After `git fetch --shallow-since=2026-08-25 origin
main` (694 → 2867 commits) both test legs answer exit 0, and two control
commits at least as deep as the tested pair — `682d03ba7` (ADR-0130's
own landing, 2026-09-01) and `0ed271574` (ADR-0131's, 2026-09-04) — also
answer exit 0, so the negative is retired rather than merely re-rolled.
All four exit codes are recorded.

The `merged_at` / `merged_by` values come from one REST read each of
`/repos/objectstack-ai/objectstack/pulls/16215` and `/pulls/16267`; both
report `merged: true`, `base.ref: main`, and a `merge_commit_sha`
byte-equal to the sha in the table. This also answers the card's
explicitly unmeasured worry about ADR-0133 — whether its landing
constituted the hand-merge its own line named. It did: `merged_by` is
os-zhuang, the authorized approver. So both records are "the text went
stale", ⛔ not "an unaccepted governance record is being cited as law",
and one line each is the whole repair.

**The date in `Accepted (…)` is the landing date**, as 0130 and 0131 do
— 2026-09-07 for both, which is the date on both the squash commit and
the merge event.

## Consumers — a reading, no edit

`git grep -n
'ADR-0132\|ADR-0133\|0132-multi-organization\|0133-org-management'` at
`a937609f6c`: **110 hits across 51 files**. ⛔ None is edited by this PR.
By area:

| area | files |
|:--|--:|
| `packages/plugins/**` (organizations, plugin-auth, plugin-dev,
plugin-security) | 18 |
| `packages/qa/dogfood/**` | 7 |
| `packages/cli/**` | 5 |
| `packages/verify/**` | 4 |
| `packages/spec/**` | 3 |
| `docs/adr/**` (0105, 0132, 0133 themselves) | 3 |
| `skills/objectstack-data/**` | 2 |
| `packages/services/service-cluster/**` | 2 |
| `.changeset/**` | 3 |
| `content/docs`, `docs/qa`, `packages/types`, `scripts/` | 4 |

The card names the census on
[objectstack-ai#17379](objectstack-ai#17379) as
one reader built on ADR-0132; the grep shows the reliance is much wider
than one card — `packages/plugins/organizations` alone cites it in its
plugin, its README and nine tests, including pin tests that name the
record as the authority for the wall they hold.

## Verification

Derived — ⛔ no hand-fed path list — with `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree; the tool took the change
set itself (three-dot vs the merge base) and printed the two ADR paths.
**18 families derived, 18 run, all exit 0**, at head `a937609f6c`:

```text
node scripts/check-adr-links.mjs                                      :: exit 0
node scripts/check-adr-links.mjs --self-test                          :: exit 0
node scripts/check-adr-symbol-anchors.mjs                             :: exit 0
node scripts/check-adr-symbol-anchors.mjs --self-test                 :: exit 0
node scripts/check-ci-filter-parity.mjs                               :: exit 0
node scripts/check-closing-keyword-parity.mjs                         :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test             :: exit 0
node scripts/check-comment-mask-corpus.mjs                            :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions     :: exit 0
pnpm check:adr-anchors                                                :: exit 0
pnpm check:cross-package-test-inputs                                  :: exit 0
pnpm check:doc-authoring                                              :: exit 0
pnpm check:driver-memory-census                                       :: exit 0
pnpm check:nul-bytes                                                  :: exit 0
pnpm check:pm-governed-merges                                         :: exit 0
pnpm check:pm-prior-rulings                                           :: exit 0
pnpm check:refd-timer-probe                                           :: exit 0
pnpm check:watch-hint-literal                                         :: exit 0
```

Reconciled: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran RANFILE` → exit 0, `18 derived
famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all
18 recorded an exit code and none of them is 3)`.

Beyond the derived set:

```text
node scripts/pm/check-governed-merges.mjs --test docs/adr/0132-….md docs/adr/0133-….md  :: exit 3
pnpm lint    (repo-wide, `eslint . --no-inline-config`, ⛔ not narrowed)                  :: exit 0
```

**Exit 3 on the governed predicate is the verdict, ⛔ not a failure**:
`GOVERNED — a human merge is the review record for this PR (objectstack-ai#9495
regime)`, `docs/adr/** ×2`. This PR is draft and stays draft; ⛔ no seat
flips it ready, enqueues it or arms auto-merge.

Exit codes were captured with redirect-then-`$?`, ⛔ never across a pipe.

⚠️ **One gate refused before it measured anything, and was re-run rather
than recorded as a pass.** On the first sweep `pnpm --filter
@objectstack/lint run check:doc-formula-expressions` exited **3** —
`PREREQUISITE NOT MET — the workspace package @objectstack/formula is
not built`, the script's own wording being `Nothing was measured: this
gate exited before running a single check`. That is NOT MEASURED,
neither green nor red. After `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` (through
`scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0`) it exits 0.
Only the exit-0 run is reported above.

⚠️ **What this list does not cover** — the derivation says so itself,
and it is repeated here so the green is not read wider than it is: 53
artifact-roster families (their `silent` verdict is a fact about a
tracked list, ⛔ not a clearance), 11 families whose declared population
is too wide to place, 2 families whose argv takes a value only CI
supplies, 1 CI job scheduled by these paths whose 5 steps have no local
invocation, and 14 families that apply only once a changeset exists — ⛔
not applicable here, see below. Those are CI's to answer on this PR.

**Tree state.** `origin/main` moved three commits (`72dd95fa5a`) while
this ran, and the derivation flagged the tree stale for one file it
reads (`scripts/pm/post-stamped.mjs`). `origin/main` was merged in (5
files, all disjoint from this diff, no `os-regen` deferral),
`@objectstack/formula` and `@objectstack/lint` were force-rebuilt
because the merge moved `packages/formula/src/validate.ts`, and the
whole sweep above was re-derived and re-run at the merged head
`a937609f6c`. The re-derivation produced a byte-identical 18-command
list and no staleness warning. Every number quoted here is from that
head.

## Changeset

`skip-changeset`. `docs/adr/**` is on the no-publish fast track: no
package's `files[]` ships an ADR, and the diff is four lines of prose in
two governance records. `Clause-②: no` — ⛔ nothing an author can write
moves.

## Diff

```text
docs/adr/0132-multi-organization-runtime-is-open-core.md | 6 +++---
docs/adr/0133-org-management-open-basics.md              | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
```

Two files, one logical Status line each. 0132 counts as three physical
lines because its line is a wrapped bullet, before and after.

## Acceptance notes

⛔ None of the following is filed as a card; each is recorded here for
the reviewing seat.

1. **ADR-0133 :85–:97 carries a pre-merge frame that survived its own
merge, and is deliberately ⛔ not touched.** The block is headed `⚠️ **A
note for the hand-merge, stated rather than decided.**` and ends `**If
the maintainer prefers one combined record, that is still a merge-time
call** — the two files are disjoint, so folding them is a move, not a
rewrite.` It addresses a reader who has not merged the file yet. Quoted
here because the dispatch required any such sentence be quoted before
touching it: ⛔ it is not touched, because it asserts nothing about this
record's acceptance state — it offers a structural option about folding
0133 into 0132 — and scope excludes every other line in both files. The
same block already reads `That obstacle is gone — the record has
landed`, about ADR-0132.
2. **Two premises in the dispatch text did not survive measurement on
this tree; neither changed the deliverable.** (a) The dispatch said the
`acceptance act` phrase occurs in the *bodies* of 0132 (twice) and 0133
(once), to be left untouched. Measured: `grep -c` gives **1** in each
file, and in both cases that single occurrence **is** the Status line
this PR edits — there is no body occurrence in either file to leave
alone. The 2× / 1× counts belong to 0130 / 0131, which is what the card
itself says. (b) The dispatch described 0133's Status line as carrying
the `Nothing … is settled until this record merges` sentence. It does
not; its trailing ⛔ sentence is the unrelated `Nothing in this file is a
new decision` one, which is preserved. The card's own table quoted 0133
correctly.
3. **Short-sha spelling.** 0130 and 0131 use 9 hex digits, which is what
`git rev-parse --short` emitted at their time. On this tree, after
deepening, the default abbreviation is 10 — and 10 is also the spelling
the card body and the claim comment both recorded (`c677cda816`,
`13c08356ab`). This PR uses 10 so that the file, the card and the claim
agree; the full 40-hex shas are in the landing table above. The sentence
*form* is the controls' form exactly.
4. **⛔ ADR-0134 and the other 17 `Proposed` records are untouched**, per
the card. 0134's `Proposed` mirrors cloud ADR-0071's own state and is
self-explanatory; the other 17 were never measured by the card and a
batch edit would treat an unmeasured population as a measured one.

## 维护者速读(草稿)

**改了什么** —— 两份治理记录(ADR-0132、ADR-0133)的 `**Status**` 行,各改一行:从「Proposed ——
等待维护者手工合并,而那次合并本身就是接受行为」改成「Accepted (2026-09-07) —— 由把它落到 `main`
上的那次合并接受」,并各自回指自己的落地 PR 与提交(0132 → objectstack-ai#16215 / `c677cda816`;0133 → objectstack-ai#16267 /
`13c08356ab`)。两份文件的其余内容、另外 17 份 `Proposed` 的 ADR、以及看着同形但必须排除的
0134,全部未动。整个 diff 是 2 个文件、4 行增、4 行删。

**为什么改** —— 这两份 ADR 早在 2026-09-07 就已经合并进 `main`,而且都是由授权批准人 os-zhuang 合的
—— 也就是说,它们自己那行字所说的「接受行为」**确实已经发生了**,只是没有人回去改那一行。ADR 的 Status
字段是这份记录唯一一处机器可读的效力声明:今天任何人(或任何工具)按它判断「这条能不能当既定法引用」,得到的都是错的答案,而正文还在要求读者「在这份记录合并之前,下面的一切都未定」。实测有
**51 个文件、110 处**引用这两份 ADR,其中 `packages/plugins/organizations` 的插件、README
和九个测试都把它当权威引用。⚠️ 这个失效是**沉默的**:两份都在 `main` 上,CI 全绿,没有任何门禁会因此变红 ——
所以它只能靠人读出来。同一套约定的 ADR-0130 / 0131 落地后都改成了 `Accepted` 并回指落地 PR,本 PR
照抄它们的拼法,⛔ 没有发明第三种。

**风险与代价(含回滚)** —— 风险极低:改的是四行散文,不改任何决策文本、任何标题、任何代码,不发布任何 npm 包内容(故
`skip-changeset`)。本地推导出的 18 个门禁族全部 exit 0,仓库级 `pnpm lint` 也 exit 0。⚠️
唯一需要您知道的判断:**这两份记录被判定为「文字过时」,而不是「未被接受却已被当法引用」** —— 依据是两份的合并人都是
os-zhuang(授权批准人),卡片里原本存疑的正是 ADR-0133 这一条,现已查实。若您认为 ADR-0133
那次合并**不**构成它所说的接受行为,那么正确的修法与本 PR **相反**(要补一次接受行为,而不是改一行字),此时请直接关掉本 PR ——
回滚成本就是关掉它,没有任何其他产物需要撤。

**席位意见** ——

**你要做的** —— ① 确认上面那条判断:ADR-0133 由 os-zhuang 合并,是否算作它那行字所说的「维护者手工合并 =
接受行为」。② 若认可,本 PR 是受管面(`docs/adr/**`),按 Prime Directive objectstack-ai#14
需要您本人的手工合并或一次授权批准 —— ⛔ 本 PR 恒为 draft,任何 AI 席位都不会把它翻成 ready、不会入队、不会开
auto-merge。③ 若您希望连带把另外 17 份 `Proposed` 的 ADR 一起盘一遍,那是另一张卡 —— 本卡刻意没量它们。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ors (objectstack-ai#19200)

Third round of the `scripts/**` unresolvable-citation migration (PRs
objectstack-ai#15806 and objectstack-ai#16338 were rounds one and two), **devx-lane slice only**.

`scripts/check-scripts-symbol-anchors.mjs` registers `scripts/**` `.mjs`
comment prose as a symbol-anchor corpus with `judgeUntrackedLineAnchors:
false`, so a citation naming no tracked file is seen, counted and
enumerated — and judged by nothing. This PR migrates **six** of those,
by the gate's own repair form (its header, under `[line-anchor]`):

> ANCHOR FORM: name the file as a file-level anchor and put the number
beside it as data

Not one digit is repaired or repointed. Every number that was part of an
anchor is still on its page, as data.

## The six, and why each target is unambiguous

| site | citation | resolves to |
|---|---|---|
| `scripts/check-changeset-no-major.mjs` | `date-range-presets.ts:101` |
`packages/spec/src/data/date-range-presets.ts` |
| `scripts/cross-package-test-inputs.mjs` | `:102`, `:92`, `:484-485`
(continuations) |
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts` |
| `scripts/ts-parse.mjs` | `tenant-audit-census.mjs:459`, `:723` |
`scripts/tenant-audit-census.mjs` |

- **`date-range-presets.ts`** — one tracked file of that basename
(`date-range-presets.test.ts` is a different basename), and line 101 is
inside the TSDoc block the surrounding prose describes.
- **`retired-key-migrate-sentence.test.ts`** — one tracked file, and all
three numbers still land on what the prose says they do:
`PUBLISHED_SKILLS_ROOT` on line 102, the "plus every `.md` file under
`skills/`" sentence on line 92, and the `skills:`-prefixed `files.push`
on lines 484-485.
- **`tenant-audit-census.mjs`** — one tracked file of that basename
(`check-tenant-audit-census.mjs` is a different basename). It carries
both cited constructs, the `.replace(/\s+/g, ' ')` collapse and the
synthetic `type CensusReceiver = ...` alias; both have drifted off the
cited lines, which is precisely why those lines are kept as dated data
rather than repointed.

## The gate's own dated record is corrected in the same edit

Both prior rounds did this, and it had gone stale in a way that matters:
the header claimed a residual of **8** and named holders (PRs objectstack-ai#16215,
objectstack-ai#16202) that have since merged, while the live command printed **18**.

That regrowth — 8 back up to 18 between rounds, because gate-header
prose written in the interval added citations faster than round two
removed them — is the most load-bearing reading here, and the header now
says so. **This is not a burn-down; it is a quantity that grows.**

## Acceptance, measured on this branch

```
node scripts/check-scripts-symbol-anchors.mjs --list-unresolvable   EXIT 0   18 -> 12
node scripts/check-scripts-symbol-anchors.mjs --self-test           EXIT 0
node scripts/check-scripts-symbol-anchors.mjs                       EXIT 0
```

Row-by-row `diff` of the before and after listings: the **only** lines
removed are the six above, plus the summary line changing from `18 ...
10 bare-filename, 5 continuation, 3 directory-qualified` to `12 ... 8
bare-filename, 1 continuation, 3 directory-qualified`. The other twelve
rows are byte-identical and still listed.

## What is deliberately NOT here

- **`judgeUntrackedLineAnchors` stays `false`.** Whether it should flip
is the open question this card exists to put in front of triage, and it
is now a sharper question than a counting one — see below.
- **The remaining twelve rows are untouched**, every one of them on
another seat's surface: `scripts/pm/check-governed-merges.mjs`,
`scripts/pm/check-skill-line-ratchet.mjs`,
`scripts/pm/check-prior-rulings.mjs`,
`scripts/pm/check-half-states.mjs`,
`scripts/pm/check-widening-tells.mjs` (governed-surface and `pm` patrol
instruments) and `scripts/check-spec-docblock-symbol-anchors.mjs`
(SUBJECT is `packages/spec` docblocks). No card is filed for them here.

## Open question for triage

A flag that flips at residual zero flips into a gate that reds on the
**next** author's first bare citation — and the 8 -> 18 regrowth
measured above says there will be a next author, soon. So what the flip
needs is a **remedy for an unbindable abbreviation** (a bare `engine.ts`
a resolver cannot bind to one of several files of that name), not a
smaller number. Reaching zero and flipping the same day would hand the
repo the permanently-red gate its own header says it retired.

Part of objectstack-ai#15809

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_

Co-authored-by: Claude <noreply@anthropic.com>
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