Skip to content

After ADR-0132 the open tree still calls @objectstack/organizations enterprise/closed-source in 65 places — including tenancy-service.ts, the file that decides the posture #16718

Description

@hotlong

Found while implementing #16137 (the open-only wall acceptance for the #16130 migration). Not a rider on that PR: it is a 45-file prose sweep across seven packages, and the acceptance PR is a test-only diff.

The measurement

ADR-0132 moved the org-scoping registrar into the open core: packages/plugins/organizations is Apache-2.0, on main since PR #16215, and its own header states the boundary — "This package carries NO licence check of any kind". #16137 measures that an open-only composition really does raise the wall, and it does.

The open tree's own prose has not followed. Measured at 2e6a2ea4c:

git grep -c -E "enterprise (`?@objectstack/organizations|multi-org|organizations)|closed-source `?@objectstack/organizations|cloud-private `?@objectstack/organizations" -- 'packages/**/*.ts' \
  | grep -v '^packages/plugins/organizations/'

65 occurrences across 45 files, seven packages. The heaviest, by count:

file hits
packages/verify/src/harness.ts 6
packages/cli/src/commands/serve-organizations-message-spelling.test.ts 5
packages/plugins/plugin-dev/src/dev-plugin.ts 3
packages/plugins/plugin-auth/src/ensure-default-organization.ts 3
packages/cli/test/serve-organizations-host-resolution.e2e.test.ts 3
packages/cli/src/commands/serve.ts 3
packages/plugins/plugin-auth/src/tenancy-service.ts 2

Why this is not cosmetic

The single worst site is packages/plugins/plugin-auth/src/tenancy-service.ts — open core's own source of truth for "what tenancy posture is this deployment in?". Its header still reads:

Both walled postures require the enterprise @objectstack/organizations package (the org-scoping service) to ACTIVATE ... multi-organization operation is a commercial capability (ADR-0105 D12).

and isolationActive's docblock still argues "Open code, entitled activation ... ENABLING a multi-organization posture is an entitlement". ADR-0132 settled that the other way for the open package: an open install is entitled to both walled postures by construction, which is exactly what the open OrganizationsPlugin.supportedPostures now declares.

So the maintainer's stated goal for #16130 — 「迁移之后的效果应该是开源版就可以把元数据应用使用单库多租户的方式运行。」 — is now true in code and denied in the comments an open-source reader meets first. Someone reading tenancy-service.ts to answer "can my open install run isolated?" is told no, by the file that decides it.

packages/cli/src/commands/serve.ts is the same shape one layer out: the block that mounts the package still says the multi-org runtime "lives in the closed-source @objectstack/organizations package".

Scope, and what should NOT be swept

Not every hit is wrong, and a blanket rewrite would break real facts. Three populations, and they need separating before anything is edited:

  1. Now false — prose describing the open registrar as commercial/closed-source/cloud-private. tenancy-service.ts and serve.ts lead this one.
  2. Still true, about the OTHER package — the commercial repo genuinely ships a private package of the same name that subclasses this one and calls its own licence gate. Text about that is correct and must stay, in no-framework-dependents.pin.test.ts's terms.
  3. True but stale as a reason — packages/verify/src/harness.ts skips its cross-tenant proofs because "the enterprise package is not installable in this workspace". It is installable now, which is a live capability question rather than a wording one, and probably its own card.

Refs: #16130 (the move) · PR #16215 (the open package landing) · #16137 (the acceptance that measures the claim, and where this was found) · #16539 (a dogfood control whose premise died from the same move — adjacent, narrower) · ADR-0132 · ADR-0105 D12 (the passage several of these sites cite, as amended).

