@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 ADDED
@@ -0,0 +1,6 @@
1
+ # `@wibly/internal-manifest` — Changelog
2
+
3
+ ## 0.1.1 — 2026-05-30
4
+
5
+ Initial public npm release. Internal runtime dependency of `@wibly/sdk` and
6
+ `@wibly/sdk-testkit` — not a supported direct import surface for game bundles.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@wibly/internal-manifest",
3
+ "version": "0.1.1",
4
+ "description": "Wibly @wibly/internal-manifest",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "license": "UNLICENSED",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/wibly/wibly"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@wibly/internal-protocol": "0.1.1",
21
+ "@wibly/internal-shared": "0.1.1",
22
+ "zod": "^3.25.76"
23
+ },
24
+ "peerDependencies": {}
25
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ isSmutTierCapable,
5
+ portalContentRatingLabel,
6
+ safetyFloorFromContentRatingTier,
7
+ } from './content-rating.js';
8
+
9
+ describe('content-rating mappers', () => {
10
+ it('maps tiers to safety floors', () => {
11
+ expect(safetyFloorFromContentRatingTier('none')).toBe('general');
12
+ expect(safetyFloorFromContentRatingTier('pg13')).toBe('pg13');
13
+ expect(safetyFloorFromContentRatingTier('mature')).toBe('mature');
14
+ expect(safetyFloorFromContentRatingTier('extra_smut')).toBe('mature');
15
+ });
16
+
17
+ it('enables smut tier only for extra_smut', () => {
18
+ expect(isSmutTierCapable('extra_smut')).toBe(true);
19
+ expect(isSmutTierCapable('mature')).toBe(false);
20
+ });
21
+
22
+ it('labels portal badges', () => {
23
+ expect(portalContentRatingLabel('none')).toBe('All ages');
24
+ expect(portalContentRatingLabel('extra_smut')).toBe('Extra Smut');
25
+ });
26
+ });
@@ -0,0 +1,51 @@
1
+ import type {
2
+ ContentRatingFloor,
3
+ ExperienceContentRatingTier,
4
+ } from './manifest.js';
5
+
6
+ /**
7
+ * Map the unified Experience content-rating tier to the Safety
8
+ * pipeline floor (`general` | `pg13` | `mature`).
9
+ */
10
+ export const safetyFloorFromContentRatingTier = (
11
+ tier: ExperienceContentRatingTier,
12
+ ): ContentRatingFloor => {
13
+ switch (tier) {
14
+ case 'none':
15
+ return 'general';
16
+ case 'pg13':
17
+ return 'pg13';
18
+ case 'mature':
19
+ case 'extra_smut':
20
+ return 'mature';
21
+ default: {
22
+ const _exhaustive: never = tier;
23
+ return _exhaustive;
24
+ }
25
+ }
26
+ };
27
+
28
+ /** Whether the User Portal should render the smut-tier selector. */
29
+ export const isSmutTierCapable = (
30
+ tier: ExperienceContentRatingTier,
31
+ ): boolean => tier === 'extra_smut';
32
+
33
+ /** Human-readable badge label for catalogue and detail pages. */
34
+ export const portalContentRatingLabel = (
35
+ tier: ExperienceContentRatingTier,
36
+ ): string => {
37
+ switch (tier) {
38
+ case 'none':
39
+ return 'All ages';
40
+ case 'pg13':
41
+ return 'PG-13';
42
+ case 'mature':
43
+ return 'Mature';
44
+ case 'extra_smut':
45
+ return 'Extra Smut';
46
+ default: {
47
+ const _exhaustive: never = tier;
48
+ return _exhaustive;
49
+ }
50
+ }
51
+ };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Canonical manifest fixtures used by the schema and validate-flow
3
+ * tests. The baseline `validManifestFixture` is a minimal-but-complete
4
+ * Experience that satisfies every required field and every structural
5
+ * check; per-failure-case fixtures derive from it via shallow clone +
6
+ * mutation in the test files (per the chunk-B1 acceptance: "one fixture
7
+ * per failure case so the AI doesn't conflate two errors when only one
8
+ * was triggered").
9
+ *
10
+ * Not imported by runtime code (services, the Admin app, the Player
11
+ * shell). The dev seed (`packages/db/src/seed-dev.ts`) is allowed —
12
+ * the seed is itself a fixture-loader, and using this shared baseline
13
+ * keeps the dev-seeded `experience_versions.manifest` blob aligned with
14
+ * the schema the Inference Gateway (chunk B2) loads at request time.
15
+ */
16
+
17
+ import type { Manifest } from './manifest.js';
18
+ import type { WorkflowSideEffect } from './phase.js';
19
+
20
+ /** Shallow patch for per-test / fixture mutations on a workflow phase. */
21
+ export type PhaseTestPatch = {
22
+ readonly id?: string;
23
+ readonly transitions?: ReadonlyArray<{ readonly to: string; readonly when?: string }>;
24
+ readonly sideEffects?: readonly WorkflowSideEffect[];
25
+ };
26
+
27
+ /**
28
+ * Mutate one phase in a cloned manifest. Manifest phases are typed
29
+ * readonly; tests use this helper instead of unsafe casts.
30
+ */
31
+ export const patchPhase = (
32
+ manifest: Manifest,
33
+ phaseId: string,
34
+ patch: PhaseTestPatch,
35
+ ): void => {
36
+ const phase = manifest.workflow.phases.find((p) => p.id === phaseId);
37
+ if (!phase) return;
38
+ Object.assign(phase as object, patch);
39
+ };
40
+
41
+ /** Mutate a phase by index (same semantics as `patchPhase`). */
42
+ export const patchPhaseAt = (
43
+ manifest: Manifest,
44
+ index: number,
45
+ patch: PhaseTestPatch,
46
+ ): void => {
47
+ const phase = manifest.workflow.phases[index];
48
+ if (!phase) return;
49
+ Object.assign(phase as object, patch);
50
+ };
51
+
52
+ const TENANT_ID = 'tnt_V1StGXR8_Z5jdHi6B_myT';
53
+ const EXPERIENCE_ID = 'exp_HelloWorldFixture000_';
54
+ const PERSONA_ID = 'per_CrumbHostPersona00_';
55
+ const FIXED_ISO = '2026-01-01T00:00:00.000Z';
56
+
57
+ /**
58
+ * A minimal Hello World shape: lobby → guess → resolved, with one
59
+ * persona binding, one quality tier, one award. Designed so a
60
+ * single-field mutation can trigger any one of the structural checks
61
+ * without colliding with another rule.
62
+ */
63
+ export const validManifestFixture: Manifest = {
64
+ id: EXPERIENCE_ID,
65
+ version: '1.0.0',
66
+ name: 'Hello World',
67
+ description: 'The smallest manifest the validator accepts.',
68
+ tenant: TENANT_ID,
69
+ creator: 'wibly-platform',
70
+ createdAt: FIXED_ISO,
71
+ personaBindings: [
72
+ { role: 'host', personaId: PERSONA_ID },
73
+ ],
74
+ inferenceEnvelope: {
75
+ maxLlmCallsPerSession: 20,
76
+ maxTokensInPerCall: 2048,
77
+ maxTokensOutPerCall: 1024,
78
+ maxTtsSecondsPerSession: 120,
79
+ qualityTiers: ['fast', 'standard', 'premium', 'creative'],
80
+ },
81
+ stateSchema: {
82
+ session: { type: 'object', properties: { round: { type: 'number' } } },
83
+ host: { type: 'object', properties: {} },
84
+ playerPublic: { type: 'object', properties: { score: { type: 'number' } } },
85
+ playerPrivate: { type: 'object', properties: {} },
86
+ team: { type: 'object', properties: {} },
87
+ },
88
+ workflow: {
89
+ initialPhase: 'lobby',
90
+ phases: [
91
+ {
92
+ id: 'lobby',
93
+ inputSet: { actors: ['host'], inputType: 'start' },
94
+ collectionRule: { kind: 'manual' },
95
+ transitions: [{ to: 'guess' }],
96
+ sideEffects: [],
97
+ },
98
+ {
99
+ id: 'guess',
100
+ inputSet: { actors: ['player'], inputType: 'guess' },
101
+ collectionRule: { kind: 'all_respond' },
102
+ transitions: [{ to: 'resolved' }],
103
+ sideEffects: [],
104
+ },
105
+ {
106
+ id: 'resolved',
107
+ inputSet: { actors: ['host'], inputType: 'next' },
108
+ collectionRule: { kind: 'manual' },
109
+ transitions: [{ to: 'lobby', when: 'play_again' }],
110
+ sideEffects: [],
111
+ },
112
+ ],
113
+ },
114
+ concurrentOpportunities: [
115
+ {
116
+ id: 'side_chat',
117
+ attachedToPhases: ['guess'],
118
+ inputSet: { actors: ['player'], inputType: 'chatter' },
119
+ collectionRule: { kind: 'timeout', ms: 30_000 },
120
+ scoringEffect: { kind: 'noop' },
121
+ },
122
+ ],
123
+ scoring: {
124
+ dimensions: [
125
+ { id: 'accuracy', label: 'Accuracy', weight: 1, scaleMin: 0, scaleMax: 10 },
126
+ ],
127
+ aggregators: [{ kind: 'sum' }],
128
+ awards: [
129
+ {
130
+ id: 'top_guesser',
131
+ label: 'Top Guesser',
132
+ dimensionId: 'accuracy',
133
+ criterion: { kind: 'top_n', n: 1 },
134
+ },
135
+ ],
136
+ },
137
+ lifecyclePolicies: [
138
+ {
139
+ situation: 'player_disconnect',
140
+ action: {
141
+ kind: 'pause_session',
142
+ timeoutMs: 30_000,
143
+ fallback: 'continue_without_them',
144
+ },
145
+ },
146
+ ],
147
+ promptSlots: {
148
+ experienceSystem: 'You are running Hello World, a tiny test Experience.',
149
+ callTypes: {
150
+ host_open_phase: 'Open the next phase with one short sentence.',
151
+ host_recap: {
152
+ template: 'Recap the round for {playerCount} players.',
153
+ vars: ['playerCount'],
154
+ },
155
+ },
156
+ },
157
+ fallbackResponses: {
158
+ host_open_phase: 'Welcome — let us begin.',
159
+ host_recap: 'Thanks for playing.',
160
+ },
161
+ widgetDependencies: ['Lobby', 'GuessInput', 'Leaderboard'],
162
+ contentRating: {
163
+ tier: 'none',
164
+ audiences: ['consumer'],
165
+ },
166
+ portalMetadata: {
167
+ heroImageUrl: 'https://assets.wibly.example/fixtures/hello-world-hero.png',
168
+ gameplayImages: [],
169
+ sampleRoundDescription:
170
+ 'A demonstration round where players guess a number and the host reveals the answer.',
171
+ occasionTags: ['party', 'quick_game'],
172
+ minPlayers: 3,
173
+ maxPlayers: 8,
174
+ estimatedDurationMinutes: 30,
175
+ },
176
+ };
177
+
178
+ /**
179
+ * Deep clone of the fixture so per-test mutations don't bleed into
180
+ * other tests. `structuredClone` is in the Node 20 std lib (no polyfill
181
+ * needed) and handles the JSON-shaped state-schema slices correctly.
182
+ *
183
+ * Note: the cloned fixture's `workflow.phases[*].sideEffects` is
184
+ * `undefined` because `validManifestFixture` does not set the field
185
+ * — the `PhaseSchema.default([])` on `sideEffects` fills it in when
186
+ * the fixture is parsed via `ManifestSchema` / `validateManifest`. If
187
+ * a caller needs the parsed (post-default) shape, run the clone
188
+ * through `validateManifest` first.
189
+ */
190
+ export const cloneFixture = (): Manifest =>
191
+ structuredClone(validManifestFixture);
192
+
193
+ /**
194
+ * Chunk B8c — fixture variant that exercises the per-phase
195
+ * `sideEffects` declaration. Adds:
196
+ * - an `inference` side effect on `guess` that writes the structured
197
+ * output into `/session/inference/host_judge`,
198
+ * - a `scoring` side effect on `resolved` that credits a fixed +1 to
199
+ * the active player on the `accuracy` dimension,
200
+ *
201
+ * The integration tests under `services/runtime/src/__integration__/`
202
+ * consume this fixture; the unit tests stick with the bare
203
+ * `validManifestFixture` so the `sideEffects: []` default path stays
204
+ * exercised.
205
+ */
206
+ export const cloneFixtureWithInferenceSideEffect = (): Manifest => {
207
+ const fixture = cloneFixture();
208
+ patchPhase(fixture, 'guess', {
209
+ sideEffects: [
210
+ {
211
+ kind: 'inference',
212
+ callKind: 'host_judge',
213
+ qualityTier: 'standard',
214
+ targetPath: '/session/inference/host_judge',
215
+ },
216
+ ],
217
+ });
218
+ patchPhase(fixture, 'resolved', {
219
+ sideEffects: [
220
+ {
221
+ kind: 'scoring',
222
+ dimension: 'accuracy',
223
+ value: 1,
224
+ source: 'compute',
225
+ },
226
+ ],
227
+ });
228
+ return fixture;
229
+ };
package/src/index.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `@platform/manifest` — Experience-manifest schemas and validator.
3
+ *
4
+ * Per Vibecode Dev Plan §2.5 the manifest blob lives in a `JSONB`
5
+ * column on `experience_versions`; this package owns the Zod schema
6
+ * that gates writes to that column and the `validateManifest` function
7
+ * that runs on every read / publish boundary. Per the chunk-B1 trap,
8
+ * `validateManifest` is the *only* validation entrypoint downstream
9
+ * services should reach for — ad-hoc Zod parses or shape checks
10
+ * elsewhere are a regression.
11
+ *
12
+ * Built in chunk B1; see `docs/chunks/B1.md` for the close note.
13
+ */
14
+
15
+ export {
16
+ ActorKindSchema,
17
+ CollectionRuleSchema,
18
+ InferenceSideEffectSchema,
19
+ InputSetSchema,
20
+ PersonaMemorySideEffectSchema,
21
+ PhaseSchema,
22
+ ScoringSideEffectSchema,
23
+ StateWriteSideEffectSchema,
24
+ TransitionSchema,
25
+ WorkflowSideEffectSchema,
26
+ type ActorKind,
27
+ type CollectionRule,
28
+ type InputSet,
29
+ type ManifestPhase,
30
+ type Phase,
31
+ type Transition,
32
+ type WorkflowSideEffect,
33
+ } from './phase.js';
34
+
35
+ export {
36
+ isSmutTierCapable,
37
+ portalContentRatingLabel,
38
+ safetyFloorFromContentRatingTier,
39
+ } from './content-rating.js';
40
+
41
+ export {
42
+ DEFAULT_ESTIMATED_DURATION_MINUTES,
43
+ DEFAULT_MAX_PLAYERS,
44
+ DEFAULT_MIN_PLAYERS,
45
+ portalContentCategoryLabel,
46
+ portalDurationLabel,
47
+ portalPlayersLabel,
48
+ } from './portal-display.js';
49
+
50
+ export {
51
+ AwardCriterionSchema,
52
+ AwardDefinitionSchema,
53
+ BundleContextSlotsSchema,
54
+ CallKindSchema,
55
+ ConcurrentOpportunitySchema,
56
+ ContentRatingAudienceSchema,
57
+ ContentRatingFloorSchema,
58
+ ContentRatingSchema,
59
+ ExperienceContentRatingTierSchema,
60
+ FallbackResponsesSchema,
61
+ GameplayImageSchema,
62
+ GameplayVideoSchema,
63
+ InferenceEnvelopeSchema,
64
+ LifecycleActionSchema,
65
+ LifecyclePolicySchema,
66
+ LifecycleSituationSchema,
67
+ ManifestIdSchema,
68
+ ManifestSchema,
69
+ ManifestTenantIdSchema,
70
+ OccasionTagSchema,
71
+ PersonaBindingSchema,
72
+ PersonaIdSchema,
73
+ PortalMetadataSchema,
74
+ PromptSlotsSchema,
75
+ PromptSlotValueSchema,
76
+ QualityTierSchema,
77
+ ScoringAggregatorSchema,
78
+ ScoringDimensionSchema,
79
+ ScoringSchema,
80
+ StateSchemaSchema,
81
+ WorkflowSchema,
82
+ type AwardCriterion,
83
+ type AwardDefinition,
84
+ type BundleContextSlots,
85
+ type CallKind,
86
+ type ConcurrentOpportunity,
87
+ type ContentRating,
88
+ type ContentRatingAudience,
89
+ type ContentRatingFloor,
90
+ type ExperienceContentRatingTier,
91
+ type FallbackResponses,
92
+ type GameplayImage,
93
+ type GameplayVideo,
94
+ type InferenceEnvelope,
95
+ type JsonValue,
96
+ type LifecycleAction,
97
+ type LifecyclePolicy,
98
+ type LifecycleSituation,
99
+ type Manifest,
100
+ type ManifestId,
101
+ type ManifestTenantId,
102
+ type OccasionTag,
103
+ type PersonaBinding,
104
+ type PersonaId,
105
+ type PortalMetadata,
106
+ type PromptSlots,
107
+ type PromptSlotValue,
108
+ type QualityTier,
109
+ type Scoring,
110
+ type ScoringAggregator,
111
+ type ScoringDimension,
112
+ type StateSchema,
113
+ type Workflow,
114
+ } from './manifest.js';
115
+
116
+ export {
117
+ validateManifest,
118
+ type ValidationError,
119
+ type ValidationErrorCode,
120
+ } from './validate.js';
121
+
122
+ export {
123
+ cloneFixture,
124
+ cloneFixtureWithInferenceSideEffect,
125
+ validManifestFixture,
126
+ } from './fixtures.js';