diff --git a/.changeset/remove-capability-aliases-3308.md b/.changeset/remove-capability-aliases-3308.md new file mode 100644 index 0000000000..690bf6d9f1 --- /dev/null +++ b/.changeset/remove-capability-aliases-3308.md @@ -0,0 +1,22 @@ +--- +"@objectstack/spec": minor +"@objectstack/cli": minor +--- + +feat(spec)!: remove deprecated `aiStudio`/`aiSeat` capability aliases (#3308) + +**BREAKING** (shipped as minor per the launch-window convention). The one-cycle +deprecation window from #3265 is over: the legacy camelCase `requires` spellings +`aiStudio`/`aiSeat` are no longer canonicalized to `ai-studio`/`ai-seat` — they +are now plain unknown tokens, rejected by `defineStack` like any other typo. + +- Removed exports `DEPRECATED_PLATFORM_CAPABILITY_ALIASES` and + `canonicalizePlatformCapability` from `@objectstack/spec`; `isKnownPlatformCapability` + no longer canonicalizes. +- `defineStack` no longer rewrites aliases (the `canonicalizeStackRequires` pass + is gone); the serve resolver no longer canonicalizes raw-artifact `requires`. + +Migration: use the canonical kebab-case tokens `ai-studio` / `ai-seat`. All +first-party configs were migrated in #862/#863; only stacks still carrying the +legacy spelling are affected. Cloud's `objectos-runtime` (pinned to an older +framework) follows on its next `.framework-sha` bump. diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 04c39114b3..eda395db9d 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -3,7 +3,7 @@ # Metadata protocol upgrade guide -Current protocol: **15.0.0** · chain support floor: **protocol 10** · generated from the ADR-0087 registries (`@objectstack/spec` `conversions/` + `migrations/`). +Current protocol: **16.0.0** · chain support floor: **protocol 10** · generated from the ADR-0087 registries (`@objectstack/spec` `conversions/` + `migrations/`). ## How to upgrade — from any past major @@ -104,6 +104,16 @@ Protocol 15 unified the conditional-visibility predicate under `visibleWhen` (AD - Why not automatic: The `.strict()` flip (ADR-0089 D3a) turns a previously silently-stripped unknown key into a parse error. There is no mapping target for an arbitrary unknown key — auto-deleting it would be exactly the silent data loss ADR-0078 bans — so each occurrence needs the author to decide: fix the typo, move it to the right layer, or delete dead metadata. - Done when: `objectstack validate` passes with no unknown-key parse errors on form fields, form sections, or page components. +## Protocol 15 → 16 + +Protocol 16 flipped `DashboardWidgetSchema` to `.strict()` (framework#3251, ADR-0021 endpoint): an undeclared top-level widget key is now a loud parse error instead of a silent strip (ADR-0049 enforce-or-remove, ADR-0078 no-silently-inert). The inline analytics shape it most often catches (`object`+`categoryField`+`valueField`+`aggregate`, pivot `rowField`/`columnField`) was already removed at protocol 9, so no mechanical rewrite applies; the residue is the strictness itself, delegated to the author because an arbitrary unknown key has no lossless canonical target. + +### Semantic (delegated to you, with acceptance criteria) + +- **`dashboard-widget-strict-unknown-keys`** — `dashboard widgets (undeclared top-level keys — legacy inline analytics, objectui-internal `component`/`data`, or typos)` → declared keys only (`dataset` + `dimensions` + `values` for analytics; `options` for renderer-specific extras) + - Why not automatic: The `.strict()` flip turns a previously silently-stripped unknown key into a parse error. There is no mapping target for an arbitrary unknown key — auto-deleting it would be exactly the silent data loss ADR-0078 bans — so each occurrence needs the author to decide: bind a `dataset` and select `dimensions`/`values`, move a renderer setting under `options`, or delete the dead key. + - Done when: `objectstack validate` passes with no unknown-key parse errors on dashboard widgets. + --- *Machine-readable equivalents: `spec-changes.json` (shipped in `@objectstack/spec` and attached to each GitHub Release) and the structured output of `objectstack migrate meta --json`.* diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index ce770599bb..f87ebd96c4 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -10,7 +10,7 @@ import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js'; import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js'; import { resolveDriverType, createStorageDriver, UnsupportedDriverError } from '../utils/storage-driver.js'; import { readEnvWithDeprecation, resolveMultiOrgEnabled, resolveAllowDegradedTenancy, isMcpServerEnabled, resolveSearchPinyinEnabled, isModuleNotFoundError } from '@objectstack/types'; -import { PLATFORM_CAPABILITY_TOKENS, canonicalizePlatformCapability } from '@objectstack/spec/kernel'; +import { PLATFORM_CAPABILITY_TOKENS } from '@objectstack/spec/kernel'; import { resolveObjectStackHome } from '@objectstack/runtime'; import { LOG_LEVELS, resolveLogLevel, readLogLevelEnv } from '../utils/log-level.js'; import { @@ -695,22 +695,14 @@ export default class Serve extends Command { // instance wins over the auto-loader). const presetName = flags.preset ?? (isDev ? 'default' : 'default'); const presetTiers = Serve.TIER_PRESETS[presetName] ?? Serve.TIER_PRESETS.default; + // Dedupe `requires` (Set keeps first-seen order). The deprecated + // `aiStudio`/`aiSeat` alias canonicalization was removed in framework#3308 + // — legacy spellings are now unknown tokens (warned below, rejected at + // authoring by defineStack). const rawRequires: string[] = Array.isArray((config as any).requires) ? (config as any).requires.filter((c: unknown) => typeof c === 'string') : []; - // Canonicalize deprecated capability spellings (aiStudio → ai-studio, …) - // so artifacts compiled by an older spec keep booting; `defineStack` - // already rewrites + warns on the source path, this covers raw artifact - // JSON (framework#3265). Dedupe after mapping (Set keeps first-seen order). - const requires: string[] = [...new Set(rawRequires.map((c: string) => { - const canonical = canonicalizePlatformCapability(c); - if (canonical !== c) { - console.warn(chalk.yellow( - ` ⚠ requires: '${c}' is a deprecated spelling — use '${canonical}' (framework#3265)`, - )); - } - return canonical; - }))]; + const requires: string[] = [...new Set(rawRequires)]; // Snapshot the app's EXPLICIT capability declarations BEFORE the platform // appends its own convenience defaults (auth→email, mcp, pinyin-search, // ALWAYS_ON_CAPABILITIES, queue/job). Only these explicit declarations carry diff --git a/packages/cli/test/serve-capability-vocabulary.test.ts b/packages/cli/test/serve-capability-vocabulary.test.ts index ed17d4e4ed..3de565debe 100644 --- a/packages/cli/test/serve-capability-vocabulary.test.ts +++ b/packages/cli/test/serve-capability-vocabulary.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { - PLATFORM_CAPABILITY_TOKENS, - DEPRECATED_PLATFORM_CAPABILITY_ALIASES, -} from '@objectstack/spec/kernel'; +import { PLATFORM_CAPABILITY_TOKENS } from '@objectstack/spec/kernel'; import Serve from '../src/commands/serve.js'; // framework#3265 — drift guard: the serve path's provider registries must stay @@ -22,8 +19,8 @@ describe('serve capability registries vs spec vocabulary (#3265)', () => { } }); - it('registries use only canonical spellings — never deprecated aliases', () => { - const legacy = Object.keys(DEPRECATED_PLATFORM_CAPABILITY_ALIASES); + it('registries use only canonical spellings — never the removed camelCase aliases (#3308)', () => { + const legacy = ['aiStudio', 'aiSeat']; for (const token of [...Object.keys(Serve.CAPABILITY_PROVIDERS), ...Object.keys(Serve.CAPABILITY_TO_TIER)]) { expect(legacy).not.toContain(token); } diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 850c2e1424..bc78f78064 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -26,7 +26,6 @@ "ConversionFixture (interface)", "ConversionNotice (interface)", "CronExpressionInputSchema (const)", - "DEPRECATED_PLATFORM_CAPABILITY_ALIASES (const)", "DatasourceMappingRule (type)", "DatasourceMappingRuleSchema (const)", "DefineStackOptions (interface)", @@ -102,7 +101,6 @@ "applyConversions (function)", "applyConversionsToFlow (function)", "applyMetaMigrations (function)", - "canonicalizePlatformCapability (function)", "cel (function)", "collectConversionNotices (function)", "composeMigrationChain (function)", @@ -1294,7 +1292,6 @@ "CustomizationPolicy (type)", "CustomizationPolicySchema (const)", "DEFAULT_METADATA_TYPE_REGISTRY (const)", - "DEPRECATED_PLATFORM_CAPABILITY_ALIASES (const)", "DeadLetterQueueEntry (type)", "DeadLetterQueueEntrySchema (const)", "DependencyConflict (type)", @@ -1750,7 +1747,6 @@ "VersionConstraint (type)", "VersionConstraintSchema (const)", "VulnerabilitySeverity (type)", - "canonicalizePlatformCapability (function)", "deriveNamespaceFromPackageId (function)", "evaluateLockForDelete (function)", "evaluateLockForWrite (function)", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index ee99fca03b..75838db21a 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -1,11 +1,11 @@ { "$comment": "GENERATED (ADR-0087 D4) — do not edit. Regenerate with: pnpm --filter @objectstack/spec gen:spec-changes. A projection of the D2 conversion table + D3 migration chain; the upgrade guide and the MCP spec_changes tool derive from this same data.", - "protocolVersion": "15.0.0", + "protocolVersion": "16.0.0", "supportFloor": 10, "migrateCommand": "objectstack migrate meta --from (N >= 10)", "aggregate": { "from": 10, - "to": 15, + "to": 16, "added": [], "converted": [ { @@ -132,6 +132,13 @@ "migrationId": "ui-schemas-strict-unknown-keys", "toMajor": 15, "rationale": "The `.strict()` flip (ADR-0089 D3a) turns a previously silently-stripped unknown key into a parse error. There is no mapping target for an arbitrary unknown key — auto-deleting it would be exactly the silent data loss ADR-0078 bans — so each occurrence needs the author to decide: fix the typo, move it to the right layer, or delete dead metadata." + }, + { + "surface": "dashboard widgets (undeclared top-level keys — legacy inline analytics, objectui-internal `component`/`data`, or typos)", + "replacement": "declared keys only (`dataset` + `dimensions` + `values` for analytics; `options` for renderer-specific extras)", + "migrationId": "dashboard-widget-strict-unknown-keys", + "toMajor": 16, + "rationale": "The `.strict()` flip turns a previously silently-stripped unknown key into a parse error. There is no mapping target for an arbitrary unknown key — auto-deleting it would be exactly the silent data loss ADR-0078 bans — so each occurrence needs the author to decide: bind a `dataset` and select `dimensions`/`values`, move a renderer setting under `options`, or delete the dead key." } ], "removed": [] @@ -307,6 +314,22 @@ } ], "removed": [] + }, + { + "from": 15, + "to": 16, + "added": [], + "converted": [], + "migrated": [ + { + "surface": "dashboard widgets (undeclared top-level keys — legacy inline analytics, objectui-internal `component`/`data`, or typos)", + "replacement": "declared keys only (`dataset` + `dimensions` + `values` for analytics; `options` for renderer-specific extras)", + "migrationId": "dashboard-widget-strict-unknown-keys", + "toMajor": 16, + "rationale": "The `.strict()` flip turns a previously silently-stripped unknown key into a parse error. There is no mapping target for an arbitrary unknown key — auto-deleting it would be exactly the silent data loss ADR-0078 bans — so each occurrence needs the author to decide: bind a `dataset` and select `dimensions`/`values`, move a renderer setting under `options`, or delete the dead key." + } + ], + "removed": [] } ] } diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index f1766e599d..687a31c831 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -123,8 +123,6 @@ export { type PluginContext } from './kernel/plugin.zod'; // Platform SERVICE capability vocabulary for `requires: [...]` (framework#3265). export { PLATFORM_CAPABILITY_TOKENS, - DEPRECATED_PLATFORM_CAPABILITY_ALIASES, - canonicalizePlatformCapability, isKnownPlatformCapability, } from './kernel/platform-capabilities'; diff --git a/packages/spec/src/kernel/platform-capabilities.test.ts b/packages/spec/src/kernel/platform-capabilities.test.ts index c93ef9eef0..c4f708214c 100644 --- a/packages/spec/src/kernel/platform-capabilities.test.ts +++ b/packages/spec/src/kernel/platform-capabilities.test.ts @@ -1,13 +1,12 @@ import { describe, expect, it } from 'vitest'; import { PLATFORM_CAPABILITY_TOKENS, - DEPRECATED_PLATFORM_CAPABILITY_ALIASES, - canonicalizePlatformCapability, isKnownPlatformCapability, } from './platform-capabilities'; // framework#3265 — one capability vocabulary across the standalone serve path -// and cloud's objectos-runtime loader, canonical spelling kebab-case. +// and cloud's objectos-runtime loader, canonical spelling kebab-case. The +// deprecated `aiStudio`/`aiSeat` aliases were removed in #3308. describe('PLATFORM_CAPABILITY_TOKENS', () => { it('is frozen and duplicate-free', () => { @@ -27,37 +26,23 @@ describe('PLATFORM_CAPABILITY_TOKENS', () => { } }); - it('contains no deprecated spellings', () => { - for (const legacy of Object.keys(DEPRECATED_PLATFORM_CAPABILITY_ALIASES)) { + it('contains no camelCase legacy spellings (aliases removed, #3308)', () => { + for (const legacy of ['aiStudio', 'aiSeat']) { expect(PLATFORM_CAPABILITY_TOKENS).not.toContain(legacy); } }); }); -describe('DEPRECATED_PLATFORM_CAPABILITY_ALIASES / canonicalizePlatformCapability', () => { - it('maps every alias onto a token that exists in the vocabulary', () => { - for (const canonical of Object.values(DEPRECATED_PLATFORM_CAPABILITY_ALIASES)) { - expect(PLATFORM_CAPABILITY_TOKENS).toContain(canonical); - } - }); - - it('canonicalizes the legacy cloud spellings', () => { - expect(canonicalizePlatformCapability('aiStudio')).toBe('ai-studio'); - expect(canonicalizePlatformCapability('aiSeat')).toBe('ai-seat'); - }); - - it('is identity for canonical and unknown tokens', () => { - expect(canonicalizePlatformCapability('ai-studio')).toBe('ai-studio'); - expect(canonicalizePlatformCapability('automation')).toBe('automation'); - expect(canonicalizePlatformCapability('not-a-capability')).toBe('not-a-capability'); - }); -}); - describe('isKnownPlatformCapability', () => { - it('accepts canonical tokens and deprecated aliases, rejects unknowns', () => { + it('accepts canonical tokens verbatim', () => { expect(isKnownPlatformCapability('ai-studio')).toBe(true); - expect(isKnownPlatformCapability('aiStudio')).toBe(true); + expect(isKnownPlatformCapability('ai-seat')).toBe(true); expect(isKnownPlatformCapability('governance')).toBe(true); + }); + + it('rejects the removed camelCase aliases and typos (no canonicalization, #3308)', () => { + expect(isKnownPlatformCapability('aiStudio')).toBe(false); + expect(isKnownPlatformCapability('aiSeat')).toBe(false); expect(isKnownPlatformCapability('automations')).toBe(false); }); }); diff --git a/packages/spec/src/kernel/platform-capabilities.ts b/packages/spec/src/kernel/platform-capabilities.ts index 51386da529..4ebfe55b52 100644 --- a/packages/spec/src/kernel/platform-capabilities.ts +++ b/packages/spec/src/kernel/platform-capabilities.ts @@ -12,10 +12,10 @@ * ADR-0066 authorization capabilities declared in `capabilities: [...]`.) * * Canonical spelling is lower-case kebab-case (`ai-studio`, `pinyin-search`, - * `hierarchy-security`). Legacy camelCase spellings that shipped in cloud - * configs are mapped through {@link DEPRECATED_PLATFORM_CAPABILITY_ALIASES} - * for one deprecation cycle — `defineStack` normalizes them with a warning at - * authoring time, and runtimes canonicalize raw artifact input the same way. + * `hierarchy-security`). The legacy camelCase spellings (`aiStudio` / `aiSeat`) + * that shipped transitionally were honored as deprecated aliases for one cycle + * (framework#3265) and have been REMOVED (framework#3308) — they are now plain + * unknown tokens, rejected by `defineStack` like any other typo. * * Growing the platform: when a new `requires`-resolvable service ships, add * its token HERE as well as to the runtime's provider registry — the CLI's @@ -66,30 +66,10 @@ export const PLATFORM_CAPABILITY_TOKENS: readonly string[] = Object.freeze([ ]); /** - * Deprecated `requires` spellings → canonical token. One deprecation cycle: - * authoring (`defineStack`) rewrites these with a warning; runtimes accept - * them via {@link canonicalizePlatformCapability} so artifacts built by an - * older spec keep booting. Do not add new aliases — new tokens ship in - * canonical kebab-case only. - */ -export const DEPRECATED_PLATFORM_CAPABILITY_ALIASES: Readonly> = - Object.freeze({ - aiStudio: 'ai-studio', - aiSeat: 'ai-seat', - }); - -/** - * Map a `requires` token to its canonical spelling (identity for tokens that - * are already canonical or unknown). - */ -export function canonicalizePlatformCapability(token: string): string { - return DEPRECATED_PLATFORM_CAPABILITY_ALIASES[token] ?? token; -} - -/** - * True when the token (after alias canonicalization) is part of the platform - * capability vocabulary. + * True when the token is part of the platform capability vocabulary. There is + * no longer any alias canonicalization (framework#3308) — a token is known iff + * it appears verbatim in {@link PLATFORM_CAPABILITY_TOKENS}. */ export function isKnownPlatformCapability(token: string): boolean { - return PLATFORM_CAPABILITY_TOKENS.includes(canonicalizePlatformCapability(token)); + return PLATFORM_CAPABILITY_TOKENS.includes(token); } diff --git a/packages/spec/src/stack-requires.test.ts b/packages/spec/src/stack-requires.test.ts index 560a24426a..033814b0f7 100644 --- a/packages/spec/src/stack-requires.test.ts +++ b/packages/spec/src/stack-requires.test.ts @@ -1,30 +1,16 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { defineStack } from './stack.zod'; -// framework#3265 — defineStack canonicalizes deprecated `requires` spellings at -// the PRODUCER (authoring time) and validates tokens against the platform -// capability vocabulary: deprecated aliases are rewritten with a warning, an -// unknown token is a hard error (no runtime provides it → declared ≠ enforced). +// framework#3265/#3308 — defineStack validates `requires` tokens against the +// platform capability vocabulary at the PRODUCER (authoring time): an unknown +// token is a hard error (no runtime provides it → declared ≠ enforced). The +// deprecated `aiStudio`/`aiSeat` aliases were removed in #3308, so those now +// reject like any other typo — there is no canonicalization step left. -afterEach(() => { - vi.restoreAllMocks(); -}); - -describe('defineStack requires canonicalization (#3265)', () => { - it('rewrites deprecated aliases to canonical kebab tokens with a warning', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const stack = defineStack({ requires: ['ai', 'aiStudio', 'automation'] }); - expect(stack.requires).toEqual(['ai', 'ai-studio', 'automation']); - expect(warn).toHaveBeenCalledTimes(1); - expect(warn.mock.calls[0][0]).toContain("'aiStudio' is a deprecated spelling"); - expect(warn.mock.calls[0][0]).toContain("'ai-studio'"); - }); - - it('canonical declarations pass through untouched and warning-free', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const stack = defineStack({ requires: ['ai', 'ai-studio', 'hierarchy-security', 'governance'] }); - expect(stack.requires).toEqual(['ai', 'ai-studio', 'hierarchy-security', 'governance']); - expect(warn).not.toHaveBeenCalled(); +describe('defineStack requires validation (#3265/#3308)', () => { + it('canonical declarations pass through untouched', () => { + const stack = defineStack({ requires: ['ai', 'ai-studio', 'ai-seat', 'hierarchy-security', 'governance'] }); + expect(stack.requires).toEqual(['ai', 'ai-studio', 'ai-seat', 'hierarchy-security', 'governance']); }); it('THROWS on an unknown token (a typo no runtime provides), naming it', () => { @@ -33,6 +19,15 @@ describe('defineStack requires canonicalization (#3265)', () => { ); }); + it('the removed camelCase aliases now REJECT like any other unknown token (#3308)', () => { + expect(() => defineStack({ requires: ['aiStudio'] })).toThrowError( + /'aiStudio' is not a known platform capability/, + ); + expect(() => defineStack({ requires: ['aiSeat'] })).toThrowError( + /'aiSeat' is not a known platform capability/, + ); + }); + it('reports every distinct unknown token but not known ones', () => { let msg = ''; try { @@ -46,23 +41,8 @@ describe('defineStack requires canonicalization (#3265)', () => { expect(msg).not.toContain("'ai' is not"); // known token isn't flagged }); - it('a deprecated alias is NOT treated as unknown — it canonicalizes and passes', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - expect(() => defineStack({ requires: ['aiStudio', 'ai-seat'] })).not.toThrow(); - // aiStudio → ai-studio (warned); ai-seat is already canonical - expect(warn).toHaveBeenCalledTimes(1); - }); - - it('warns once per distinct token, not once per occurrence', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - defineStack({ requires: ['aiStudio', 'aiStudio'] }); - expect(warn).toHaveBeenCalledTimes(1); - }); - - it('non-strict mode skips canonicalization and warnings by contract', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it('non-strict mode skips validation by contract (unknown token passes through)', () => { const stack = defineStack({ requires: ['aiStudio'] }, { strict: false }); expect(stack.requires).toEqual(['aiStudio']); - expect(warn).not.toHaveBeenCalled(); }); }); diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 0ef9b97cb8..0f8ee8b20a 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -4,10 +4,7 @@ import { z } from 'zod'; import { ManifestSchema } from './kernel/manifest.zod'; import { validateObjectNamespacePrefix } from './kernel/namespace-prefix'; -import { - PLATFORM_CAPABILITY_TOKENS, - DEPRECATED_PLATFORM_CAPABILITY_ALIASES, -} from './kernel/platform-capabilities'; +import { PLATFORM_CAPABILITY_TOKENS } from './kernel/platform-capabilities'; import { ClusterCapabilityConfigSchema } from './kernel/cluster.zod'; import { DatasourceSchema } from './data/datasource.zod'; import { TranslationBundleSchema, TranslationConfigSchema } from './system/translation.zod'; @@ -427,11 +424,12 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({ * intent). Use this for the AI service too: `requires: ['ai']` makes a missing * `@objectstack/service-ai` a hard boot error rather than a broken-but-booted app. * - * Tokens must be canonical members of the platform vocabulary - * (`PLATFORM_CAPABILITY_TOKENS`; deprecated aliases like `aiStudio` are - * rewritten with a warning). An UNKNOWN token — a typo or stale reference no - * runtime provides — is a `defineStack` **error**, not a silent no-op - * (framework#3265). + * Tokens must be members of the platform vocabulary + * (`PLATFORM_CAPABILITY_TOKENS`, canonical kebab-case). An UNKNOWN token — a + * typo or stale reference no runtime provides — is a `defineStack` **error**, + * not a silent no-op (framework#3265). The legacy camelCase spellings + * `aiStudio`/`aiSeat` were deprecated aliases in the prior release and were + * removed in framework#3308 — use `ai-studio`/`ai-seat`. * * If a capability is also provided explicitly via `plugins[]`, the * explicit instance wins (and the resolver does not double-register). @@ -998,51 +996,16 @@ function validateHierarchyScopeCapability(data: unknown): string[] { return errors; } -/** - * Canonicalize `requires` tokens against the platform capability vocabulary - * (framework#3265): deprecated alias spellings (`aiStudio` → `ai-studio`, - * `aiSeat` → `ai-seat`) are REWRITTEN to canonical at authoring time — the - * producer is fixed, so every consumer (serve resolver, cloud loader, - * discovery) sees one spelling (Prime Directive #12). Emits a deprecation - * warning per rewritten token. Unknown tokens are NOT handled here — they are - * rejected by {@link validateKnownCapabilities}. Strict mode only. - */ -function canonicalizeStackRequires(config: ObjectStackDefinition): ObjectStackDefinition { - const raw = config.requires; - if (!raw || raw.length === 0) return config; - - const warned = new Set(); - let changed = false; - const canonical = raw.map((token) => { - const mapped = DEPRECATED_PLATFORM_CAPABILITY_ALIASES[token]; - if (mapped) { - if (!warned.has(token)) { - warned.add(token); - console.warn( - `[defineStack] requires: '${token}' is a deprecated spelling — use '${mapped}'. ` + - `The alias is honored for one release, then removed (framework#3265).`, - ); - } - changed = true; - return mapped; - } - return token; - }); - - return changed ? { ...config, requires: canonical } : config; -} - /** * Reject `requires` tokens that are not part of the platform capability - * vocabulary (framework#3265). Runs AFTER {@link canonicalizeStackRequires}, so - * deprecated aliases have already resolved to canonical tokens — an unknown - * token here is a genuine typo or a stale reference that NO runtime provides, so - * every runtime would otherwise SILENTLY ignore it (declared ≠ enforced). Fail - * at the producer, loudly (Prime Directive #12): the warn-first grace period - * this replaced is over — the vocabulary is the union of every token the - * framework CLI and cloud's objectos-runtime resolve, plus the enterprise - * plugin-provided ones (`hierarchy-security` / `ai-seat` / `governance`). - * Returns one error per distinct unknown token. + * vocabulary (framework#3265). An unknown token is a genuine typo or a stale + * reference that NO runtime provides, so every runtime would otherwise SILENTLY + * ignore it (declared ≠ enforced). Fail at the producer, loudly (Prime + * Directive #12): the vocabulary is the union of every token the framework CLI + * and cloud's objectos-runtime resolve, plus the enterprise plugin-provided ones + * (`hierarchy-security` / `ai-seat` / `governance`). The legacy `aiStudio` / + * `aiSeat` aliases were removed in #3308, so those now reject too. Returns one + * error per distinct unknown token. */ function validateKnownCapabilities(config: ObjectStackDefinition): string[] { const raw = config.requires; @@ -1085,11 +1048,11 @@ export function defineStack( throw new Error(formatZodError(result.error, 'defineStack validation failed')); } - // Canonicalize `requires` (deprecated aliases → kebab canon) BEFORE the - // validators below read the definition, then REJECT any unknown capability - // token (framework#3265): no runtime provides it, so it would otherwise be - // silently ignored (declared ≠ enforced, Prime Directive #12). - const data = canonicalizeStackRequires(result.data); + // REJECT any unknown capability token (framework#3265/#3308): no runtime + // provides it, so it would otherwise be silently ignored (declared ≠ + // enforced, Prime Directive #12). No alias canonicalization — the deprecated + // `aiStudio`/`aiSeat` spellings were removed in #3308. + const data = result.data; const capErrors = validateKnownCapabilities(data); if (capErrors.length > 0) {