@wibly/internal-manifest 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,607 @@
1
+ /**
2
+ * Top-level Experience-manifest Zod schema.
3
+ *
4
+ * Per Platform Spec §2.1 + §6.6.2, the manifest is the contract between
5
+ * the Studio (or a hand-authored Experience) and the Runtime. It captures
6
+ * every piece of Experience configuration the Runtime needs to drive a
7
+ * Session:
8
+ *
9
+ * - identity & ownership (`id`, `version`, `tenant`, `creator`),
10
+ * - safety / cost shape (`inferenceEnvelope`, `contentRating`,
11
+ * `fallbackResponses`),
12
+ * - workflow (`workflow.phases`, `concurrentOpportunities`,
13
+ * `lifecyclePolicies`),
14
+ * - state shape (`stateSchema`),
15
+ * - inference inputs (`promptSlots`, `personaBindings`),
16
+ * - rendering (`widgetDependencies`),
17
+ * - resolution (`scoring`).
18
+ *
19
+ * The schema is *strict on shape, permissive on content*. Strings have
20
+ * minimum lengths; numerics have positive / non-negative bounds; cross-
21
+ * field invariants live in `validate.ts`. The `stateSchema` slices and
22
+ * the `scoring` payload carry domain content the Runtime parses against
23
+ * its own per-Experience schemas — at the manifest layer we capture them
24
+ * as JSON-shaped values so a manifest can ship without round-tripping
25
+ * Zod-to-JSON-Schema (a Phase 2 concern; B4 + B7 own the producer side).
26
+ *
27
+ * Per chunk-A1 §4.1, every exported `Manifest*` type is `z.infer` of its
28
+ * schema's *output* — defaults and transforms produce concrete values
29
+ * downstream consumers can read without re-narrowing.
30
+ */
31
+
32
+ import { z } from 'zod';
33
+
34
+ import {
35
+ CallKindSchema,
36
+ CollectionRuleSchema,
37
+ InputSetSchema,
38
+ PhaseSchema,
39
+ } from './phase.js';
40
+
41
+ export { CallKindSchema };
42
+
43
+ // -----------------------------------------------------------------------------
44
+ // JSON value (used for state-schema slices and scoring payloads)
45
+ // -----------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Recursive JSON-shaped value. Used wherever the manifest carries an
49
+ * opaque payload the Runtime parses against its own per-Experience Zod
50
+ * schema (state slices, scoring effect descriptors). The recursion is
51
+ * one-deep on `z.lazy` per Zod's idiom; the type is exported separately
52
+ * so consumers can annotate without re-deriving from the schema.
53
+ */
54
+ export type JsonValue =
55
+ | string
56
+ | number
57
+ | boolean
58
+ | null
59
+ | readonly JsonValue[]
60
+ | { readonly [k: string]: JsonValue };
61
+
62
+ const JsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
63
+ z.union([
64
+ z.string(),
65
+ z.number(),
66
+ z.boolean(),
67
+ z.null(),
68
+ z.array(JsonValueSchema),
69
+ z.record(JsonValueSchema),
70
+ ]),
71
+ );
72
+
73
+ // -----------------------------------------------------------------------------
74
+ // Identity / ownership
75
+ // -----------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Experience id. Format is `exp_<nanoid>` per the prefixed-id convention
79
+ * from `@wibly/internal-shared/id` (matches the `experiences.id` PK shape from
80
+ * chunk A2). The manifest schema validates the prefix only; the full
81
+ * nanoid alphabet is enforced at the catalogue / publish boundary.
82
+ */
83
+ export const ManifestIdSchema = z
84
+ .string()
85
+ .regex(/^exp_[A-Za-z0-9_-]+$/, 'must be an exp_ prefixed nanoid');
86
+
87
+ /**
88
+ * Tenant id. Format is `tnt_<nanoid>`. Optional / nullable on the
89
+ * manifest because first-party Experiences (Hello World, Rashomon,
90
+ * Flatterer) ship without a tenant binding — they're owned by the
91
+ * platform rather than a corporate tenant.
92
+ */
93
+ export const ManifestTenantIdSchema = z
94
+ .string()
95
+ .regex(/^tnt_[A-Za-z0-9_-]+$/, 'must be a tnt_ prefixed nanoid');
96
+
97
+ /**
98
+ * Persona id. Format is `per_<nanoid>` per `pkId('per')` from chunk A2.
99
+ */
100
+ export const PersonaIdSchema = z
101
+ .string()
102
+ .regex(/^per_[A-Za-z0-9_-]+$/, 'must be a per_ prefixed nanoid');
103
+
104
+ // -----------------------------------------------------------------------------
105
+ // Persona binding
106
+ // -----------------------------------------------------------------------------
107
+
108
+ /**
109
+ * One `(role, personaId)` pair. The Runtime resolves the active persona
110
+ * for a call by looking up the role tag (e.g. `'host'` / `'judge'`) in
111
+ * `personaBindings` and reading the bound persona id. Roles are
112
+ * manifest-local strings, not enum members, so a Creator can introduce
113
+ * new roles without a schema bump. Uniqueness on `role` is enforced by
114
+ * `validateManifest` (one role can't bind two personas).
115
+ */
116
+ export const PersonaBindingSchema = z.object({
117
+ role: z.string().min(1),
118
+ personaId: PersonaIdSchema,
119
+ });
120
+
121
+ // -----------------------------------------------------------------------------
122
+ // Inference envelope
123
+ // -----------------------------------------------------------------------------
124
+
125
+ /**
126
+ * The four quality tiers the Inference Gateway routes to (per chunk B2).
127
+ * `fast` and `standard` are the cheap-and-quick tiers; `premium` and
128
+ * `creative` route to higher-cost models. The manifest declares which
129
+ * tiers an Experience is allowed to request; the Gateway 402's anything
130
+ * outside the declared set.
131
+ */
132
+ export const QualityTierSchema = z.enum([
133
+ 'fast',
134
+ 'standard',
135
+ 'premium',
136
+ 'creative',
137
+ ]);
138
+
139
+ /**
140
+ * Per-Session inference budget, plus the set of quality tiers this
141
+ * Experience is allowed to request. The Runtime decrements against
142
+ * `maxLlmCallsPerSession` and `maxTtsSecondsPerSession` once per call;
143
+ * `maxTokensInPerCall` / `maxTokensOutPerCall` cap a single call.
144
+ *
145
+ * Per the spec's "the inference envelope is non-zero" structural check,
146
+ * every cap is `.positive()` (`maxTtsSecondsPerSession` is non-negative
147
+ * because a no-TTS Experience is a legitimate shape). `qualityTiers` is
148
+ * non-empty because an envelope with no allowed tiers makes the
149
+ * Experience unrunnable.
150
+ */
151
+ export const InferenceEnvelopeSchema = z.object({
152
+ maxLlmCallsPerSession: z.number().int().positive(),
153
+ maxTokensInPerCall: z.number().int().positive(),
154
+ maxTokensOutPerCall: z.number().int().positive(),
155
+ maxTtsSecondsPerSession: z.number().int().nonnegative(),
156
+ qualityTiers: z.array(QualityTierSchema).min(1),
157
+ });
158
+
159
+ // -----------------------------------------------------------------------------
160
+ // State schema (per-recipient slice descriptors)
161
+ // -----------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Per Platform Spec §2.4.4 the Runtime maintains four state slices and
165
+ * projects per-recipient views onto them. The manifest declares the
166
+ * shape of each slice as a Zod-compatible JSON Schema. At the manifest
167
+ * layer the slice is `JsonValue` (opaque) — the per-Experience Zod
168
+ * schemas the Runtime consumes are produced at packaging time (B7) and
169
+ * shipped alongside the manifest blob, not parsed from it inline.
170
+ *
171
+ * `playerPublic` and `playerPrivate` form the per-player public/private
172
+ * split (visible to all players vs visible only to the player). `team`
173
+ * is optional because not every Experience uses team scope.
174
+ */
175
+ export const StateSchemaSchema = z.object({
176
+ session: JsonValueSchema,
177
+ host: JsonValueSchema,
178
+ playerPublic: JsonValueSchema,
179
+ playerPrivate: JsonValueSchema,
180
+ team: JsonValueSchema,
181
+ });
182
+
183
+ // -----------------------------------------------------------------------------
184
+ // Workflow
185
+ // -----------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Workflow declaration: the initial phase the Runtime enters on session
189
+ * start, plus the full phase set. Per the chunk acceptance, the
190
+ * structural validator catches `initialPhase` not in `phases`,
191
+ * duplicate phase ids, transitions to non-existent phases, unreachable
192
+ * phases, and (via `PhaseSchema`) phases without an exit transition.
193
+ */
194
+ export const WorkflowSchema = z.object({
195
+ initialPhase: z.string().min(1),
196
+ phases: z.array(PhaseSchema).min(1),
197
+ });
198
+
199
+ // -----------------------------------------------------------------------------
200
+ // Concurrent opportunities
201
+ // -----------------------------------------------------------------------------
202
+
203
+ /**
204
+ * Concurrent opportunities (per Platform Spec §3.9.2 — the "Yellow"
205
+ * pattern). One opportunity attaches to one or more phases and runs as
206
+ * a parallel input collection alongside the phase's primary input set.
207
+ * `scoringEffect` is opaque at the manifest layer; the per-Experience
208
+ * scoring engine resolves it.
209
+ *
210
+ * `multiFire` (chunk B8c) opts the opportunity into repeat firing
211
+ * within a single phase entry. Default `false` preserves the chunk-
212
+ * B8b behaviour where each opportunity fires at most once per phase
213
+ * (avoiding loops on additional matching submissions). MVP fixtures
214
+ * stay on the single-fire path; Rashomon-style wagers that need
215
+ * repeat scoring set `multiFire: true` once the chunk-B8c scoring
216
+ * ledger is wired (the ledger row is idempotent on `messageId` via
217
+ * the submission's envelope id, which gives the dedup the multi-fire
218
+ * mode relies on).
219
+ */
220
+ export const ConcurrentOpportunitySchema = z.object({
221
+ id: z.string().min(1),
222
+ attachedToPhases: z.array(z.string().min(1)).min(1),
223
+ inputSet: InputSetSchema,
224
+ collectionRule: CollectionRuleSchema,
225
+ scoringEffect: JsonValueSchema,
226
+ multiFire: z.boolean().optional(),
227
+ });
228
+
229
+ // -----------------------------------------------------------------------------
230
+ // Scoring (per Platform Spec §8.2)
231
+ // -----------------------------------------------------------------------------
232
+
233
+ /**
234
+ * One scoring dimension (e.g. "funniness", "coherence", "speed"). The
235
+ * `weight` defaults to `1` and is used by aggregators to combine
236
+ * dimension scores into a final ranking. `scaleMin` / `scaleMax` lock
237
+ * the per-judgement range.
238
+ */
239
+ export const ScoringDimensionSchema = z.object({
240
+ id: z.string().min(1),
241
+ label: z.string().min(1),
242
+ weight: z.number().nonnegative().default(1),
243
+ scaleMin: z.number(),
244
+ scaleMax: z.number(),
245
+ });
246
+
247
+ /**
248
+ * Discriminated aggregator descriptor. The Runtime applies the aggregator
249
+ * to the per-dimension scores in the scoring ledger to produce a single
250
+ * leaderboard value per actor. Add new variants in the chunk that needs
251
+ * them; the engine in B8c is the canonical consumer.
252
+ */
253
+ export const ScoringAggregatorSchema = z.discriminatedUnion('kind', [
254
+ z.object({ kind: z.literal('sum') }),
255
+ z.object({ kind: z.literal('average') }),
256
+ z.object({ kind: z.literal('weighted_sum') }),
257
+ z.object({ kind: z.literal('max') }),
258
+ z.object({ kind: z.literal('min') }),
259
+ ]);
260
+
261
+ /**
262
+ * One award (e.g. "Funniest", "Most Improved"). `dimensionId` references
263
+ * a `ScoringDimension.id`; cross-reference validation lives in
264
+ * `validateManifest`. `criterion.kind` discriminates the resolution
265
+ * rule — `top_n` picks the top N actors by the dimension's value;
266
+ * `threshold` picks every actor at or above a value.
267
+ */
268
+ export const AwardCriterionSchema = z.discriminatedUnion('kind', [
269
+ z.object({
270
+ kind: z.literal('top_n'),
271
+ n: z.number().int().positive(),
272
+ }),
273
+ z.object({ kind: z.literal('threshold'), value: z.number() }),
274
+ ]);
275
+
276
+ export const AwardDefinitionSchema = z.object({
277
+ id: z.string().min(1),
278
+ label: z.string().min(1),
279
+ dimensionId: z.string().min(1),
280
+ criterion: AwardCriterionSchema,
281
+ });
282
+
283
+ export const ScoringSchema = z.object({
284
+ dimensions: z.array(ScoringDimensionSchema),
285
+ aggregators: z.array(ScoringAggregatorSchema),
286
+ awards: z.array(AwardDefinitionSchema),
287
+ });
288
+
289
+ // -----------------------------------------------------------------------------
290
+ // Lifecycle policies (per Platform Spec §3.8.2)
291
+ // -----------------------------------------------------------------------------
292
+
293
+ /**
294
+ * Lifecycle policy entries: declarative responses to Runtime situations
295
+ * a long-running Session can hit (player disconnect, host disconnect,
296
+ * inference outage, host reclaim, etc.). Each entry tags the
297
+ * `situation` and a discriminated `action`.
298
+ *
299
+ * The action set is intentionally narrow at MVP — `pause_session`,
300
+ * `continue_without_them`, `end_session`, `replace_actor` cover
301
+ * Hello World and the two first-party Experiences. Add new actions
302
+ * with the chunks that need them (B8a / B8b / B8c, B15).
303
+ *
304
+ * **Naming.** The literals match the verbs the chunk-B8c lifecycle
305
+ * policy engine reads from the manifest (per Development Spec
306
+ * §6.10c "policy.ts applies the manifest's declarative response").
307
+ * Earlier drafts of this schema used the shorter `pause` /
308
+ * `continue` literals; B8c's plan settled the names as
309
+ * `pause_session` / `continue_without_them` because `continue`
310
+ * (without its object) is ambiguous with the JS keyword and
311
+ * `pause` reads as ambiguous between "pause this phase" and "pause
312
+ * the entire session" once concurrent opportunities (P10) land.
313
+ */
314
+ export const LifecycleSituationSchema = z.enum([
315
+ 'player_disconnect',
316
+ 'host_disconnect',
317
+ 'host_reclaim',
318
+ 'inference_outage',
319
+ 'safety_block',
320
+ ]);
321
+
322
+ export const LifecycleActionSchema = z.discriminatedUnion('kind', [
323
+ z.object({
324
+ kind: z.literal('pause_session'),
325
+ /** Wall-clock ms to pause before falling back to `fallback`. */
326
+ timeoutMs: z.number().int().positive(),
327
+ fallback: z.enum(['continue_without_them', 'end_session']),
328
+ }),
329
+ z.object({ kind: z.literal('continue_without_them') }),
330
+ z.object({ kind: z.literal('end_session') }),
331
+ z.object({
332
+ kind: z.literal('replace_actor'),
333
+ /** Persona role to bind in place of the disconnected actor. */
334
+ withRole: z.string().min(1),
335
+ }),
336
+ ]);
337
+
338
+ export const LifecyclePolicySchema = z.object({
339
+ situation: LifecycleSituationSchema,
340
+ action: LifecycleActionSchema,
341
+ });
342
+
343
+ // -----------------------------------------------------------------------------
344
+ // Prompt slots (per docs/conventions/prompt-composition.md)
345
+ // -----------------------------------------------------------------------------
346
+
347
+ /**
348
+ * Per the prompt-composition doc, every slot value is either a literal
349
+ * string or a template with named vars interpolated from session state.
350
+ * Templates use `{varName}` placeholders; the composer (chunk B4)
351
+ * resolves them. `vars` is non-empty when supplied — a template with no
352
+ * vars is just a literal and should be authored as one.
353
+ */
354
+ export const PromptSlotValueSchema = z.union([
355
+ z.string(),
356
+ z.object({
357
+ template: z.string().min(1),
358
+ vars: z.array(z.string().min(1)).min(1).optional(),
359
+ }),
360
+ ]);
361
+
362
+ /**
363
+ * The known set of `callKind`s the prompt composer (chunk B4) maps to
364
+ * the eight-layer prompt. Listed in
365
+ * `docs/conventions/prompt-composition.md`. Every callKind that appears
366
+ * here MUST appear in that document; new callKinds land in the chunk
367
+ * that introduces them with the doc updated in the same commit.
368
+ *
369
+ * Defined in `phase.ts` (re-exported above) so the chunk-B8c
370
+ * `WorkflowSideEffect.inference.callKind` schema can reference it
371
+ * without a circular import.
372
+ */
373
+
374
+ /**
375
+ * Manifest-supplied prompt content. `experienceSystem` is the layer-3
376
+ * Experience system message; `callTypes` keys per `callKind` populate
377
+ * layer 4; `outputSchemas` populates layer 7 with a JSON-shaped schema
378
+ * the Gateway renders. Per the prompt-composition doc, the platform
379
+ * supplies the structural layers (1, 4-structural, 5, 6, 8) and the
380
+ * Persona Service supplies layer 2; everything below is the manifest's
381
+ * contribution.
382
+ */
383
+ export const PromptSlotsSchema = z.object({
384
+ experienceSystem: PromptSlotValueSchema,
385
+ callTypes: z.record(CallKindSchema, PromptSlotValueSchema),
386
+ outputSchemas: z.record(CallKindSchema, JsonValueSchema).optional(),
387
+ });
388
+
389
+ // -----------------------------------------------------------------------------
390
+ // Fallback responses
391
+ // -----------------------------------------------------------------------------
392
+
393
+ /**
394
+ * Pre-written copy the Runtime emits when an inference call fails or
395
+ * the safety pipeline blocks the model output. Keyed per `callKind` so
396
+ * a host_judge fallback is distinct from a host_recap fallback.
397
+ */
398
+ export const FallbackResponsesSchema = z.record(CallKindSchema, z.string().min(1));
399
+
400
+ // -----------------------------------------------------------------------------
401
+ // Content rating
402
+ // -----------------------------------------------------------------------------
403
+
404
+ /**
405
+ * Safety-pipeline floor values. Derived from
406
+ * {@link ExperienceContentRatingTierSchema} via
407
+ * `safetyFloorFromContentRatingTier` — do not set independently on
408
+ * the manifest.
409
+ */
410
+ export const ContentRatingFloorSchema = z.enum(['general', 'pg13', 'mature']);
411
+
412
+ export const ContentRatingAudienceSchema = z.enum([
413
+ 'consumer',
414
+ 'corporate',
415
+ 'private',
416
+ ]);
417
+
418
+ /**
419
+ * Unified Experience content-rating tier. Drives Safety posture,
420
+ * Portal badge copy, and smut-tier UI eligibility.
421
+ */
422
+ export const ExperienceContentRatingTierSchema = z.enum([
423
+ 'none',
424
+ 'pg13',
425
+ 'mature',
426
+ 'extra_smut',
427
+ ]);
428
+
429
+ /**
430
+ * `tier` is the single content-rating selector for an Experience.
431
+ * `audiences` lists the audience tags an Experience advertises against
432
+ * (e.g. `consumer`, `corporate`) — edited on the Studio Audience tab.
433
+ */
434
+ export const ContentRatingSchema = z.object({
435
+ tier: ExperienceContentRatingTierSchema,
436
+ audiences: z.array(ContentRatingAudienceSchema).min(1),
437
+ });
438
+
439
+ // -----------------------------------------------------------------------------
440
+ // Portal metadata (per Surfaces & Identity Addendum §4.3, chunk B11)
441
+ // -----------------------------------------------------------------------------
442
+
443
+ /**
444
+ * Occasions the User Portal's catalogue page uses to group titles for
445
+ * browsing. The internal Catalogue document still groups by mechanic
446
+ * (Improv / Argumentative / Deduction etc.); this enum is purely the
447
+ * Portal-display axis. Adding a value here lands in the chunk that
448
+ * needs it; do not weaken the enum to `z.string()` ("the Portal can
449
+ * filter on anything") — the Portal's filter UI is a hard-coded set.
450
+ */
451
+ export const OccasionTagSchema = z.enum([
452
+ 'party',
453
+ 'date_night',
454
+ 'family',
455
+ 'quick_game',
456
+ 'team_building',
457
+ ]);
458
+
459
+ export const GameplayImageSchema = z.object({
460
+ title: z.string().min(1),
461
+ imageUrl: z.string().url(),
462
+ });
463
+
464
+ export const GameplayVideoSchema = z.object({
465
+ title: z.string().min(1),
466
+ videoUrl: z.string().url(),
467
+ });
468
+
469
+ /**
470
+ * Per-Experience portal-page metadata. Renders the User Portal's
471
+ * `/experience/:id` page (per Surfaces & Identity Addendum §4.3).
472
+ * Every published Experience must ship this block; the chunk-B11
473
+ * build pipeline rejects manifests that omit it.
474
+ *
475
+ * URL fields are validated as URLs at the schema level; assets are
476
+ * R2-hosted in production. Content-rating badge and smut-tier UI are
477
+ * derived from the manifest-level `contentRating.tier`, not stored
478
+ * here.
479
+ *
480
+ * @deferred chunk-E1 — Rashomon (separate Lovable repo) ships a
481
+ * minimal `portalMetadata` block when that Experience lands in-tree.
482
+ * @deferred chunk-E2 — Flatterer (separate Lovable repo) ships a
483
+ * minimal `portalMetadata` block when that Experience lands in-tree.
484
+ */
485
+ export const PortalMetadataSchema = z
486
+ .object({
487
+ heroImageUrl: z.string().url(),
488
+ gameplayImages: z.array(GameplayImageSchema).default([]),
489
+ gameplayVideo: GameplayVideoSchema.optional(),
490
+ personaPreviewAudioUrl: z.string().url().optional(),
491
+ sampleRoundDescription: z.string().min(1),
492
+ occasionTags: z.array(OccasionTagSchema).min(1),
493
+ minPlayers: z.number().int().min(1).default(3),
494
+ maxPlayers: z.number().int().min(1).default(8),
495
+ estimatedDurationMinutes: z.number().int().min(1).default(30),
496
+ })
497
+ .superRefine((value, ctx) => {
498
+ if (value.maxPlayers < value.minPlayers) {
499
+ ctx.addIssue({
500
+ code: z.ZodIssueCode.custom,
501
+ message: 'maxPlayers must be greater than or equal to minPlayers',
502
+ path: ['maxPlayers'],
503
+ });
504
+ }
505
+ });
506
+
507
+ // -----------------------------------------------------------------------------
508
+ // Bundle reservations (Phase 2 — inert in MVP)
509
+ // -----------------------------------------------------------------------------
510
+
511
+ /**
512
+ * Phase-2 Bundle manifest mirrors (Chunk B18 reservation).
513
+ * Reserved for Phase 2; current Runtime ignores these. The validator
514
+ * accepts them but does not enforce structural cross-checks — P-bundles
515
+ * owns that logic.
516
+ *
517
+ * @deferred Phase 2 — P-bundles
518
+ */
519
+ export const BundleContextSlotsSchema = z.record(JsonValueSchema);
520
+
521
+ // -----------------------------------------------------------------------------
522
+ // Manifest top-level
523
+ // -----------------------------------------------------------------------------
524
+
525
+ /**
526
+ * The root Experience manifest. Every field is a stable contract: a
527
+ * future chunk that needs to add a top-level field bumps a manifest
528
+ * compatibility version (Phase 2) rather than ad-hoc widening this
529
+ * schema.
530
+ *
531
+ * `tenant` is `null` for first-party Experiences (Hello World, the
532
+ * MVP fixtures) and a `tnt_…` id otherwise. The Runtime trusts the
533
+ * tenant binding here only as a hint — authoritative tenant scoping
534
+ * lives on `experiences.tenant_id` in the catalogue (chunk A2).
535
+ *
536
+ * `createdAt` is an ISO-8601 timestamp string. The DB-side timestamp
537
+ * lives on `experience_versions.created_at`; this field captures the
538
+ * value the Studio pinned at manifest authoring time so the manifest
539
+ * blob is self-describing.
540
+ */
541
+ export const ManifestSchema = z.object({
542
+ id: ManifestIdSchema,
543
+ version: z.string().min(1),
544
+ name: z.string().min(1),
545
+ description: z.string().min(1),
546
+ tenant: ManifestTenantIdSchema.nullable().optional(),
547
+ creator: z.string().min(1),
548
+ createdAt: z.string().datetime({ offset: true }),
549
+ personaBindings: z.array(PersonaBindingSchema),
550
+ inferenceEnvelope: InferenceEnvelopeSchema,
551
+ stateSchema: StateSchemaSchema,
552
+ workflow: WorkflowSchema,
553
+ concurrentOpportunities: z.array(ConcurrentOpportunitySchema),
554
+ scoring: ScoringSchema,
555
+ lifecyclePolicies: z.array(LifecyclePolicySchema),
556
+ promptSlots: PromptSlotsSchema,
557
+ fallbackResponses: FallbackResponsesSchema,
558
+ widgetDependencies: z.array(z.string().min(1)),
559
+ contentRating: ContentRatingSchema,
560
+ portalMetadata: PortalMetadataSchema,
561
+ /** Reserved for Phase 2; current Runtime ignores these. */
562
+ bundleCompatible: z.boolean().default(false).optional(),
563
+ /** Reserved for Phase 2; current Runtime ignores these. */
564
+ bundleScopedDimensions: z.array(z.string().min(1)).default([]).optional(),
565
+ /** Reserved for Phase 2; current Runtime ignores these. */
566
+ bundleWriteDimensions: z.array(z.string().min(1)).default([]).optional(),
567
+ /** Reserved for Phase 2; current Runtime ignores these. */
568
+ bundleContextSlots: BundleContextSlotsSchema.default({}).optional(),
569
+ });
570
+
571
+ // -----------------------------------------------------------------------------
572
+ // Inferred types
573
+ // -----------------------------------------------------------------------------
574
+
575
+ export type ManifestId = z.infer<typeof ManifestIdSchema>;
576
+ export type ManifestTenantId = z.infer<typeof ManifestTenantIdSchema>;
577
+ export type PersonaId = z.infer<typeof PersonaIdSchema>;
578
+ export type PersonaBinding = z.infer<typeof PersonaBindingSchema>;
579
+ export type QualityTier = z.infer<typeof QualityTierSchema>;
580
+ export type InferenceEnvelope = z.infer<typeof InferenceEnvelopeSchema>;
581
+ export type StateSchema = z.infer<typeof StateSchemaSchema>;
582
+ export type Workflow = z.infer<typeof WorkflowSchema>;
583
+ export type ConcurrentOpportunity = z.infer<typeof ConcurrentOpportunitySchema>;
584
+ export type ScoringDimension = z.infer<typeof ScoringDimensionSchema>;
585
+ export type ScoringAggregator = z.infer<typeof ScoringAggregatorSchema>;
586
+ export type AwardCriterion = z.infer<typeof AwardCriterionSchema>;
587
+ export type AwardDefinition = z.infer<typeof AwardDefinitionSchema>;
588
+ export type Scoring = z.infer<typeof ScoringSchema>;
589
+ export type LifecycleSituation = z.infer<typeof LifecycleSituationSchema>;
590
+ export type LifecycleAction = z.infer<typeof LifecycleActionSchema>;
591
+ export type LifecyclePolicy = z.infer<typeof LifecyclePolicySchema>;
592
+ export type PromptSlotValue = z.infer<typeof PromptSlotValueSchema>;
593
+ export type CallKind = z.infer<typeof CallKindSchema>;
594
+ export type PromptSlots = z.infer<typeof PromptSlotsSchema>;
595
+ export type FallbackResponses = z.infer<typeof FallbackResponsesSchema>;
596
+ export type ContentRatingFloor = z.infer<typeof ContentRatingFloorSchema>;
597
+ export type ContentRatingAudience = z.infer<typeof ContentRatingAudienceSchema>;
598
+ export type ExperienceContentRatingTier = z.infer<
599
+ typeof ExperienceContentRatingTierSchema
600
+ >;
601
+ export type ContentRating = z.infer<typeof ContentRatingSchema>;
602
+ export type OccasionTag = z.infer<typeof OccasionTagSchema>;
603
+ export type GameplayImage = z.infer<typeof GameplayImageSchema>;
604
+ export type GameplayVideo = z.infer<typeof GameplayVideoSchema>;
605
+ export type PortalMetadata = z.infer<typeof PortalMetadataSchema>;
606
+ export type BundleContextSlots = z.infer<typeof BundleContextSlotsSchema>;
607
+ export type Manifest = z.infer<typeof ManifestSchema>;