Skip to content

spec: re-type TenantPlan / sys_environment.plan as an opaque plan identifier - #7655

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-7513-tenantplan-opaque
Aug 11, 2026
Merged

spec: re-type TenantPlan / sys_environment.plan as an opaque plan identifier#7655
os-zhuang merged 4 commits into
mainfrom
claude/issue-7513-tenantplan-opaque

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#7513

What

Widens TenantPlanSchema / TenantPlan (packages/spec/src/cloud/tenant.zod.ts) from a closed 5-value enum (free / starter / pro / enterprise / custom) to an opaque plan identifier — any string. The schema's .describe() now states that the vocabulary is control-plane config owned by the cloud distribution, not protocol, and documents the shared convention that an empty/unrecognized value is treated as the free tier by cloud-side readers.

This executes the OPAQUE arm of the maintainer ruling on cloud#1216 (2026-08-10, quoted verbatim in #7513), whose one open condition — "does any reader branch on the plan values?" — has been measured and resolved (cloud seat, 2026-08-11): no reader outside the cloud distribution branches on plan values.

Why this is safe (re-verified against origin/main before implementing)

  • git grep -l "TenantPlan" origin/main -- 'packages/**/*.ts' | grep -v packages/spec/ → empty (positive control: the unfiltered grep hits the spec files). No framework consumer outside packages/spec references the type.
  • packages/cloud-connection/src/runtime-config-plugin.ts is plan-agnostic by design: its only touch is resolvePlanFeatures?: (plan: string | undefined) => …, called as featuresFor(resolved.plan, features) — a type check (typeof resolved.plan === 'string'), never a value branch.
  • The only places that DO branch on specific plan values (isFreePlan, planAllowsAiStudio, etc.) live in the cloud distribution's own entitlement modules — exactly "control-plane config," per the ruling.

Scope

  1. TenantPlanSchemaz.string().describe(…) with the ownership statement.
  2. Kept the one-sentence empty/unknown ⇒ free-tier convention in the describe (not enforced by the schema itself — cloud-side readers own that normalization).
  3. This is a pure widening (the old enum's values are a strict subset of "any string"); no runtime parser depended on the closed set, so no migration is needed. Field-level .default('free') usages (TenantDatabaseSchema.plan, ProvisionTenantRequestSchema.plan, EnvironmentSchema.plan) are unchanged.
  4. The pre-existing pin test asserting the closed enum (packages/spec/src/cloud/tenant.test.ts) is flipped, not deleted — it now asserts the new contract's substance: opaque acceptance (including a cloud-vocabulary value the old enum would have rejected, e.g. solo), continued rejection of non-string shapes, and that the .describe() carries the ownership + convention statements.
  5. Regenerated content/docs/references/cloud/{tenant,environment}.mdx via pnpm --filter @objectstack/spec gen:docs (the plan field type changed from Enum<'free' | …> to string in both).

Out of scope (belongs to cloud#1216, downstream, blocked on this landing)

Dropping the cloud repo's hand-written BillingPlan union's divergence from spec — that stays in cloud#1216 per the ruling.

Tests

  • pnpm --filter @objectstack/spec test: 376/376 test files, 9870/9870 tests passed (a filter-argument quirk ran the full suite rather than just the 3 targeted files; recorded as-is since it is a stronger, not weaker, signal).
  • pnpm --filter @objectstack/spec typecheck: clean (tsc --noEmit, check:scripts-typecheck, check:test-typecheck all pass).
  • pnpm --filter @objectstack/spec check:generated: found 1 stale artifact (content/docs/references/**, from the description text landing in the JSON Schema), regenerated with gen:docs; re-run green (all 13 artifacts up to date).
  • pnpm check:merge-driver, pnpm check:adr-anchors, pnpm check:spec-parsed-alias: green.
  • pnpm check:i18n: green, after building @objectstack/cli (its own prerequisite, unrelated to this change — the gate refuses to run against an unbuilt CLI).
  • node scripts/check-nul-bytes.mjs: clean.

Generated by Claude Code

…ntifier
Widens `TenantPlanSchema` from a closed 5-value enum (free/starter/pro/
enterprise/custom) to an opaque string. The vocabulary is control-plane
config owned by the cloud distribution, not protocol -- the schema's
.describe() now states that ownership and the shared empty/unknown => free
tier convention.
Executes the OPAQUE arm of the maintainer ruling on cloud#1216
(2026-08-10), whose one open condition -- "does any reader branch on plan
values?" -- was measured (cloud seat, 2026-08-11) and resolved: no reader
outside the cloud distribution branches on plan values. Re-verified against
origin/main before implementing (empty framework-consumer grep, positive
control hits spec files).
Pure widening: every value the old enum accepted is still accepted, no
runtime parser depended on the closed set. The pre-existing pin test
asserting the closed enum is flipped, not deleted, to assert the new
contract's substance (opaque acceptance including a cloud-vocabulary value
the old enum rejected, continued rejection of non-string shapes, and the
describe() carrying the ownership + convention statements).
Regenerates content/docs/references/cloud/{tenant,environment}.mdx via
`pnpm --filter @objectstack/spec gen:docs` (plan field type: Enum<...> ->
string).
Fixes#7513
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 11, 2026 10:15am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Pre-sync for the merge queue (PM instruction on #7513): the os-regen merge
driver exits 0 with zero conflict markers while silently able to drop one
side, so force every merge=os-regen path (.gitattributes) to origin/main's
exact content rather than trust the driver's merge output. Only
content/docs/references/cloud/{tenant,environment}.mdx differed (this
branch's own regenerated docs) -- everything else already matched
origin/main byte-for-byte. The full gen pipeline regenerates the two
touched docs pages from source next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
Full gen pipeline run post-merge (pnpm --filter @objectstack/spec build,
then gen:docs) reproduces this branch's tenant/environment reference-doc
changes from source, confirming check:generated is clean against the
merged tree (all 13 artifacts up to date).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude