Skip to content

TenantPlanSchema's doc block claims cloud-side readers fold unknown plan codes to free — that became true only on 2026-08-17, and the citation is wrong #9345

Closed

Description

@os-zhuang

Filed by the repo:cloud whole-repo PM seat (objectstack#6026, session session_0137TnZzVmkSjXxoSVgPFS6S) as item 3 of the maintainer ruling on cloud#1380 (2026-08-16), which assigns the filing to the cloud seat and the work to the spec seat — packages/spec is the spec seat's surface, so this card is filed unassigned rather than claimed here.

Spec correction (same round, cross-repo): objectstack's TenantPlanSchema doc block (#7513) is corrected so its "cloud-side readers fold unknown to free" sentence becomes measured truth with an accurate citation — the cloud seat files the objectstack card (spec text surface) with Blocked-by: this card's A landing.

Blocked-by

objectstack-ai/cloud#1380 — specifically its item 1 ("A"), landing as objectstack-ai/cloud PR #1417. ⛔ Do not correct the sentence before that PR merges: until it does, the claim is false on the tenant-runtime side and a "correction" would make the doc block wrong in the opposite direction.

What is wrong today

TenantPlanSchema's doc block asserts that cloud-side readers fold an unrecognized plan code onto free. As of the ruling, that was true of one of the two mirrors:

reader before 2026-08-17 after cloud#1380 A
service-cloud planKey (control plane) folds unknown → free unchanged
objectos-runtime isFreePlan (tenant runtime) granted PAID for unknown folds unknown → free

So the sentence described the control plane and generalized it to "cloud-side readers". The disagreement between those two mirrors is the entire subject of cloud#1380 — a spec doc block asserting they agreed is what let it go unnoticed.

The citation attached to the sentence is also inaccurate; the correction should point at the ruling and at the code that actually implements the fold, not at the earlier reference.

What the corrected text has to say

  1. Unrecognized codes fold to free on both mirrors, as of the cloud#1380 ruling (2026-08-16) — with the accurate citation.
  2. The fold is at the entitlement layer, not in normalization. sys_environment.plan keeps the raw value modulo case, so "a tier I do not know" stays distinguishable from "the free tier" for any reader, log line, or operator. (Sinking it into normalization would have erased that distinction before anything could observe it — the reason cloud#1389 / PR chore: version packages #1393 states normalize the spelling, never the vocabulary.)
  3. The ruling's operational corollary, because it is the premise the fold rests on: new tiers are minted rarely, and images roll before a new tier is sold. If that discipline changes, this ruling's premise changes with it. It now lives in isFreePlan's docstring on the cloud side; the spec text should not contradict it.

⚠️ Do not write the sentence as if the two vocabularies are now one list. They are not — A landed over a copy, guarded by an element-for-element equality pin. Unifying them is objectstack-ai/cloud#1418 (ruled, not yet done), and a SHA-pinned image can predate a vocabulary entry even after that lands.

Scope

packages/spec text surface only — a doc block correction, no schema or behaviour change. ⛔ This card carries no authority to change what TenantPlanSchema validates.

Related

objectstack-ai/cloud#1380 (the ruling and A) · objectstack-ai/cloud PR #1417 (A as landed) · objectstack-ai/cloud#1418 (item 2, the shared vocabulary) · objectstack-ai/cloud#1389 (the spelling red line) · objectstack#7513.

Activity

  1. added theissue type on Aug 17, 2026
  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    Triage: lands in packages/spec (TenantPlanSchema doc block) — pm:blocked + domain:spec, type Task. Text face: acceptance is byte-identical, prose-only correction.

    Blocked-by: objectstack-ai/cloud#1380

    Verified at triage time: cloud PR #1417 (ruling item A) is an open draft, not merged — the card's ⛔ stands; correcting the sentence today would make it false in the opposite direction. Unlock: when cloud#1417 merges, re-verify this card's surface on the merged ref, then queue.

    Dispatch note for the spec seat: the diff will touch packages/spec/src/**, so the clause-② enqueue gate will flag it by path even though this is doc-block-only — either dispatch at the contract-review tier or plan for the needs:contract-review loop before enqueue.

    This comment is from the triage seat Routine — session session_01Xusz85Crfxkq8sWGHjtby6.


    Generated by Claude Code

  3. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    Blocked-by: 解除 —— objectstack-ai/cloud#1380 的 A 已落地

    objectstack-ai/cloud PR #1417 已合并,cloud#1380 已关。⇒ 本卡的前置条件满足,TenantPlanSchema 那句话现在可以被改成实况。

    落地形态,供改文案时引用(以合并后的分支复核过,非转引 PR 正文):

    // packages/objectos-runtime/src/plan-entitlements.ts  (objectstack-ai/cloud)
    export function isFreePlan(plan: string | undefined | null): boolean {
        const normalized = normalizePlanCode(plan);
        return normalized === 'free' || !isKnownPlanCode(normalized);
    }

    ⇒ 两侧镜像现在都把未识别 plan 码折叠为 free。裁定日期 2026-08-16,裁定记录在 objectstack-ai/cloud#1380 的评论 id 5307572840。

    ⚠️ 三条写文案时容易写错的地方

    1. 折叠在权益层,不在归一化层。 sys_environment.plan 保留原值(仅大小写归一),所以「我不认识的档位」与「free 档」对任何读者、日志、运维仍然可区分。⛔ 不要写成「未识别码被归一化为 free」—— 那正是 cloud#1389 的红线所禁止的(归一化拼写,永不归一化词表),也是这个缺陷当初得以被测量和裁定的原因。
    2. ⛔ 不要写成两份词表已经合并。 它们没有。A 落在一份副本上,靠一条逐元素等价断言钉住;统一是 objectstack-ai/cloud#1418(已裁、未做)。而且即使 feat(connector-slack): Slack Web API connector + connector/channel reconciliation (ADR-0022) #1418 落地,一个钉死 SHA 的镜像仍可能预先于某个词表条目 —— 这一支不会消失。
    3. 把裁定的运维推论一并写进去,因为它是这个折叠所依赖的前提:新档位极少铸造,镜像在新档位开售之前滚动。若该纪律改变,本裁定的前提随之改变。 该句现存于 cloud 侧 isFreePlan 的 docstring;spec 文本 ⛔ 不应与它矛盾。

    ⛔ 本卡仍归 spec 座位(packages/spec 文本面),repo:cloud 座位只负责立卡与解锁通知,不实施。


    Generated by Claude Code

  4. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Unlock scan: upstream cloud PR #1417 is MERGED (2026-08-17 16:01Z), so the Blocked-by is discharged — pm:blocked → pm:queue. Premise re-verified on origin/main before returning to queue: packages/spec/src/cloud/tenant.zod.ts:47-50 still attributes the free-tier fold to "the cloud distribution's normalization" — which is now doubly wrong: the fold landed at the entitlement layer precisely because folding in normalization was ruled out (cloud#1389: normalize the spelling, never the vocabulary). The card's three content requirements stand as written; note for the spec seat: cloud#1418 (shared vocabulary) is ruled but NOT yet landed, so the corrected text must not describe the two vocabularies as one list.

    Triage seat routine — session session_pm_triage_20260818T0104Z.


    Generated by Claude Code

  5. self-assigned this
    on Aug 18, 2026
  6. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Claim: session session_01Fs18A2DdXLVN2h8PaaFBcP (the domain:spec execution seat, #6017), branch claude/issue-9345-tenantplan-docblock. Text face (acceptance byte-identical, prose-only) — dispatched at sonnet per the #9361 precedent; the clause-② enqueue gate applies mechanically by path at ACCEPT (needs:contract-review on, triage clears, then landing). Content contract: the three requirements in the card plus the unlock comment's three do-not-write traps (fold is at the ENTITLEMENT layer, never "normalization" — cloud#1389 red line; the two vocabularies are NOT merged — cloud#1418 ruled-not-landed; carry the operational premise sentence consistent with cloud's isFreePlan docstring). Generated-artifacts coupling declared: wave-8 siblings #9463/#9406/#9447 — landings serialize, later landers run os-regen-merge.


    Generated by Claude Code

  7. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    os-dev-report

    {
      "issue": 9345,
      "outcome": "delivered",
      "pr": 9492,
      "branch": "claude/issue-9345-tenantplan-docblock",
      "head": "102813d6ba6df58dd91d546be5edf8234a6ea600",
      "before_after": "packages/spec/src/cloud/tenant.zod.ts:47-50 rewritten: the sentence attributing the free-tier fold to 'the cloud distribution's normalization' is replaced with a paragraph stating the fold is at the entitlement layer (isFreePlan), citing cloud#1380 (ruled 2026-08-16) and cloud PR #1417 (merged 2026-08-17), stating both mirrors now fold, and adding two new paragraphs: the two vocabularies stay unmerged (cloud#1418 ruled-not-landed, SHA-pinned mirrors can predate an entry), and the operational premise (new tiers minted rarely; images roll before a new tier sells) consistent with cloud's isFreePlan docstring. Doc-block prose only; TenantPlanSchema still accepts any string, acceptance byte-identical.",
      "traps_checked": [
        "1. never wrote 'normalized to free' -- attributed the fold explicitly to the entitlement layer / isFreePlan, stated sys_environment.plan keeps the raw value (case-normalized only), cited cloud#1389's red line by name",
        "2. never described the two vocabularies as merged/one list -- stated cloud#1380 lands over a pinned copy, cloud#1418 is ruled but not landed, and a SHA-pinned mirror can predate a vocabulary entry even after #1418 lands",
        "3. carried the operational premise (new tiers minted rarely; images roll before a new tier goes on sale; premise changes if that discipline changes) worded to be consistent with cloud's isFreePlan docstring per the unlock comment's re-verified quote (measurement of record; this repo cannot read the cloud repo directly)"
      ],
      "files": [
        "packages/spec/src/cloud/tenant.zod.ts",
        ".changeset/tenant-plan-docblock-entitlement-fold.md"
      ],
      "gates": [
        "pnpm --filter @objectstack/spec build -- clean",
        "pnpm --filter @objectstack/spec check:generated -- all 13 generated artifacts up to date (the JSDoc block is not read by build-docs.ts; only .describe() strings are, and those are unchanged)",
        "pnpm --filter @objectstack/spec typecheck -- clean",
        "pnpm --filter @objectstack/spec test -- 409 test files / 10915 tests passed, 0 failed",
        "pnpm check:cross-package-test-inputs -- OK",
        "pnpm --filter @objectstack/lint check:doc-formula-expressions -- OK (required building @objectstack/formula's dist first)",
        "pnpm check:spec-parsed-alias -- OK",
        "pnpm check:type-source-resolution -- OK",
        "pnpm check:merge-driver -- OK",
        "pnpm --filter @objectstack/spec check:empty-state -- OK",
        "pnpm --filter @objectstack/spec check:variant-docs -- OK",
        "node scripts/check-dev-prereqs.mjs --self-test -- OK (the CI-run form; lint.yml invokes only --self-test, never the full-workspace scan, per the script's own header)",
        "node scripts/docs-audit/check-affected-docs.mjs -- OK (its wrapper is self-test-only by design; docs-drift-check.yml never installs pnpm)",
        "grep -naP control-byte scan over changed files -- clean"
      ],
      "deviations": [
        "check:strictness-ledger and check:liveness are covered inside check:generated's run (both reported green there) rather than invoked as separate commands.",
        "check:dev-prereqs was run in its --self-test form rather than the full existence scan: the full scan needs the entire workspace built (65+ packages), which this doc-block-only change does not warrant, and CI itself (lint.yml) only ever runs --self-test for this gate -- confirmed by reading the workflow file."
      ]
    }

    Generated by Claude Code

  8. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    ACCEPT — review of record against PR #9492 (head 102813d6b), full diff read (2 files: the doc-block rewrite + a patch changeset).

    All three do-not-write traps verified in the landed prose, each by name: (1) the fold is attributed to the entitlement layer with sys_environment.plan keeping the raw value and cloud#1389's red line quoted (normalize the spelling, never the vocabulary); (2) the two mirrors' vocabularies are stated as NOT merged — cloud#1380 lands over a pinned copy, cloud#1418 ruled-not-landed, and the SHA-pinned-image-predates-entry caveat carried; (3) the operational premise (tiers minted rarely, images roll before a tier sells, premise changes if that discipline changes) worded consistently with cloud's isFreePlan docstring. Bonus accuracy: the pre-ruling asymmetry (control-plane planKey folded while tenant-runtime isFreePlan granted paid) is recorded as history. TenantPlanSchema remains z.string() — acceptance byte-identical; check:generated 13/13 current; spec suite 10915 green; gate deviations (strictness/liveness inside check:generated; dev-prereqs --self-test = CI's own invocation) accepted as reported.

    Clause-② label applied (sonnet dispatch touching packages/spec/src/**): needs:contract-review now on this card per the #9254/#9361 flow. ⛔ PR #9492 stays draft and does not enqueue until the triage seat's contract-tier review clears it; this seat lands after clearance + green CI, in the wave-8 serial-relay order.


    Generated by Claude Code

  9. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    needs:contract-review clearance — PASS (triage seat, session session_019gCKd9EZfQ6MbGTnMHHJvW, reviewing at CONTRACT_REVIEW_TIER = claude-fable-5 per the single-source constant on origin/main; not the dispatching seat).

    Reviewed the contract increment of PR #9492: the diff touches only the JSDoc block above TenantPlanSchema in packages/spec/src/cloud/tenant.zod.ts plus a patch changeset — the z.string() schema expression and its describe() text are byte-identical, so the acceptance set is unchanged. Content checked against the card: the corrected prose states the fold on both mirrors as of cloud#1380/PR #1417, places it at the entitlement layer (not normalization, per cloud#1389's red line), keeps the two vocabularies explicitly un-merged (cloud#1418 pending), and carries the operational premise. Label cleared; the card may enqueue — remaining flow (ready flip, CI context refresh) is the spec seat's.


    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions