Skip to content

Commit 06ff734

Browse files
authored
feat(spec)!: remove deprecated aiStudio/aiSeat capability aliases (#3308) (#3329)
The one-cycle deprecation window from #3265 is over and a major release is being prepared, so the legacy camelCase `requires` aliases are dropped. `aiStudio`/`aiSeat` are no longer canonicalized to `ai-studio`/`ai-seat` — they now reject as unknown tokens in defineStack (the #3302 end-state). - spec: remove DEPRECATED_PLATFORM_CAPABILITY_ALIASES + canonicalizePlatformCapability; isKnownPlatformCapability no longer canonicalizes; defineStack drops canonicalizeStackRequires. - cli: serve resolver no longer canonicalizes raw-artifact requires. - Regenerate api-surface.json (4 export deletions); sync ADR-0087 spec-changes.json + protocol-upgrade-guide.md (pre-existing v16 drift surfaced by the path-filtered check). Breaking change shipped as minor per the launch-window convention. Cloud objectos-runtime (pinned to an older framework) follows on its next .framework-sha bump.
1 parent 9d897b3 commit 06ff734

11 files changed

Lines changed: 126 additions & 180 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
feat(spec)!: remove deprecated `aiStudio`/`aiSeat` capability aliases (#3308)
7+
8+
**BREAKING** (shipped as minor per the launch-window convention). The one-cycle
9+
deprecation window from #3265 is over: the legacy camelCase `requires` spellings
10+
`aiStudio`/`aiSeat` are no longer canonicalized to `ai-studio`/`ai-seat` — they
11+
are now plain unknown tokens, rejected by `defineStack` like any other typo.
12+
13+
- Removed exports `DEPRECATED_PLATFORM_CAPABILITY_ALIASES` and
14+
`canonicalizePlatformCapability` from `@objectstack/spec`; `isKnownPlatformCapability`
15+
no longer canonicalizes.
16+
- `defineStack` no longer rewrites aliases (the `canonicalizeStackRequires` pass
17+
is gone); the serve resolver no longer canonicalizes raw-artifact `requires`.
18+
19+
Migration: use the canonical kebab-case tokens `ai-studio` / `ai-seat`. All
20+
first-party configs were migrated in #862/#863; only stacks still carrying the
21+
legacy spelling are affected. Cloud's `objectos-runtime` (pinned to an older
22+
framework) follows on its next `.framework-sha` bump.

docs/protocol-upgrade-guide.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# Metadata protocol upgrade guide
55

6-
Current protocol: **15.0.0** · chain support floor: **protocol 10** · generated from the ADR-0087 registries (`@objectstack/spec` `conversions/` + `migrations/`).
6+
Current protocol: **16.0.0** · chain support floor: **protocol 10** · generated from the ADR-0087 registries (`@objectstack/spec` `conversions/` + `migrations/`).
77

88
## How to upgrade — from any past major
99

@@ -104,6 +104,16 @@ Protocol 15 unified the conditional-visibility predicate under `visibleWhen` (AD
104104
- 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.
105105
- Done when: `objectstack validate` passes with no unknown-key parse errors on form fields, form sections, or page components.
106106

107+
## Protocol 15 → 16
108+
109+
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.
110+
111+
### Semantic (delegated to you, with acceptance criteria)
112+
113+
- **`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)
114+
- 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.
115+
- Done when: `objectstack validate` passes with no unknown-key parse errors on dashboard widgets.
116+
107117
---
108118

109119
*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`.*

packages/cli/src/commands/serve.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
1010
import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js';
1111
import { resolveDriverType, createStorageDriver, UnsupportedDriverError } from '../utils/storage-driver.js';
1212
import { readEnvWithDeprecation, resolveMultiOrgEnabled, resolveAllowDegradedTenancy, isMcpServerEnabled, resolveSearchPinyinEnabled, isModuleNotFoundError } from '@objectstack/types';
13-
import { PLATFORM_CAPABILITY_TOKENS, canonicalizePlatformCapability } from '@objectstack/spec/kernel';
13+
import { PLATFORM_CAPABILITY_TOKENS } from '@objectstack/spec/kernel';
1414
import { resolveObjectStackHome } from '@objectstack/runtime';
1515
import { LOG_LEVELS, resolveLogLevel, readLogLevelEnv } from '../utils/log-level.js';
1616
import {
@@ -695,22 +695,14 @@ export default class Serve extends Command {
695695
// instance wins over the auto-loader).
696696
const presetName = flags.preset ?? (isDev ? 'default' : 'default');
697697
const presetTiers = Serve.TIER_PRESETS[presetName] ?? Serve.TIER_PRESETS.default;
698+
// Dedupe `requires` (Set keeps first-seen order). The deprecated
699+
// `aiStudio`/`aiSeat` alias canonicalization was removed in framework#3308
700+
// — legacy spellings are now unknown tokens (warned below, rejected at
701+
// authoring by defineStack).
698702
const rawRequires: string[] = Array.isArray((config as any).requires)
699703
? (config as any).requires.filter((c: unknown) => typeof c === 'string')
700704
: [];
701-
// Canonicalize deprecated capability spellings (aiStudio → ai-studio, …)
702-
// so artifacts compiled by an older spec keep booting; `defineStack`
703-
// already rewrites + warns on the source path, this covers raw artifact
704-
// JSON (framework#3265). Dedupe after mapping (Set keeps first-seen order).
705-
const requires: string[] = [...new Set(rawRequires.map((c: string) => {
706-
const canonical = canonicalizePlatformCapability(c);
707-
if (canonical !== c) {
708-
console.warn(chalk.yellow(
709-
` ⚠ requires: '${c}' is a deprecated spelling — use '${canonical}' (framework#3265)`,
710-
));
711-
}
712-
return canonical;
713-
}))];
705+
const requires: string[] = [...new Set(rawRequires)];
714706
// Snapshot the app's EXPLICIT capability declarations BEFORE the platform
715707
// appends its own convenience defaults (auth→email, mcp, pinyin-search,
716708
// ALWAYS_ON_CAPABILITIES, queue/job). Only these explicit declarations carry

packages/cli/test/serve-capability-vocabulary.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import {
3-
PLATFORM_CAPABILITY_TOKENS,
4-
DEPRECATED_PLATFORM_CAPABILITY_ALIASES,
5-
} from '@objectstack/spec/kernel';
2+
import { PLATFORM_CAPABILITY_TOKENS } from '@objectstack/spec/kernel';
63
import Serve from '../src/commands/serve.js';
74

85
// framework#3265 — drift guard: the serve path's provider registries must stay
@@ -22,8 +19,8 @@ describe('serve capability registries vs spec vocabulary (#3265)', () => {
2219
}
2320
});
2421

25-
it('registries use only canonical spellings — never deprecated aliases', () => {
26-
const legacy = Object.keys(DEPRECATED_PLATFORM_CAPABILITY_ALIASES);
22+
it('registries use only canonical spellings — never the removed camelCase aliases (#3308)', () => {
23+
const legacy = ['aiStudio', 'aiSeat'];
2724
for (const token of [...Object.keys(Serve.CAPABILITY_PROVIDERS), ...Object.keys(Serve.CAPABILITY_TO_TIER)]) {
2825
expect(legacy).not.toContain(token);
2926
}

packages/spec/api-surface.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"ConversionFixture (interface)",
2727
"ConversionNotice (interface)",
2828
"CronExpressionInputSchema (const)",
29-
"DEPRECATED_PLATFORM_CAPABILITY_ALIASES (const)",
3029
"DatasourceMappingRule (type)",
3130
"DatasourceMappingRuleSchema (const)",
3231
"DefineStackOptions (interface)",
@@ -102,7 +101,6 @@
102101
"applyConversions (function)",
103102
"applyConversionsToFlow (function)",
104103
"applyMetaMigrations (function)",
105-
"canonicalizePlatformCapability (function)",
106104
"cel (function)",
107105
"collectConversionNotices (function)",
108106
"composeMigrationChain (function)",
@@ -1294,7 +1292,6 @@
12941292
"CustomizationPolicy (type)",
12951293
"CustomizationPolicySchema (const)",
12961294
"DEFAULT_METADATA_TYPE_REGISTRY (const)",
1297-
"DEPRECATED_PLATFORM_CAPABILITY_ALIASES (const)",
12981295
"DeadLetterQueueEntry (type)",
12991296
"DeadLetterQueueEntrySchema (const)",
13001297
"DependencyConflict (type)",
@@ -1750,7 +1747,6 @@
17501747
"VersionConstraint (type)",
17511748
"VersionConstraintSchema (const)",
17521749
"VulnerabilitySeverity (type)",
1753-
"canonicalizePlatformCapability (function)",
17541750
"deriveNamespaceFromPackageId (function)",
17551751
"evaluateLockForDelete (function)",
17561752
"evaluateLockForWrite (function)",

packages/spec/spec-changes.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"$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.",
3-
"protocolVersion": "15.0.0",
3+
"protocolVersion": "16.0.0",
44
"supportFloor": 10,
55
"migrateCommand": "objectstack migrate meta --from <N> (N >= 10)",
66
"aggregate": {
77
"from": 10,
8-
"to": 15,
8+
"to": 16,
99
"added": [],
1010
"converted": [
1111
{
@@ -132,6 +132,13 @@
132132
"migrationId": "ui-schemas-strict-unknown-keys",
133133
"toMajor": 15,
134134
"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."
135+
},
136+
{
137+
"surface": "dashboard widgets (undeclared top-level keys — legacy inline analytics, objectui-internal `component`/`data`, or typos)",
138+
"replacement": "declared keys only (`dataset` + `dimensions` + `values` for analytics; `options` for renderer-specific extras)",
139+
"migrationId": "dashboard-widget-strict-unknown-keys",
140+
"toMajor": 16,
141+
"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."
135142
}
136143
],
137144
"removed": []
@@ -307,6 +314,22 @@
307314
}
308315
],
309316
"removed": []
317+
},
318+
{
319+
"from": 15,
320+
"to": 16,
321+
"added": [],
322+
"converted": [],
323+
"migrated": [
324+
{
325+
"surface": "dashboard widgets (undeclared top-level keys — legacy inline analytics, objectui-internal `component`/`data`, or typos)",
326+
"replacement": "declared keys only (`dataset` + `dimensions` + `values` for analytics; `options` for renderer-specific extras)",
327+
"migrationId": "dashboard-widget-strict-unknown-keys",
328+
"toMajor": 16,
329+
"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."
330+
}
331+
],
332+
"removed": []
310333
}
311334
]
312335
}

packages/spec/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@ export { type PluginContext } from './kernel/plugin.zod';
123123
// Platform SERVICE capability vocabulary for `requires: [...]` (framework#3265).
124124
export {
125125
PLATFORM_CAPABILITY_TOKENS,
126-
DEPRECATED_PLATFORM_CAPABILITY_ALIASES,
127-
canonicalizePlatformCapability,
128126
isKnownPlatformCapability,
129127
} from './kernel/platform-capabilities';
130128

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { describe, expect, it } from 'vitest';
22
import {
33
PLATFORM_CAPABILITY_TOKENS,
4-
DEPRECATED_PLATFORM_CAPABILITY_ALIASES,
5-
canonicalizePlatformCapability,
64
isKnownPlatformCapability,
75
} from './platform-capabilities';
86

97
// framework#3265 — one capability vocabulary across the standalone serve path
10-
// and cloud's objectos-runtime loader, canonical spelling kebab-case.
8+
// and cloud's objectos-runtime loader, canonical spelling kebab-case. The
9+
// deprecated `aiStudio`/`aiSeat` aliases were removed in #3308.
1110

1211
describe('PLATFORM_CAPABILITY_TOKENS', () => {
1312
it('is frozen and duplicate-free', () => {
@@ -27,37 +26,23 @@ describe('PLATFORM_CAPABILITY_TOKENS', () => {
2726
}
2827
});
2928

30-
it('contains no deprecated spellings', () => {
31-
for (const legacy of Object.keys(DEPRECATED_PLATFORM_CAPABILITY_ALIASES)) {
29+
it('contains no camelCase legacy spellings (aliases removed, #3308)', () => {
30+
for (const legacy of ['aiStudio', 'aiSeat']) {
3231
expect(PLATFORM_CAPABILITY_TOKENS).not.toContain(legacy);
3332
}
3433
});
3534
});
3635

37-
describe('DEPRECATED_PLATFORM_CAPABILITY_ALIASES / canonicalizePlatformCapability', () => {
38-
it('maps every alias onto a token that exists in the vocabulary', () => {
39-
for (const canonical of Object.values(DEPRECATED_PLATFORM_CAPABILITY_ALIASES)) {
40-
expect(PLATFORM_CAPABILITY_TOKENS).toContain(canonical);
41-
}
42-
});
43-
44-
it('canonicalizes the legacy cloud spellings', () => {
45-
expect(canonicalizePlatformCapability('aiStudio')).toBe('ai-studio');
46-
expect(canonicalizePlatformCapability('aiSeat')).toBe('ai-seat');
47-
});
48-
49-
it('is identity for canonical and unknown tokens', () => {
50-
expect(canonicalizePlatformCapability('ai-studio')).toBe('ai-studio');
51-
expect(canonicalizePlatformCapability('automation')).toBe('automation');
52-
expect(canonicalizePlatformCapability('not-a-capability')).toBe('not-a-capability');
53-
});
54-
});
55-
5636
describe('isKnownPlatformCapability', () => {
57-
it('accepts canonical tokens and deprecated aliases, rejects unknowns', () => {
37+
it('accepts canonical tokens verbatim', () => {
5838
expect(isKnownPlatformCapability('ai-studio')).toBe(true);
59-
expect(isKnownPlatformCapability('aiStudio')).toBe(true);
39+
expect(isKnownPlatformCapability('ai-seat')).toBe(true);
6040
expect(isKnownPlatformCapability('governance')).toBe(true);
41+
});
42+
43+
it('rejects the removed camelCase aliases and typos (no canonicalization, #3308)', () => {
44+
expect(isKnownPlatformCapability('aiStudio')).toBe(false);
45+
expect(isKnownPlatformCapability('aiSeat')).toBe(false);
6146
expect(isKnownPlatformCapability('automations')).toBe(false);
6247
});
6348
});

packages/spec/src/kernel/platform-capabilities.ts

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
* ADR-0066 authorization capabilities declared in `capabilities: [...]`.)
1313
*
1414
* Canonical spelling is lower-case kebab-case (`ai-studio`, `pinyin-search`,
15-
* `hierarchy-security`). Legacy camelCase spellings that shipped in cloud
16-
* configs are mapped through {@link DEPRECATED_PLATFORM_CAPABILITY_ALIASES}
17-
* for one deprecation cycle — `defineStack` normalizes them with a warning at
18-
* authoring time, and runtimes canonicalize raw artifact input the same way.
15+
* `hierarchy-security`). The legacy camelCase spellings (`aiStudio` / `aiSeat`)
16+
* that shipped transitionally were honored as deprecated aliases for one cycle
17+
* (framework#3265) and have been REMOVED (framework#3308) — they are now plain
18+
* unknown tokens, rejected by `defineStack` like any other typo.
1919
*
2020
* Growing the platform: when a new `requires`-resolvable service ships, add
2121
* 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([
6666
]);
6767

6868
/**
69-
* Deprecated `requires` spellings → canonical token. One deprecation cycle:
70-
* authoring (`defineStack`) rewrites these with a warning; runtimes accept
71-
* them via {@link canonicalizePlatformCapability} so artifacts built by an
72-
* older spec keep booting. Do not add new aliases — new tokens ship in
73-
* canonical kebab-case only.
74-
*/
75-
export const DEPRECATED_PLATFORM_CAPABILITY_ALIASES: Readonly<Record<string, string>> =
76-
Object.freeze({
77-
aiStudio: 'ai-studio',
78-
aiSeat: 'ai-seat',
79-
});
80-
81-
/**
82-
* Map a `requires` token to its canonical spelling (identity for tokens that
83-
* are already canonical or unknown).
84-
*/
85-
export function canonicalizePlatformCapability(token: string): string {
86-
return DEPRECATED_PLATFORM_CAPABILITY_ALIASES[token] ?? token;
87-
}
88-
89-
/**
90-
* True when the token (after alias canonicalization) is part of the platform
91-
* capability vocabulary.
69+
* True when the token is part of the platform capability vocabulary. There is
70+
* no longer any alias canonicalization (framework#3308) — a token is known iff
71+
* it appears verbatim in {@link PLATFORM_CAPABILITY_TOKENS}.
9272
*/
9373
export function isKnownPlatformCapability(token: string): boolean {
94-
return PLATFORM_CAPABILITY_TOKENS.includes(canonicalizePlatformCapability(token));
74+
return PLATFORM_CAPABILITY_TOKENS.includes(token);
9575
}

0 commit comments

Comments
 (0)