Skip to content

spec: re-type TenantPlan / sys_environment.plan as an opaque plan identifier — measured: no reader outside the cloud distribution branches on its values (cloud#1216) #7513

Description

@os-zhuang

Spec-side half of cloud#1216, filed by the repo:cloud seat per the maintainer's ruling there (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd), verbatim:

> Direction hinges on one measurement: does any reader branch on the plan values (solo / team / business) — in framework code, console, or cloud itself beyond writing them? […] All readers treat it opaquely → spec re-types the field as an opaque plan identifier whose vocabulary is control-plane config (documented as such in the describe), and cloud owns the value set without touching the schema.

The measurement has been run (2026-08-11, cloud seat, session session_01PPDkETJ37ntVTFUZWX5FQg). Result: the opaque arm. No reader outside the cloud distribution branches on plan values.

Measurement (all at origin/main of each repo; framework also valid at cloud's pin 62b6a2fb)

Framework — zero TenantPlan consumers outside spec, and the one plan-adjacent seam is deliberately opaque:

  • git grep -l "TenantPlan" origin/main -- 'packages/**/*.ts' | grep -v packages/spec/empty (positive control: the same grep unfiltered hits the spec files).
  • packages/cloud-connection/src/runtime-config-plugin.ts is plan-agnostic by design: the header says which keys exist and "which billing plan unlocks them is a distribution concern" (:25), the resolver "receives an opaque environment token (the cloud distribution passes the plan string)" (:30), and the only touch is resolvePlanFeatures?: (plan: string | undefined) (:135) called as features = featuresFor(resolved.plan, features) (:272-273) — a pass-through into a distribution-supplied callback, typed string | undefined, no value branching.

Console (objectui) — display-only:

  • The only non-noise reader is packages/app-shell/src/environment/entitlements.ts:68-71 (planPhrase): interpolates the plan string into upgrade-dialog copy, branching only on falsy/'free' — a value both vocabularies share. No branch on solo/team/business anywhere in src/** / packages/** (checked with the noise classes excluded: flow-node 'team' kinds, sys_team, business-unit).

Cloud itself — the branching readers are the distribution's own policy modules, i.e. exactly "control-plane config":

  • packages/objectos-runtime/src/plan-entitlements.ts (isFreePlan, planAllowsAiStudio, planToAiModel, planAllowsScim, aiDailyQuotaUnitsForPlan, planAllowsCustomDomain) and its documented mirror packages/service-cloud/src/plan-entitlements.ts — consumed by cloud-runtime-config-plugin.ts:43, which plugs into the framework's opaque seam above. The ADR-0005 comments in those files state the policy is cloud-owned.

Re-check commands (per premise-line discipline):

git grep -l "TenantPlan" origin/main -- 'packages/**/*.ts' | grep -v packages/spec/   # framework, expect empty
git grep -nw "plan" origin/main -- packages/cloud-connection/src/runtime-config-plugin.ts
# objectui: git grep -nE "'(solo|team|business)'" origin/main -- 'packages/**/*.ts*' (exclude team_member/flow-kind noise)

Ask (spec lane)

Per the ruling's opaque arm:

  1. Replace the closed TenantPlanSchema enum (packages/spec/src/cloud/tenant.zod.tsfree/starter/pro/enterprise/custom) with an opaque plan identifier (string), with the describe stating the vocabulary is control-plane config owned by the cloud distribution, not protocol.
  2. One sentence worth keeping in the describe: the shared convention that empty/unknown ⇒ free tier (isFreePlan in both cloud mirrors normalizes that way, and the console's only branch is 'free').
  3. This is a widening — the enum's current values are a strict subset of "any string" — and nothing was found that parses the enum at runtime today (cloud#1216 established the row is validated by the cloud-declared object, not spec, on this path). Version/strictness handling per the spec seat's own rules.

Downstream (stays in cloud, blocked on this card): cloud#1216 — drop the hand-written BillingPlan union's divergence from spec once the opaque type lands (the #1027-style consolidation this divergence currently blocks).

(Unlabeled on purpose — packages/spec has one owner; routing is the triage seat's.)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions