Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"agent": "03",
|
||||
"title": "Plan permission contract",
|
||||
"status": "design_only",
|
||||
"version": "1.0.0",
|
||||
"coordinates_with": [
|
||||
"docs/plan-permissions/01-dashboard-feature-catalog.md",
|
||||
"docs/plan-permissions/01-feature-keys.json",
|
||||
"docs/plan-permissions/02-plans-permissions-current.md",
|
||||
"docs/plan-permissions/02-extension-points.json"
|
||||
],
|
||||
"assumptions": [
|
||||
"Package equals plans row; no separate packages table",
|
||||
"is_custom=true (including seeded Enterprise) defaults all features ON unless override false",
|
||||
"Meters (max_products, credits, can_use_ai) remain; feature keys gate surfaces/actions",
|
||||
"Client canFeature fail-open when features map missing until API cutover"
|
||||
],
|
||||
"runtime_formula": {
|
||||
"effective": "plan_allows(key) AND global_section_enabled(section(key)) AND global_feature_enabled(key)",
|
||||
"plan_allows": [
|
||||
"if key in plans.features -> plans.features[key]",
|
||||
"else if plans.is_custom -> true",
|
||||
"else -> DefaultMatrix[normalizePlanName(name)][key] (unknown -> false unless catalog All-plans)"
|
||||
],
|
||||
"global_missing_default": true,
|
||||
"parent_prefix_rule": "UI treats disabled parent as disabling children; Assert uses specific action key"
|
||||
},
|
||||
"data_model": {
|
||||
"plans_features_column": {
|
||||
"table": "plans",
|
||||
"column": "features",
|
||||
"type": "JSONB NOT NULL DEFAULT '{}'",
|
||||
"semantics": "sparse overrides only; not full expanded matrix"
|
||||
},
|
||||
"platform_feature_gates": {
|
||||
"table": "platform_feature_gates",
|
||||
"columns": {
|
||||
"gate_key": "TEXT PRIMARY KEY",
|
||||
"kind": "TEXT CHECK IN ('section','feature')",
|
||||
"enabled": "BOOLEAN NOT NULL DEFAULT true",
|
||||
"updated_at": "TIMESTAMPTZ",
|
||||
"updated_by": "UUID NULL REFERENCES users(id)"
|
||||
},
|
||||
"section_keys": [
|
||||
"shell",
|
||||
"dashboard",
|
||||
"catalog",
|
||||
"feeds",
|
||||
"stores",
|
||||
"processing",
|
||||
"marketing",
|
||||
"integrations",
|
||||
"billing",
|
||||
"settings",
|
||||
"support",
|
||||
"capabilities"
|
||||
]
|
||||
},
|
||||
"do_not_add": [
|
||||
"packages table",
|
||||
"parallel permissions package outside billing",
|
||||
"v1 company-level feature overrides"
|
||||
]
|
||||
},
|
||||
"feature_key_namespace": {
|
||||
"authority": "docs/plan-permissions/01-feature-keys.json",
|
||||
"pattern": "section.leaf(.subleaf)* snake_case",
|
||||
"capability_prefix": "capability.",
|
||||
"exclude": [
|
||||
"/admin/*"
|
||||
]
|
||||
},
|
||||
"default_matrix_sketch": {
|
||||
"custom_and_enterprise_is_custom": "all_true_unless_override",
|
||||
"free_denies": [
|
||||
"catalog.products.process_ai_titles",
|
||||
"catalog.products.process_ai_descriptions",
|
||||
"marketing.campaigns.generate_ai",
|
||||
"marketing.campaigns.send",
|
||||
"marketing.brand_ai_apply",
|
||||
"marketing.seo.ai_rewrite",
|
||||
"integrations.ai.byok",
|
||||
"settings.api_keys",
|
||||
"capability.ai_processing",
|
||||
"capability.campaign_ai",
|
||||
"capability.email_live_send",
|
||||
"capability.brand_ai_apply",
|
||||
"capability.seo_ai_rewrite",
|
||||
"capability.api_access",
|
||||
"capability.byok"
|
||||
],
|
||||
"starter_plus_allows_ai_surfaces": true,
|
||||
"growth_byok_default": true,
|
||||
"all_plans_core": [
|
||||
"shell.navigation",
|
||||
"dashboard.overview",
|
||||
"catalog.products",
|
||||
"catalog.products.process_categories",
|
||||
"catalog.products.process_attributes",
|
||||
"feeds.list",
|
||||
"feeds.export_feeds",
|
||||
"stores.hub",
|
||||
"processing.monitor",
|
||||
"billing.overview",
|
||||
"settings.profile",
|
||||
"support.center",
|
||||
"capability.normalize_specs_fill",
|
||||
"capability.eprel",
|
||||
"capability.sku_cap",
|
||||
"capability.ai_credits"
|
||||
],
|
||||
"notes": [
|
||||
"Expand leaves from 01 catalog; parent deny implies children deny for UI",
|
||||
"capability.sku_cap / ai_credits enable hooks; numeric limits stay on plans meters"
|
||||
]
|
||||
},
|
||||
"api": {
|
||||
"admin_plans": {
|
||||
"list": "GET /api/admin/plans",
|
||||
"upsert": "POST /api/admin/plans",
|
||||
"plan_fields_added": [
|
||||
"features",
|
||||
"resolved_features"
|
||||
],
|
||||
"features_write_semantics": "replace stored overrides object"
|
||||
},
|
||||
"admin_feature_gates": {
|
||||
"list": "GET /api/admin/feature-gates",
|
||||
"put": "PUT /api/admin/feature-gates",
|
||||
"put_section": "PUT /api/admin/feature-gates/sections/{section}",
|
||||
"auth": "RequirePlatformAdmin",
|
||||
"response_shape": {
|
||||
"sections": {
|
||||
"marketing": true
|
||||
},
|
||||
"features": {
|
||||
"capability.byok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_capabilities": {
|
||||
"embed_in_credits_overview": true,
|
||||
"endpoints": [
|
||||
"GET /api/billing/credits",
|
||||
"GET /api/auth/me",
|
||||
"GET /api/billing/capabilities"
|
||||
],
|
||||
"credits_overview_fields_added": [
|
||||
"features",
|
||||
"disabled_features",
|
||||
"feature_etag"
|
||||
],
|
||||
"capabilities_response": {
|
||||
"plan_id": "int64",
|
||||
"plan_name": "string",
|
||||
"is_custom": "bool",
|
||||
"features": "map[string]bool effective full registry",
|
||||
"sections": "map[string]bool global section enables",
|
||||
"entitlements": {
|
||||
"can_use_ai": "bool",
|
||||
"can_use_eprel": "bool",
|
||||
"is_free_plan": "bool",
|
||||
"is_paid_plan": "bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enforcement_error": {
|
||||
"http_status": 402,
|
||||
"body": {
|
||||
"error": "feature_disabled",
|
||||
"code": "plan_gate",
|
||||
"feature": "marketing.campaigns.generate_ai",
|
||||
"upgrade_url": "/plans"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend_symbols_to_extend": {
|
||||
"reuse": [
|
||||
"apps/api/internal/billing/entitlements.go#EntitlementsForCompany",
|
||||
"apps/api/internal/billing/entitlements.go#ComputeEntitlements",
|
||||
"apps/api/internal/billing/service.go#Plan",
|
||||
"apps/api/internal/billing/service.go#CreditsOverview",
|
||||
"apps/api/internal/billing/service.go#UpsertPlan",
|
||||
"apps/api/internal/billing/service.go#EnsureDefaultPlans",
|
||||
"apps/api/internal/billing/service.go#AssertCanStartProcessing",
|
||||
"apps/api/internal/httpapi/billing_handlers.go",
|
||||
"apps/web/src/routes/admin/billing/+page.svelte",
|
||||
"apps/web/src/lib/billing-display.ts",
|
||||
"apps/web/src/lib/plan-gates.ts",
|
||||
"apps/web/src/lib/components/Nav.svelte"
|
||||
],
|
||||
"new_symbols": [
|
||||
"DefaultPlanFeatures",
|
||||
"ResolveFeatures",
|
||||
"FeaturesForCompany",
|
||||
"AssertFeature",
|
||||
"canFeature"
|
||||
]
|
||||
},
|
||||
"migration": {
|
||||
"file_suggestion": "apps/api/sql/schema/026_plan_features.sql",
|
||||
"idempotent": true,
|
||||
"destructive": false,
|
||||
"steps": [
|
||||
"ALTER TABLE plans ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '{}'",
|
||||
"CREATE TABLE IF NOT EXISTS platform_feature_gates (...)",
|
||||
"EnsureDefaultPlans must not clobber non-empty features",
|
||||
"Migrator inserts '{}' when source lacks column"
|
||||
]
|
||||
},
|
||||
"frontend_gating": {
|
||||
"helper": "canFeature(credits, key)",
|
||||
"nav_filter": "Nav.svelte primaryItems + moreItemsAll by parent feature_key",
|
||||
"fail_open_until_cutover": true,
|
||||
"optional_env": "FEATURES_ENFORCE=1"
|
||||
},
|
||||
"breaking_changes": [],
|
||||
"breaking_changes_policy": "prefer_none_additive_only",
|
||||
"preserve_contracts": [
|
||||
"can_use_ai",
|
||||
"can_use_eprel",
|
||||
"is_free_plan",
|
||||
"is_paid_plan",
|
||||
"public plan names Free/Starter/Growth/Business/Enterprise",
|
||||
"Stripe checkout public-ladder-only",
|
||||
"402 plan_gate for existing credit/SKU errors"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user