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

Closed

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.ts — free/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.)

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 11, 2026
  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue + domain:spec.

    • Why queue, not decision-box: the ruling was already made (cloud#1216, maintainer 2026-08-10, quoted verbatim in the body) with one open condition — the reader measurement — and that measurement has been run and resolved to the opaque arm. A ruled card's execution half queues; sending it back to the box is the rest: filterAppForUser treats app hidden flag as builder-only access gate — built-in account app returns 404 for all normal users #4829 failure shape.
    • Landing anchor (re-verified @ origin/main 1530870): packages/spec/src/cloud/tenant.zod.ts:35 still exports the closed TenantPlanSchema enum, consumed at :100/:234/:332. Acceptance surface changes (closed enum → opaque string = widening) ⇒ domain:spec by the acceptance-face criterion, red-line clause.
    • Premise spot-check: framework-side consumer grep re-run — TenantPlan outside packages/spec/ is empty (positive control: unfiltered grep hits spec), matching the body's measurement.
    • Dedup: downstream cloud#1216 is pm:blocked in cloud and stays there (correct Blocked-by direction, single dispatch entry here). No open objectstack card covers the re-type.
    • target:v17: no — contract widening with no runtime parser of the enum on this path (per the cloud#1216 measurement); nothing a user hits today, not release-blocking by the four-class test. Size/model suggestion: S, sonnet.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 11, 2026
  4. self-assigned this
    on Aug 11, 2026
  5. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (spec lane)
    Session: session_01JY2Q5Xto1u8YHADgrZDTnk
    Branch: claude/issue-7513-tenantplan-opaque
    Worktree: objectstack-issue-7513
    Domain: domain:spec
    File surface: packages/spec/src/cloud/tenant.zod.ts (TenantPlanSchema + in-file consumers), any in-package TenantPlan references, generated references/baselines. Stop on breach; explain in the report.
    Container & model: S mechanical (triage suggestion adopted: S, sonnet — closed-enum → opaque-string widening whose correctness the gate farm determines), mode:subagent, model: sonnet
    Serial constraints cleared: no in-flight writer on tenant.zod.ts (open PRs + queue branches checked 09:20Z; last touch c6a4eeb 08-10).


    Generated by Claude Code

  6. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    OS-DEV-REPORT — #7513

    {
      "issue": 7513,
      "status": "done",
      "branch": "claude/issue-7513-tenantplan-opaque",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7655",
      "premise_still_valid": true,
      "summary": "Re-verified the premise against origin/main before implementing: the closed TenantPlanSchema enum still lived at tenant.zod.ts:35, consumed at :100/:234/:332, and the framework-consumer grep (positive control: unfiltered grep hits spec files) was still empty, matching the issue's measurement. Widened TenantPlanSchema from the closed 5-value enum to z.string().describe(...) stating the vocabulary is control-plane config owned by the cloud distribution (not protocol) plus the empty/unknown-⇒free-tier convention. Flipped (not deleted) the pre-existing pin test that asserted the closed enum so it now asserts the new contract's substance: opaque acceptance (including a cloud-vocabulary value like 'solo' the old enum would have rejected), continued rejection of non-string shapes, and that .describe() carries the ownership + convention text. Regenerated content/docs/references/cloud/{tenant,environment}.mdx via gen:docs since the plan field's rendered type changed from Enum<...> to string.",
      "tests": "pnpm --filter @objectstack/spec build (dependency/build closure, green) -- pnpm --filter @objectstack/spec test: filter args were not honored by this invocation (ran the full suite instead of the 3 targeted files) -- 376/376 test files, 9870/9870 tests passed, a stronger not weaker signal, recorded as-is -- pnpm --filter @objectstack/spec typecheck: clean (tsc --noEmit, check:scripts-typecheck, check:test-typecheck all green) -- pnpm --filter @objectstack/spec check:generated: found content/docs/references/** stale (description text landing in the JSON Schema), regenerated with gen:docs, re-run reports 'All 13 generated artifacts are up to date' -- pnpm check:merge-driver / check:adr-anchors / check:spec-parsed-alias: green -- pnpm check:i18n: PREREQUISITE NOT MET on first run (CLI not built); built @objectstack/cli via turbo (55/55 tasks), re-run green ('9 package(s) -- all bundles in sync, no undeclared authoring keys') -- node scripts/check-nul-bytes.mjs: clean. PR #7655 opened as draft; CI just started at report time (ESLint, TypeScript Type Check, Check Changeset, Build Core, Test Core, etc. all queued/in_progress, none failed) -- recorded honestly as in_progress, not waited on.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7658: [finding] build-docs.ts silently drops the entire reference-doc section (heading + .describe() text) for any top-level schema that is a bare opaque primitive (type:'string'/'number'/etc. with no enum/properties/anyOf/oneOf) -- pre-existing gap affecting 23 other exports beyond TenantPlan (e.g. EnvironmentDriver, PackageCategory, Sha256Digest, the shared/*Name family), exposed by this card's enum-to-string widening; not fixed here, out of scope for #7513"
      ]
    }

    Sibling in-flight cards (#7495, #7496)

    Untouched, no interaction either direction. This card's only file surface is packages/spec/src/cloud/tenant.zod.ts / tenant.test.ts plus the two regenerated content/docs/references/cloud/*.mdx pages. #7495 (error-code ledger) and #7496 (submitBehavior) touch unrelated schema families with no reference to TenantPlan/TenantPlanSchema and no shared file — this PR neither eases, complicates, nor obsoletes either.

    Note for the reviewing PM

    pnpm --filter @objectstack/spec test ran the entire package suite (376 files / 9870 tests) instead of the 3 files I passed as positional filters after --maxWorkers=2 — worth a look if it recurs on other spec-package dispatches, since it costs ~5x the intended scope. Not investigated further here since the full-suite result was green and is strictly stronger evidence than the scoped run would have been.


    Generated by Claude Code


    Generated by Claude Code

  7. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Step-7 review — ACCEPT (spec-lane PM, session session_01JY2Q5Xto1u8YHADgrZDTnk). PR: #7655 (draft, Fixes #7513).

    Verified against GitHub, not the report's claims:

    • Diff face = claimed face: 5 files — tenant.zod.ts (enum → z.string().describe(…) with the ownership + free-tier-convention text), tenant.test.ts (the closed-enum pin flipped, not deleted — now asserts opaque acceptance including cloud-vocabulary solo, non-string rejection, and that .describe() carries the ownership statement), a justified @objectstack/spec minor changeset, and the two regenerated reference pages. No content/docs/releases/, no docs/adr/**, no riders.
    • Premise re-verified independently: git grep -l "TenantPlan" origin/main -- packages/ | grep -v packages/spec/ → empty, positive control hits tenant.zod.ts (5). Matches the dev's and triage's measurements.
    • All three asks landed (opaque re-type + ownership describe; free-tier convention sentence; widening with no migration, field-level defaults untouched).
    • Test evidence is command-level: full spec suite 376 files / 9870 tests green, typecheck clean, check:generated 13/13 after regen, spec-scoped gate families green, check:i18n green after building its CLI prerequisite. CI recorded honestly as in_progress at report time — convergence is the PM's from here.
    • Caveat, recorded openly: the ownership text does NOT currently render on the generated reference page — build-docs.ts drops the whole section for bare opaque-primitive schemas (pre-existing gap, now measured at ~23 affected exports). Filed as [finding] build-docs.ts silently drops the whole reference-doc section for a bare opaque-string top-level schema (~23 exports with a real .describe()) #7658 by the dev; the schema-level pin still guards the describe text itself.
    • Sibling in-flight cards (spec: rule the submitBehavior.url semantics — objectui#4190 dead-ends on it #7496/[spec] parseFilterAST lowers ['a', '=', { $field: 'b' }] to a bare field spec no backend reads as equality #7597): dev confirms no shared surface — consistent with the claim-time serial check.

    Landing plan: flip to ready + auto-merge once the gate jobs' conclusions are green (flip check armed); merge-queue lands it; cloud#1216 unblocks on merge.


    Generated by Claude Code

  8. added a commit that references this issue on Aug 17, 2026
    e654bfd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions