@@ -427,6 +427,12 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({
427427 * intent). Use this for the AI service too: `requires: ['ai']` makes a missing
428428 * `@objectstack/service-ai` a hard boot error rather than a broken-but-booted app.
429429 *
430+ * Tokens must be canonical members of the platform vocabulary
431+ * (`PLATFORM_CAPABILITY_TOKENS`; deprecated aliases like `aiStudio` are
432+ * rewritten with a warning). An UNKNOWN token — a typo or stale reference no
433+ * runtime provides — is a `defineStack` **error**, not a silent no-op
434+ * (framework#3265).
435+ *
430436 * If a capability is also provided explicitly via `plugins[]`, the
431437 * explicit instance wins (and the resolver does not double-register).
432438 *
@@ -439,7 +445,7 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({
439445 * });
440446 * ```
441447 */
442- requires : z . array ( z . string ( ) ) . optional ( ) . describe ( 'Capability names this stack requires from the platform (canonical kebab-case tokens from PLATFORM_CAPABILITY_TOKENS; declared-but-missing ⇒ fail-fast at startup)' ) ,
448+ requires : z . array ( z . string ( ) ) . optional ( ) . describe ( 'Capability names this stack requires from the platform (canonical kebab-case tokens from PLATFORM_CAPABILITY_TOKENS; an unknown token is a defineStack error, declared-but-missing ⇒ fail-fast at startup)' ) ,
443449
444450 /**
445451 * Plugin tier presets to auto-register (e.g. `core`, `ai`, `ui`, `auth`).
@@ -993,21 +999,13 @@ function validateHierarchyScopeCapability(data: unknown): string[] {
993999}
9941000
9951001/**
996- * Canonicalize `requires` tokens and warn-validate them against the platform
997- * capability vocabulary (framework#3265).
998- *
999- * - Deprecated alias spellings (`aiStudio` → `ai-studio`, `aiSeat` →
1000- * `ai-seat`) are REWRITTEN to canonical at authoring time — the producer is
1001- * fixed, so every consumer (serve resolver, cloud loader, discovery) sees
1002- * one spelling (Prime Directive #12).
1003- * - Tokens outside the vocabulary get a console warning. Warn-only for now —
1004- * both runtimes previously ignored unknown tokens silently, so a hard reject
1005- * could brick working stacks; once the vocabulary proves complete this is
1006- * intended to become a defineStack error.
1007- *
1008- * Returns the (possibly rewritten) definition; emits warnings via
1009- * `console.warn`. Strict mode only — non-strict mode skips all validation by
1010- * contract.
1002+ * Canonicalize `requires` tokens against the platform capability vocabulary
1003+ * (framework#3265): deprecated alias spellings (`aiStudio` → `ai-studio`,
1004+ * `aiSeat` → `ai-seat`) are REWRITTEN to canonical at authoring time — the
1005+ * producer is fixed, so every consumer (serve resolver, cloud loader,
1006+ * discovery) sees one spelling (Prime Directive #12). Emits a deprecation
1007+ * warning per rewritten token. Unknown tokens are NOT handled here — they are
1008+ * rejected by {@link validateKnownCapabilities}. Strict mode only.
10111009 */
10121010function canonicalizeStackRequires ( config : ObjectStackDefinition ) : ObjectStackDefinition {
10131011 const raw = config . requires ;
@@ -1028,20 +1026,41 @@ function canonicalizeStackRequires(config: ObjectStackDefinition): ObjectStackDe
10281026 changed = true ;
10291027 return mapped ;
10301028 }
1031- if ( ! PLATFORM_CAPABILITY_TOKENS . includes ( token ) && ! warned . has ( token ) ) {
1032- warned . add ( token ) ;
1033- console . warn (
1034- `[defineStack] requires: '${ token } ' is not a known platform capability — ` +
1035- `check for a typo (known tokens are kebab-case, e.g. 'ai-studio', 'pinyin-search'). ` +
1036- `Unknown tokens are ignored by the runtime today; this will become a validation error (framework#3265).` ,
1037- ) ;
1038- }
10391029 return token ;
10401030 } ) ;
10411031
10421032 return changed ? { ...config , requires : canonical } : config ;
10431033}
10441034
1035+ /**
1036+ * Reject `requires` tokens that are not part of the platform capability
1037+ * vocabulary (framework#3265). Runs AFTER {@link canonicalizeStackRequires}, so
1038+ * deprecated aliases have already resolved to canonical tokens — an unknown
1039+ * token here is a genuine typo or a stale reference that NO runtime provides, so
1040+ * every runtime would otherwise SILENTLY ignore it (declared ≠ enforced). Fail
1041+ * at the producer, loudly (Prime Directive #12): the warn-first grace period
1042+ * this replaced is over — the vocabulary is the union of every token the
1043+ * framework CLI and cloud's objectos-runtime resolve, plus the enterprise
1044+ * plugin-provided ones (`hierarchy-security` / `ai-seat` / `governance`).
1045+ * Returns one error per distinct unknown token.
1046+ */
1047+ function validateKnownCapabilities ( config : ObjectStackDefinition ) : string [ ] {
1048+ const raw = config . requires ;
1049+ if ( ! raw || raw . length === 0 ) return [ ] ;
1050+ const seen = new Set < string > ( ) ;
1051+ const errors : string [ ] = [ ] ;
1052+ for ( const token of raw ) {
1053+ if ( PLATFORM_CAPABILITY_TOKENS . includes ( token ) || seen . has ( token ) ) continue ;
1054+ seen . add ( token ) ;
1055+ errors . push (
1056+ `requires: '${ token } ' is not a known platform capability — check for a typo ` +
1057+ `(known tokens are kebab-case, e.g. 'ai-studio', 'pinyin-search', 'automation'). ` +
1058+ `No runtime provides it, so it would be silently ignored.` ,
1059+ ) ;
1060+ }
1061+ return errors ;
1062+ }
1063+
10451064export function defineStack (
10461065 config : ObjectStackDefinitionInput ,
10471066 options ?: DefineStackOptions ,
@@ -1066,10 +1085,19 @@ export function defineStack(
10661085 throw new Error ( formatZodError ( result . error , 'defineStack validation failed' ) ) ;
10671086 }
10681087
1069- // Canonicalize `requires` (deprecated aliases → kebab canon) and warn on
1070- // unknown capability tokens BEFORE the validators below read the definition.
1088+ // Canonicalize `requires` (deprecated aliases → kebab canon) BEFORE the
1089+ // validators below read the definition, then REJECT any unknown capability
1090+ // token (framework#3265): no runtime provides it, so it would otherwise be
1091+ // silently ignored (declared ≠ enforced, Prime Directive #12).
10711092 const data = canonicalizeStackRequires ( result . data ) ;
10721093
1094+ const capErrors = validateKnownCapabilities ( data ) ;
1095+ if ( capErrors . length > 0 ) {
1096+ const header = `defineStack capability validation failed (${ capErrors . length } issue${ capErrors . length === 1 ? '' : 's' } ):` ;
1097+ const lines = capErrors . map ( ( e ) => ` ✗ ${ e } ` ) ;
1098+ throw new Error ( `${ header } \n\n${ lines . join ( '\n' ) } ` ) ;
1099+ }
1100+
10731101 const crossRefErrors = validateCrossReferences ( data ) ;
10741102 if ( crossRefErrors . length > 0 ) {
10751103 const header = `defineStack cross-reference validation failed (${ crossRefErrors . length } issue${ crossRefErrors . length === 1 ? '' : 's' } ):` ;
0 commit comments