diff --git a/.changeset/prune-orphan-featureflag-schema.md b/.changeset/prune-orphan-featureflag-schema.md new file mode 100644 index 0000000000..130fd3a9e9 --- /dev/null +++ b/.changeset/prune-orphan-featureflag-schema.md @@ -0,0 +1,26 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec)!: remove the orphaned `FeatureFlagSchema` module (`@objectstack/spec/kernel` feature.zod) + +Follow-through of the capabilities-descriptor prune (#3605). `kernel/feature.zod.ts` +(`FeatureStrategy`, `FeatureFlagSchema`, the `FeatureFlag` factory and its +`FeatureFlag` / `FeatureFlagInput` types) had **zero runtime consumers**, and its +only protocol home — the static `ObjectStackCapabilities.system.features` +descriptor — was itself removed as dead in #3605 (no endpoint ever served it). +The module was a compile-checkable shape with nowhere to go: not authorable +(`defineStack` has no `features` key; strict parsing strips it), not registered, +not read by any engine. + +**Migration — flags are runtime configuration, not authored metadata:** + +| Removed | Live replacement | +| --- | --- | +| `FeatureFlagSchema` / `FeatureFlag.create()` rollout documents (strategies, percentage/group conditions) | the `feature_flags` **settings manifest** (`@objectstack/service-settings`, ADR-0007) — org-tunable `ai_enabled` / `beta_*` toggles, env-overridable via `OS_FEATURE_FLAGS_*` | +| deployment-level capability gating | `PUBLIC_AUTH_FEATURES` registry (`kernel/public-auth-features.ts`) + `requiresFeature` sugar on actions/params (unchanged, live) | +| runtime capability discovery | `GET /api/v1/discovery` (dynamic, declared === enforced) | + +Docs regenerated (`references/kernel/feature.mdx` removed); the platform skill's +Feature Flags section and the hand-written quick-reference row now point at the +settings surface; `PROTOCOL_MAP.md` row dropped. diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx index dd0a03e601..2b6da1c9ea 100644 --- a/content/docs/getting-started/quick-reference.mdx +++ b/content/docs/getting-started/quick-reference.mdx @@ -74,7 +74,6 @@ Plugin architecture, manifests, and kernel runtime. | **[Service Registry](/docs/references/kernel/service-registry)** | `service-registry.zod.ts` | ServiceRegistryConfig, ServiceMetadata | Service discovery | | **[Startup Orchestrator](/docs/references/kernel/startup-orchestrator)** | `startup-orchestrator.zod.ts` | StartupOptions, StartupOrchestrationResult | System startup | | **[Events](/docs/kernel/events)** | `events.zod.ts` | Event, EventBusConfig | System event bus | -| **[Feature](/docs/references/kernel/feature)** | `feature.zod.ts` | FeatureFlag | Feature flags | | **[Metadata Loader](/docs/references/kernel/metadata-loader)** | `metadata-loader.zod.ts` | MetadataLoaderContract | Metadata loading | | **[Package Registry](/docs/references/kernel/package-registry)** | `package-registry.zod.ts` | InstalledPackage, InstallPackageRequest | Package resolution | diff --git a/content/docs/references/kernel/feature.mdx b/content/docs/references/kernel/feature.mdx deleted file mode 100644 index d36ea4dec1..0000000000 --- a/content/docs/references/kernel/feature.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Feature -description: Feature protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -Feature Rollout Strategy - - -**Source:** `packages/spec/src/kernel/feature.zod.ts` - - -## TypeScript Usage - -```typescript -import { FeatureFlag, FeatureStrategy } from '@objectstack/spec/kernel'; -import type { FeatureFlag, FeatureStrategy } from '@objectstack/spec/kernel'; - -// Validate data -const result = FeatureFlag.parse(data); -``` - ---- - -## FeatureFlag - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Feature key (snake_case) | -| **label** | `string` | optional | Display label | -| **description** | `string` | optional | | -| **enabled** | `boolean` | optional | Is globally enabled | -| **strategy** | `Enum<'boolean' \| 'percentage' \| 'user_list' \| 'group' \| 'custom'>` | optional | | -| **conditions** | `{ percentage?: number; users?: string[]; groups?: string[]; expression?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }` | optional | | -| **environment** | `Enum<'dev' \| 'staging' \| 'prod' \| 'all'>` | optional | Environment validity | -| **expiresAt** | `string` | optional | Feature flag expiration date | - - ---- - -## FeatureStrategy - -### Allowed Values - -* `boolean` -* `percentage` -* `user_list` -* `group` -* `custom` - - ---- - diff --git a/content/docs/references/kernel/index.mdx b/content/docs/references/kernel/index.mdx index e0d642a05c..d0576bd6d2 100644 --- a/content/docs/references/kernel/index.mdx +++ b/content/docs/references/kernel/index.mdx @@ -12,7 +12,6 @@ This section contains all protocol schemas for the kernel layer of ObjectStack. - diff --git a/content/docs/references/kernel/meta.json b/content/docs/references/kernel/meta.json index 21bcfe12cf..89f77f1f99 100644 --- a/content/docs/references/kernel/meta.json +++ b/content/docs/references/kernel/meta.json @@ -27,7 +27,6 @@ "context", "dev-plugin", "execution-context", - "feature", "metadata-customization", "metadata-loader", "metadata-persistence", diff --git a/packages/qa/dogfood/test/expression-conformance.ledger.ts b/packages/qa/dogfood/test/expression-conformance.ledger.ts index 683daad319..f00de776f2 100644 --- a/packages/qa/dogfood/test/expression-conformance.ledger.ts +++ b/packages/qa/dogfood/test/expression-conformance.ledger.ts @@ -80,13 +80,15 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ }, { id: 'cel-formula', - summary: 'computed / formula field + mapping / feature expressions', + summary: 'computed / formula field + mapping expressions', dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log', enforcement: '@objectstack/formula celEngine (interpret)', + // kernel/feature.zod.ts:expression was covered here until the orphaned + // FeatureFlagSchema module was removed (zero runtime consumers once its + // capabilities-descriptor home went, #3605). covers: [ 'data/field.zod.ts:expression', 'shared/mapping.zod.ts:expression', - 'kernel/feature.zod.ts:expression', ], }, { diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md index 7021262422..94001cd5c1 100644 --- a/packages/spec/PROTOCOL_MAP.md +++ b/packages/spec/PROTOCOL_MAP.md @@ -192,7 +192,6 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l | [`manifest.zod.ts`](src/kernel/manifest.zod.ts) | ⭐ | **Manifest**. The `package.json` equivalent for the stack. | | [`context.zod.ts`](src/kernel/context.zod.ts) | | **Execution Context**. Request-scoped context (user, transaction). | | [`events.zod.ts`](src/kernel/events.zod.ts) | | **Kernel Events**. System lifecycle events. | -| [`feature.zod.ts`](src/kernel/feature.zod.ts) | | **Feature Flags**. Toggleable system features. | | [`service-registry.zod.ts`](src/kernel/service-registry.zod.ts) | | **Service Registry**. Internal dependency injection. | | [`metadata-loader.zod.ts`](src/kernel/metadata-loader.zod.ts) | | **Loader**. Logic for loading definitions from disk/DB. | | [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) | ⭐ | **Plugin Loading**. Loading strategies, production-safe hot reload (`environment`, `productionSafety`), full plugin sandboxing (`scope`, `ipc`), code splitting, caching, and performance monitoring. | diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 71399a0581..3d22378ed5 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1419,10 +1419,6 @@ "ExecutionContextSchema (const)", "ExtensionPoint (type)", "ExtensionPointSchema (const)", - "FeatureFlag (type)", - "FeatureFlagInput (type)", - "FeatureFlagSchema (const)", - "FeatureStrategy (const)", "FieldChange (type)", "FieldChangeSchema (const)", "GetPackageRequest (type)", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 5451bf8d5b..3c4ddd66b6 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -979,8 +979,6 @@ "kernel/EventWebhookConfig", "kernel/ExecutionContext", "kernel/ExtensionPoint", - "kernel/FeatureFlag", - "kernel/FeatureStrategy", "kernel/FieldChange", "kernel/GetPackageRequest", "kernel/GetPackageResponse", diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index bb0c715cda..520e90cdfa 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -113,7 +113,6 @@ const SKILL_MAP: Record = { 'kernel/plugin-lifecycle-events.zod.ts', 'kernel/plugin-capability.zod.ts', 'kernel/plugin-loading.zod.ts', - 'kernel/feature.zod.ts', 'kernel/metadata-plugin.zod.ts', ], 'objectstack-i18n': [ diff --git a/packages/spec/src/kernel/feature.test.ts b/packages/spec/src/kernel/feature.test.ts deleted file mode 100644 index 332551c0a6..0000000000 --- a/packages/spec/src/kernel/feature.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - FeatureStrategy, - FeatureFlagSchema, - FeatureFlag, - type FeatureFlag as FeatureFlagType, -} from './feature.zod'; - -describe('FeatureStrategy', () => { - it('should accept valid strategies', () => { - expect(() => FeatureStrategy.parse('boolean')).not.toThrow(); - expect(() => FeatureStrategy.parse('percentage')).not.toThrow(); - expect(() => FeatureStrategy.parse('user_list')).not.toThrow(); - expect(() => FeatureStrategy.parse('group')).not.toThrow(); - expect(() => FeatureStrategy.parse('custom')).not.toThrow(); - }); - - it('should reject invalid strategies', () => { - expect(() => FeatureStrategy.parse('random')).toThrow(); - expect(() => FeatureStrategy.parse('')).toThrow(); - }); -}); - -describe('FeatureFlagSchema', () => { - const minimalFlag = { - name: 'dark_mode', - }; - - it('should accept minimal feature flag', () => { - expect(() => FeatureFlagSchema.parse(minimalFlag)).not.toThrow(); - }); - - it('should apply default values', () => { - const parsed = FeatureFlagSchema.parse(minimalFlag); - expect(parsed.enabled).toBe(false); - expect(parsed.strategy).toBe('boolean'); - expect(parsed.environment).toBe('all'); - }); - - it('should accept feature flag with all fields', () => { - const full: FeatureFlagType = { - name: 'new_dashboard', - label: 'New Dashboard', - description: 'Enables the new dashboard UI', - enabled: true, - strategy: 'percentage', - conditions: { - percentage: 50, - users: ['user_1', 'user_2'], - groups: ['beta_testers'], - expression: 'user.plan == "pro"', - }, - environment: 'staging', - expiresAt: '2025-12-31T23:59:59Z', - }; - - const parsed = FeatureFlagSchema.parse(full); - expect(parsed.label).toBe('New Dashboard'); - expect(parsed.conditions?.percentage).toBe(50); - expect(parsed.conditions?.users).toHaveLength(2); - expect(parsed.environment).toBe('staging'); - }); - - it('should reject invalid name (not snake_case)', () => { - expect(() => FeatureFlagSchema.parse({ name: 'DarkMode' })).toThrow(); - expect(() => FeatureFlagSchema.parse({ name: 'dark-mode' })).toThrow(); - expect(() => FeatureFlagSchema.parse({ name: '1invalid' })).toThrow(); - }); - - it('should reject name that is too short', () => { - expect(() => FeatureFlagSchema.parse({ name: 'a' })).toThrow(); - }); - - it('should accept all environment values', () => { - const envs = ['dev', 'staging', 'prod', 'all'] as const; - envs.forEach(environment => { - const parsed = FeatureFlagSchema.parse({ name: 'test_flag', environment }); - expect(parsed.environment).toBe(environment); - }); - }); - - it('should reject invalid environment', () => { - expect(() => FeatureFlagSchema.parse({ - name: 'test_flag', - environment: 'local', - })).toThrow(); - }); - - it('should reject percentage out of range', () => { - expect(() => FeatureFlagSchema.parse({ - name: 'test_flag', - conditions: { percentage: 101 }, - })).toThrow(); - - expect(() => FeatureFlagSchema.parse({ - name: 'test_flag', - conditions: { percentage: -1 }, - })).toThrow(); - }); - - it('should accept percentage at boundaries', () => { - const zero = FeatureFlagSchema.parse({ - name: 'test_flag', - conditions: { percentage: 0 }, - }); - expect(zero.conditions?.percentage).toBe(0); - - const hundred = FeatureFlagSchema.parse({ - name: 'test_flag', - conditions: { percentage: 100 }, - }); - expect(hundred.conditions?.percentage).toBe(100); - }); - - it('should validate expiresAt as ISO datetime', () => { - expect(() => FeatureFlagSchema.parse({ - name: 'test_flag', - expiresAt: 'not-a-date', - })).toThrow(); - - expect(() => FeatureFlagSchema.parse({ - name: 'test_flag', - expiresAt: '2025-06-15T10:00:00Z', - })).not.toThrow(); - }); -}); - -describe('FeatureFlag.create', () => { - it('should return the config object as-is', () => { - const config = { name: 'my_feature', enabled: true }; - const result = FeatureFlag.create(config); - expect(result).toEqual(config); - }); -}); diff --git a/packages/spec/src/kernel/feature.zod.ts b/packages/spec/src/kernel/feature.zod.ts deleted file mode 100644 index 8fea9b4827..0000000000 --- a/packages/spec/src/kernel/feature.zod.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; -import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; -import { ExpressionInputSchema } from '../shared/expression.zod'; - -/** - * Feature Rollout Strategy - */ -import { lazySchema } from '../shared/lazy-schema'; -export const FeatureStrategy = z.enum([ - 'boolean', // Simple On/Off - 'percentage', // Gradual rollout (0-100%) - 'user_list', // Specific users - 'group', // Specific groups/roles - 'custom' // Custom constraint/script -]); - -/** - * Feature Flag Protocol - * - * Manages feature toggles and gradual rollouts. - * Used for CI/CD, A/B Testing, and Trunk-Based Development. - */ -export const FeatureFlagSchema = lazySchema(() => z.object({ - name: SnakeCaseIdentifierSchema.describe('Feature key (snake_case)'), - label: z.string().optional().describe('Display label'), - description: z.string().optional(), - - /** Default state */ - enabled: z.boolean().default(false).describe('Is globally enabled'), - - /** Rollout Strategy */ - strategy: FeatureStrategy.default('boolean'), - - /** Strategy Configuration */ - conditions: z.object({ - percentage: z.number().min(0).max(100).optional(), - users: z.array(z.string()).optional(), - groups: z.array(z.string()).optional(), - expression: ExpressionInputSchema.optional().describe('Custom predicate (CEL).') - }).optional(), - - /** Integration */ - environment: z.enum(['dev', 'staging', 'prod', 'all']).default('all') - .describe('Environment validity'), - - /** Expiration */ - expiresAt: z.string().datetime().optional().describe('Feature flag expiration date'), -})); - -export const FeatureFlag = Object.assign(FeatureFlagSchema, { - create: >(config: T) => config, -}); - -export type FeatureFlag = z.infer; -export type FeatureFlagInput = z.input; diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts index 9ad3186a3f..9eb70a630b 100644 --- a/packages/spec/src/kernel/index.ts +++ b/packages/spec/src/kernel/index.ts @@ -6,7 +6,12 @@ export * from './context.zod'; export * from './dependency-resolution.zod'; export * from './dev-plugin.zod'; export * from './events.zod'; -export * from './feature.zod'; +// feature.zod (FeatureFlagSchema / FeatureStrategy / FeatureFlag factory) was +// REMOVED per ADR-0056 D8: zero runtime consumers, and its only protocol home — +// the static ObjectStackCapabilities.system.features descriptor — was itself +// removed as dead (#3605). Live toggles are the `feature_flags` settings +// manifest (ADR-0007, env-overridable via OS_FEATURE_FLAGS_*) and the +// PUBLIC_AUTH_FEATURES registry (public-auth-features.ts) for auth gates. export * from './manifest.zod'; export * from './metadata-customization.zod'; export * from './namespace-prefix'; diff --git a/packages/spec/src/kernel/public-auth-features.ts b/packages/spec/src/kernel/public-auth-features.ts index abf5dffcdf..b541888fed 100644 --- a/packages/spec/src/kernel/public-auth-features.ts +++ b/packages/spec/src/kernel/public-auth-features.ts @@ -18,10 +18,12 @@ import type { z } from 'zod'; * where each flag's consumption surface, default semantics, and gated spec * inputs (or exemption rationale) are recorded and CI-enforced. * - * NOT to be confused with `kernel/feature.zod.ts` (`FeatureFlagSchema`) — - * that schema models tenant-scoped runtime rollout toggles (strategies, - * expiry, conditions). This registry classifies the fixed, deployment-level - * capability flags that plugin-auth advertises to anonymous clients. + * NOT to be confused with runtime rollout toggles — those live on the + * `feature_flags` settings manifest (ADR-0007), not in the spec (the former + * `kernel/feature.zod.ts` `FeatureFlagSchema` was removed as an orphan: zero + * runtime consumers once its dead capabilities-descriptor home went, #3605). + * This registry classifies the fixed, deployment-level capability flags that + * plugin-auth advertises to anonymous clients. * * Consumers: * - `ui/action.zod.ts` — `requiresFeature` sugar on actions/params is lowered diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 5fcdc83d0f..7f9b18b624 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -1050,43 +1050,21 @@ const metrics = kernel.getPluginMetrics(); ## Feature Flags -Feature flags are a **protocol shape, not a config key**. `FeatureFlagSchema` (from -`@objectstack/spec/kernel`) defines the flag document. There is **no `featureFlags:` / -`features:` key on `defineStack`** — strict parsing silently strips unknown keys, so such -config would be a no-op. (The static `ObjectStackCapabilities.system.features` descriptor -that once carried flags was removed with the dead capabilities-descriptor cluster — no -endpoint ever served it; runtime capability discovery is `GET /api/v1/discovery`.) - - -```typescript -import { FeatureFlag } from '@objectstack/spec/kernel'; - -// FeatureFlag.create() gives you a compile-checked flag value: -const aiCopilot = FeatureFlag.create({ - name: 'experimental_ai_copilot', - label: 'AI Copilot', - enabled: true, - strategy: 'percentage', - conditions: { percentage: 25 }, // 25% of users - environment: 'prod', // 'dev' | 'staging' | 'prod' | 'all' (default 'all') -}); - -const betaKanban = FeatureFlag.create({ - name: 'beta_kanban_view', - label: 'Kanban View', - enabled: true, - strategy: 'group', - conditions: { groups: ['beta_testers'] }, - environment: 'all', -}); -``` - -Strategies: `boolean` | `percentage` | `user_list` | `group` | `custom` - -Looking for live, resolvable toggles today? Those are different surfaces, not -`FeatureFlagSchema`: the `feature_flags` settings manifest -(`@objectstack/service-settings`, env-overridable via `OS_FEATURE_FLAGS_*`) and auth -capability gates (`requiresFeature` → `PUBLIC_AUTH_FEATURES`). +Feature flags are **not a spec/metadata concept**. There is no `featureFlags:` / +`features:` key on `defineStack` (strict parsing silently strips unknown keys), and the +former `FeatureFlagSchema` (`@objectstack/spec/kernel`) was removed — it had zero runtime +consumers, and its only protocol home (the static `ObjectStackCapabilities.system.features` +descriptor) was itself dead: no endpoint ever served it. Runtime capability discovery is +`GET /api/v1/discovery`. + +The live toggle surfaces are runtime configuration, not authored metadata: + +- **`feature_flags` settings manifest** (`@objectstack/service-settings`) — org-tunable + toggles like `ai_enabled` / `beta_*`, resolvable at runtime and env-overridable via + `OS_FEATURE_FLAGS_*` (ADR-0007 settings cascade). +- **Auth capability gates** — `requiresFeature` on actions/params lowers to the + `PUBLIC_AUTH_FEATURES` registry (`kernel/public-auth-features.ts`), the fixed + deployment-level flags plugin-auth advertises to anonymous clients. --- --- diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md index a4226f9376..b39cd4171f 100644 --- a/skills/objectstack-platform/references/_index.md +++ b/skills/objectstack-platform/references/_index.md @@ -12,7 +12,6 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Driver Identifier - `node_modules/@objectstack/spec/src/data/seed.zod.ts` — Seed Import Strategy - `node_modules/@objectstack/spec/src/kernel/context.zod.ts` — Runtime Mode Enum -- `node_modules/@objectstack/spec/src/kernel/feature.zod.ts` — Feature Rollout Strategy - `node_modules/@objectstack/spec/src/kernel/manifest.zod.ts` — Structured permission grants requested by a plugin (ADR-0025 §3.2). - `node_modules/@objectstack/spec/src/kernel/metadata-plugin.zod.ts` — Metadata Plugin Protocol - `node_modules/@objectstack/spec/src/kernel/plugin-capability.zod.ts` — Plugin Capability Protocol