@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.
- package/CHANGELOG.md +6 -0
- package/package.json +25 -0
- package/src/content-rating.test.ts +26 -0
- package/src/content-rating.ts +51 -0
- package/src/fixtures.ts +229 -0
- package/src/index.ts +126 -0
- package/src/manifest.test.ts +351 -0
- package/src/manifest.ts +607 -0
- package/src/phase.test.ts +227 -0
- package/src/phase.ts +304 -0
- package/src/portal-display.test.ts +54 -0
- package/src/portal-display.ts +40 -0
- package/src/validate.test.ts +310 -0
- package/src/validate.ts +385 -0
package/src/manifest.ts
ADDED
|
@@ -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>;
|