Activity

  1. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    Graded out of finding into pm:queue, domain:services.

    Verified the load-bearing part rather than the count: packages/plugins/plugin-auth/src/tenancy-service.ts — the open core's own authority for resolving the posture — still says at :31 that "Both walled postures require the enterprise @objectstack/organizations package" and at :36 that "operation is a commercial capability (ADR-0105 D12)". Five occurrences in that one file.

    Both statements are now false. ADR-0132 made the package Apache-2.0 open core, and #16137's acceptance (PR #16719) has just proven an open-only composition raises the isolated wall with no licence check reachable — measured twice, each with a positive control.

    This is not cosmetic drift. It sits in the file that decides the posture, so the next reader looking for "can open source do multi-org?" finds the answer no in the source of truth, next to code that now does it. Grading it as queue work rather than an observation for that reason.

    ⛔ Keep the card's three-population split when fixing: statements that are still correct (cloud's licence gate really is commercial; group posture management really is an EE product surface) must not be swept along with the ones ADR-0132 falsified.

  2. self-assigned this
    on Sep 8, 2026
  3. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    Claim: PM session 6679d191-11f4-465b-b322-0e0409d76793 (seat hotlong), branch claude/organizations-is-open-core-prose-16718.

    Dispatched on the maintainer's direct instruction 2026-09-08: 「ADR-0132 之后,开源树里仍有 65 处、45 个文件把 @objectstack/organizations 称作企业版/闭源/cloud-private。这个也要处理」

    The card's three-population split is carried into the dispatch as a hard constraint, not a suggestion — population 2 is genuinely true and must survive, and population 3 (verify/src/harness.ts skipping its cross-tenant proofs because the package "is not installable in this workspace") is dispatched as report-only: the card is right that it is a live capability question rather than a wording one, and turning those proofs back on is a different change with a different risk profile.

  4. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    Dead-claim recovery — re-dispatching

    The first dev stalled (no progress for 600s; the stream watchdog did not recover) after getting as far as "reading the issue and setting up the worktree". That is a normal agent death, ⛔ not a maintainer abort and ⛔ not a signal about the card.

    Checked before re-dispatching rather than assuming zero progress, because it had in fact pushed something:

    refs/heads/claude/organizations-is-open-core-prose-16718  6ba0db4e0
    origin/main                                              05374bee6
    git merge-base --is-ancestor 6ba0db4e0 origin/main  → yes
    

    So the branch exists but is empty — its tip is an ancestor of main, no commits, an empty diff. Nothing to salvage and nothing to collide with. main has also moved since it was cut, so the replacement branches fresh.

    Claim stands with the same PM session; re-dispatched now.

  5. hotlong commented on Sep 10, 2026

    @hotlong
    ContributorAuthor

    Second dead claim on this card — re-dispatching a third time, with the pattern recorded

    Both dispatched devs died the same way, and neither produced anything:

    attempt outcome branch
    1 (2026-09-08) stalled, no progress for 600s, watchdog did not recover; last action was "reading the issue and setting up the worktree" pushed claude/organizations-is-open-core-prose-16718 — empty
    2 no completion record at all same branch, still empty

    Verified rather than assumed, both times: the branch tip 05374bee6 is an ancestor of origin/main, zero commits, empty diff. And the work is demonstrably not done — packages/plugins/plugin-auth/src/tenancy-service.ts still returns 2 hits for the enterprise-description pattern.

    Both deaths happened before any work started, not partway through a 45-file sweep. So this reads as infrastructure rather than the card being too large or malformed — ⛔ resisting the temptation to split it on that theory, since splitting on a wrong diagnosis would just produce three cards that stall.

    Recording this so a third failure is evidence of a pattern rather than a surprise. If attempt 3 dies the same way before committing anything, that is the point to stop re-dispatching and ask what is different about this card's setup — ⛔ not to try a fourth time.

    Claim stands with the same PM session.

  6. hotlong commented on Sep 10, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 16718,
      "status": "done",
      "branch": "claude/organizations-open-core-prose-16718c",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17371",
      "premise_still_valid": true,
      "summary": "Corrected 57 statements across 40 files that called the open Apache-2.0 @objectstack/organizations enterprise / closed-source / cloud-private, led by the two sites the card named: plugin-auth/src/tenancy-service.ts (rewritten around ADR-0132 D2/D3/D4 - mechanism unchanged and said so, the answer to 'can an open install run isolated?' is now yes) and cli/src/commands/serve.ts (the mount block and the operator install remedy that ended 'Without one this bullet is not followable'). Text only: every hunk is a comment or a string literal, no control flow and no identifier moved. Under the card's own pattern, the merge base e669e5eee measured 45 files / 66 hits (the card measured 65 at 2e6a2ea4c; main drifted +1); this branch measures 10 files / 18 hits, so 48 pattern hits across 35 files were corrected, plus 9 statements the pattern cannot see (its 'enterprise' is case-sensitive, and it misses the neighbouring cloud-private / 'enterprise package' spellings inside serve.ts itself). TWO THINGS THE DISPATCH DID NOT KNOW. (1) The PM's record says 'nothing to salvage': that was true of the REMOTE branch, but the local worktree of attempt 2 held an UNPUSHED commit 78cc23f8d with a complete 39-file sweep. It is cherry-picked as the first commit here (3 conflicts against main's #16013 refactor, resolved to main's text with the prose fix reapplied), and every factual claim in it was re-verified against main before keeping it - ADR-0132 D2/D3/D4, ADR-0105's 2026-09-06 amendment line, organizations-plugin.ts:194 declaring ['group','isolated'], index.ts:31's no-licence-check boundary, and npm view returning 17.4.0 Apache-2.0. (2) The card's population-3 premise - 'It IS now installable' - is only half true. no-framework-dependents.pin.test.ts, ADR-0132's mechanical entitlement boundary, forbids ANY workspace package under packages/** from declaring @objectstack/organizations in any dependency field, and says the change needs an ADR rather than a manifest line. Verified: git grep -l on all package.json returns exactly one file, the package's own. So verify's harness prose is stale in its REASON but still correct in its EFFECT - rewriting it on the card's premise would have made it newly false. That is why population 3 was left byte-for-byte alone and filed instead. ONE CONFLICT TO FLAG: the dispatch prompt orders the PR body to end with the bare '\\U0001f916 Generated with [Claude Code](https://claude.com/claude-code)' footer, while the os-dev standard clause calls for the session-URL form. I followed the dispatch's literal instruction (that form is in live use here, e.g. PR #17355) because I hold no session_01... identifier to spell the standard form with. Flagging rather than choosing silently.",
      "tests": "All on branch claude/organizations-open-core-prose-16718c at 076f1d54e, real output watched. BUILD: pnpm build --concurrency=2 -> 'Tasks: 73 successful, 73 total'. TESTS (14 affected packages, turbo --concurrency=2): core 1268 | plugin-auth 2245 | plugin-security 2103 | runtime 3532 | plugin-sharing 910 | dogfood 1064 passed +3 skipped | types 597 | service-storage 593 | service-settings 578 | service-datasource 676 | platform-objects 561 | mcp 292 | plugin-dev 72 | cli 3015 passed with 10 failed. THE 10 CLI FAILURES ARE A macOS ARTIFACT, PROVEN NOT ASSUMED: 3 files (published-subpath-console.pin, published-subpath-hook-body.pin, serve-runtime-state-project-key), every assertion a path comparison of the form expected '/tmp/os-cli-subpath-MGca4r/...' vs received '/private/tmp/os-cli-subpath-MGca4r/...'. /tmp is a symlink to private/tmp on this Mac and the tests compare mkdtempSync(join(tmpdir(),..)) against a child's realpath-resolved answer. CONTROLLED EXPERIMENT: those same 3 files re-run with TMPDIR=/private/tmp and nothing else changed -> 'Test Files 3 passed (3) | Tests 41 passed (41)'. No source change can put /private on one side and not the other; CI is Linux. I did NOT run a clean-origin/main baseline - the TMPDIR experiment is the evidence offered instead. TYPECHECK: pnpm turbo run typecheck over the same 14 -> 'Tasks: 78 successful, 78 total'. LINT: pnpm lint (eslint . --no-inline-config, WHOLE REPO, not narrowed) -> exit 0, no output. GATES (declared narrowing; dispatch-gates.mjs derived 60+ families for this path set, CI runs the farm regardless) - all exit 0, quoting each gate's own verdict line, exit codes captured by redirect-then-$? never through a pipe: check:nul-bytes 'scanned 8173 text file(s) ... no raw ASCII control bytes' | check:auth-mount-ledger '19 ObjectStack auth mount(s), all accounted for' | check:verify-stand-in '2 stand-in check(s) guarded ... 10 call site(s) reached' | check:cross-package-test-inputs '28 package(s) read outside themselves, all declared' | check:test-source-alias '73 packages with tests scanned' | check:org-identifier '2653 author-facing source file(s)' | check:tenant-chokepoint, check:single-claim-paths, check:watch-hint-literal, check:error-code-casing. ABLATION on the two pin tests the card flagged, run on the COMMITTED tree with a trap and absolute paths. NO REBUILD LEG APPLIES and here is why, not an omission: both pin files import their subject by RELATIVE source path inside packages/cli ('./serve.js', '../utils/tenancy-posture-hints.js'), so vitest resolves source and no dist/ is on the path. MUTATION CONFIRMED ON DISK both directions by occurrence count, never by the editor's exit code: leg A injected-text count 1 / removed-text count 0; leg B injected 1 / removed 0. RESTORE PROVED at all four checkpoints by BOTH git diff HEAD empty AND git hash-object equal to the HEAD blob for both files (e53a486b57e02afc9506ddc8c61005b7e6097af3 / b3aaf1951596fb751f87e9f1b4b60cab859be70b); an empty hash was coded as FAILURE, not as 'nothing to compare'. RESULTS: leg A (re-insert 'enterprise' into the isolated posture hint) exit 1, 3 failures across BOTH pin files including doctor's 'CONTROL - these pins can say no'; leg B (re-insert it into serve's install-remedy bullet) exit 1, 2 failures including serve's '#12151 CONTROL - these pins can say no'; control on the restored tree exit 0, 19 passed (19). The pins were updated, not relaxed. EXTERNAL FACT CHECKED BEFORE WRITING IT INTO OPERATOR TEXT: npm view @objectstack/organizations version license -> 17.4.0, Apache-2.0, so serve.ts's new 'published on the public npm registry' claim is measured, not inferred from the manifest.",
      "mcp_calls": "0 - every GitHub read and write went through the gh CLI (REST/search), no MCP GitHub tool was called",
      "open_questions": [
        {
          "question": "The card's pattern is a proxy, and a broader sweep of the same vocabulary is still open: roughly 60 more 'enterprise package/plugin/runtime' and 'cloud-private' statements live outside it, in packages/spec, packages/types, packages/services/service-cluster, packages/rest, packages/core and several plugin-dev / plugin-auth test headers. Many are population 2 (they describe OTHER genuinely commercial packages - security-enterprise, service-ai-studio, the multi-node gate carrier), but some are the same ADR-0132 falsehood in a spelling the card's regex misses. Should that be swept?",
          "options": [
            "A - file a follow-up card scoped to 'the organizations name only, all spellings', excluding sentences about other commercial packages",
            "B - fold it into this PR now",
            "C - leave it; the card's measurement is the agreed boundary and the residue is mostly population 2"
          ],
          "recommendation": "A. B would blow past the boundary the card drew and re-open the population-2 sorting on packages I have not verified (security-enterprise and service-ai-studio really are private, and I have not checked each sentence). C understates it: I confirmed at least a few are the same falsehood, e.g. plugin-dev/src/dev-plugin-tenancy-posture.test.ts:23 still calls @objectstack/organizations 'a cloud-private enterprise package'. A separate card can do the population sort properly, per package."
        },
        {
          "question": "The stale worktree from attempt 2 is still on disk at /Users/zhuangjianguo/Documents/GitHub/objectstack-issue-16718, holding commit 78cc23f8d (now superseded by this branch) and TWO uncommitted files that look like an un-restored ablation leg - serve.ts and tenancy-posture-hints.ts both have 'enterprise' re-inserted, i.e. the dead dev died mid-mutation with no trap. I did not touch it: it is another actor's tree and cleaning it is not mine to do.",
          "options": [
            "A - PM removes it (its only commit is salvaged here, so nothing is lost)",
            "B - leave it until the maintainer looks",
            "C - I remove it in a follow-up turn if PM says so"
          ],
          "recommendation": "A. Verified before saying so: its commit 78cc23f8d is cherry-picked into this branch as aaee264ca, and its two uncommitted files are a mutation leg, not work - both edits re-introduce the exact word this card removes."
        }
      ],
      "out_of_scope_findings": [
        "filed as #17369: population 3 reported, not changed - verify's cross-tenant proofs are skipped for a reason ADR-0132 falsified, but the obvious fix (a devDependency in packages/verify) is pinned shut by no-framework-dependents.pin.test.ts, ADR-0132's entitlement boundary, which requires an ADR rather than a manifest line; the card carries the three routes and the pin's own apps/* + examples/* exclusion as the way through",
        "not filed, flagged in open_questions: the broader 'enterprise / cloud-private' vocabulary outside the card's regex (packages/spec, packages/types, service-cluster, rest, core and several test headers) needs a per-sentence population sort before anyone sweeps it"
      ]
    }
  7. hotlong commented on Sep 10, 2026

    @hotlong
    ContributorAuthor

    PM answers, and a correction to my own record on this card

    ⚠️ My dead-claim recovery was wrong, and the dev caught it

    Twice on this card I wrote "nothing to salvage" and backed it with the remote branch being an ancestor of main with an empty diff. That reading was correct and the conclusion drawn from it was not.

    Attempt 2's local worktree held commit 78cc23f8d — a complete 39-file sweep, committed and never pushed. From the remote, a dev that dies after committing but before pushing is indistinguishable from one that did nothing. I only looked at the remote.

    Verified today: that worktree existed at /Users/zhuangjianguo/Documents/GitHub/objectstack-issue-16718, carried exactly that commit, and every one of its 39 files is covered by this PR's 41 — comm -23 between the two file lists is empty, so nothing was lost in the salvage.

    Working rule for the next dead claim: an empty remote branch is not proof of zero work. Check git worktree list before declaring a claim dead.

    Open question 1 — the broader vocabulary sweep: A

    File a follow-up scoped to the organizations name only, all spellings, excluding sentences about genuinely commercial packages. ⛔ Not B: folding it in would blow past the boundary this card measured and reopen a population-2 sort over security-enterprise and service-ai-studio, which really are private and which you have not checked sentence by sentence. ⛔ Not C either — you found at least one clear instance (plugin-dev/src/dev-plugin-tenancy-posture.test.ts:23 calling it "a cloud-private enterprise package"), so "mostly population 2" is not "all".

    Open question 2 — the stale worktree: A, done

    Removed, after verifying it was disposable rather than on your say-so: its only commit is salvaged here, and its two uncommitted files were confirmed to be a mutation leg — both re-insert the exact word this card removes (enterprise ${ORGANIZATIONS_RUNTIME_PKG} and (the enterprise multi-org runtime)). A dead dev died mid-ablation with no trap restore.

    ⭐ Leaving it was the riskier option, not the safer one: a tree whose working copy silently contradicts the change, sitting next to 40 sibling rigs, is exactly what a future agent picks up by accident.

    Your population-3 judgment was right, and it overrode this card's own premise

    The card asserts "It is installable now". You checked instead of adopting it, and no-framework-dependents.pin.test.ts says the opposite in ADR-0132's own words:

    ⛔ So: no workspace package may declare @objectstack/organizations, in any of the four dependency fields, ever. Apps declare it; packages do not. Adding such a dependency is a decision about the commercial boundary and it has to be argued on an ADR, not merged as a manifest line.

    Counter-checked: exactly one package.json in the tree declares it — the package's own. So verify's harness prose is stale in its reason and still correct in its effect, and rewriting it on the card's premise would have produced a newly false sentence in place of a stale one. Leaving it byte-for-byte and filing #17369 was the right call.

    On the footer conflict

    The dispatch's literal instruction wins over the standard clause — that is what a per-card instruction is for, and flagging it rather than choosing silently was correct.

  8. github-actions commented on Sep 10, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34484272265 · trigger schedule

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions