@forgeax/engine-project 0.1.21 → 0.1.24

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.
@@ -16,13 +16,15 @@ function makeReadThrow(err: Error): (path: string) => Promise<string> {
16
16
  const VALID_FORGE_JSON = JSON.stringify({
17
17
  id: 'test-game',
18
18
  name: 'Test Game',
19
- schemaVersion: '1.0.0',
19
+ schemaVersion: '2.0.0',
20
+ plugins: [],
20
21
  });
21
22
 
22
23
  const VALID_WITH_DEFAULT_SCENE = JSON.stringify({
23
24
  id: 'test-game',
24
25
  name: 'Test Game',
25
- schemaVersion: '1.0.0',
26
+ schemaVersion: '2.0.0',
27
+ plugins: [],
26
28
  defaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
27
29
  });
28
30
 
@@ -31,27 +33,31 @@ const INVALID_JSON = '{id: broken';
31
33
  const WITH_SCENES_ARRAY = JSON.stringify({
32
34
  id: 'has-scenes',
33
35
  name: 'Has Scenes',
34
- schemaVersion: '1.0.0',
36
+ schemaVersion: '2.0.0',
37
+ plugins: [],
35
38
  scenes: [{ id: 'l1', name: 'Level 1', pack: 'scenes/level1.pack.json' }],
36
39
  });
37
40
 
38
41
  const WITH_BAD_GUID = JSON.stringify({
39
42
  id: 'bad-guid',
40
43
  name: 'Bad GUID',
41
- schemaVersion: '1.0.0',
44
+ schemaVersion: '2.0.0',
45
+ plugins: [],
42
46
  defaultScene: 'not-a-guid',
43
47
  });
44
48
 
45
49
  const WITHOUT_DEFAULT_SCENE = JSON.stringify({
46
50
  id: 'no-default',
47
51
  name: 'No Default',
48
- schemaVersion: '1.0.0',
52
+ schemaVersion: '2.0.0',
53
+ plugins: [],
49
54
  });
50
55
 
51
- const WITH_NPC_POLICY = JSON.stringify({
56
+ const WITH_LEGACY_POLICY = JSON.stringify({
52
57
  id: 'npc-game',
53
58
  name: 'NPC Game',
54
- schemaVersion: '1.0.0',
59
+ schemaVersion: '2.0.0',
60
+ plugins: [],
55
61
  npc: {
56
62
  model: 'deepseek-v4-flash-openai',
57
63
  maxTokens: 220,
@@ -124,7 +130,7 @@ describe('loadGameProject — return path 5: valid forge.json', () => {
124
130
  expect(result.value).toBeDefined();
125
131
  expect(result.value.id).toBe('test-game');
126
132
  expect(result.value.name).toBe('Test Game');
127
- expect(result.value.schemaVersion).toBe('1.0.0');
133
+ expect(result.value.schemaVersion).toBe('2.0.0');
128
134
  }
129
135
  });
130
136
 
@@ -145,13 +151,10 @@ describe('loadGameProject — return path 5: valid forge.json', () => {
145
151
  }
146
152
  });
147
153
 
148
- it('accepts the Digital Life NPC policy in forge.json', async () => {
149
- const result = await loadGameProject(makeRead(WITH_NPC_POLICY));
150
- expect(result.ok).toBe(true);
151
- if (result.ok) {
152
- expect(result.value.npc?.model).toBe('deepseek-v4-flash-openai');
153
- expect(result.value.npc?.budget?.maxConcurrent).toBe(4);
154
- }
154
+ it('rejects the removed NPC policy instead of dual-reading it', async () => {
155
+ const result = await loadGameProject(makeRead(WITH_LEGACY_POLICY));
156
+ expect(result.ok).toBe(false);
157
+ if (!result.ok) expect(result.error.code).toBe('forge-unknown-field');
155
158
  });
156
159
 
157
160
  it('is idempotent — same read returns same result', async () => {
@@ -10,14 +10,16 @@ function makeRead(content: string): (path: string) => Promise<string> {
10
10
  const VALID_WITH_SCENE = JSON.stringify({
11
11
  id: 'test-game',
12
12
  name: 'Test Game',
13
- schemaVersion: '1.0.0',
13
+ schemaVersion: '2.0.0',
14
+ plugins: [],
14
15
  defaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
15
16
  });
16
17
 
17
18
  const WITHOUT_SCENE = JSON.stringify({
18
19
  id: 'no-scene',
19
20
  name: 'No Scene',
20
- schemaVersion: '1.0.0',
21
+ schemaVersion: '2.0.0',
22
+ plugins: [],
21
23
  });
22
24
 
23
25
  // ── signature verification ──────────────────────────────────────────────────
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { GameProjectSchema } from '../schema.js';
4
+
5
+ const validProject = {
6
+ id: 'schema-v2-game',
7
+ name: 'Schema V2 Game',
8
+ schemaVersion: '2.0.0',
9
+ plugins: [],
10
+ };
11
+
12
+ describe('GameProjectSchema v2', () => {
13
+ it('requires the strict schema 2.0 project shape and allows an empty EntryTree', () => {
14
+ const result = GameProjectSchema.safeParse(validProject);
15
+
16
+ expect(result.success).toBe(true);
17
+ if (result.success) expect(result.data.plugins).toEqual([]);
18
+ });
19
+
20
+ it('rejects every schema version other than 2.0.0', () => {
21
+ for (const schemaVersion of ['1.0.0', '2.1.0', 'v2']) {
22
+ const result = GameProjectSchema.safeParse({ ...validProject, schemaVersion });
23
+ expect(result.success, schemaVersion).toBe(false);
24
+ }
25
+ });
26
+
27
+ it('requires plugins as the only installable capability root', () => {
28
+ const { plugins: _plugins, ...withoutPlugins } = validProject;
29
+ const result = GameProjectSchema.safeParse(withoutPlugins);
30
+
31
+ expect(result.success).toBe(false);
32
+ });
33
+
34
+ it('rejects legacy root fields instead of preserving a dual manifest path', () => {
35
+ const legacyFields = [
36
+ 'entry',
37
+ 'executionEntry',
38
+ 'physics',
39
+ 'pointerLock',
40
+ 'input',
41
+ 'preview',
42
+ 'npc',
43
+ ] as const;
44
+
45
+ for (const field of legacyFields) {
46
+ const result = GameProjectSchema.safeParse({
47
+ ...validProject,
48
+ [field]: field === 'pointerLock' ? true : './legacy.ts',
49
+ });
50
+ expect(result.success, field).toBe(false);
51
+ }
52
+ });
53
+
54
+ it('accepts enabled, disabled, and realm-inherited Entry states', () => {
55
+ const result = GameProjectSchema.safeParse({
56
+ ...validProject,
57
+ plugins: [
58
+ { id: 'enabled', name: './enabled.plugin.ts', realm: 'engine' },
59
+ { id: 'disabled', name: './disabled.plugin.ts', disabled: true, realm: 'engine' },
60
+ { id: 'inherited', name: './inherited.plugin.ts' },
61
+ ],
62
+ });
63
+
64
+ expect(result.success).toBe(true);
65
+ });
66
+
67
+ it('rejects nullable disabled state because absence means enabled', () => {
68
+ const result = GameProjectSchema.safeParse({
69
+ ...validProject,
70
+ plugins: [{ id: 'nullable', name: './nullable.plugin.ts', disabled: null }],
71
+ });
72
+
73
+ expect(result.success).toBe(false);
74
+ });
75
+
76
+ it('accepts a Group root with nested children and inherited realm', () => {
77
+ const result = GameProjectSchema.safeParse({
78
+ ...validProject,
79
+ plugins: [
80
+ {
81
+ id: 'game',
82
+ name: 'cordis:group',
83
+ group: true,
84
+ realm: 'engine',
85
+ config: [
86
+ { id: 'world', name: './world.plugin.ts' },
87
+ { id: 'player', name: './player.plugin.ts', inject: ['world'] },
88
+ ],
89
+ },
90
+ ],
91
+ });
92
+
93
+ expect(result.success).toBe(true);
94
+ });
95
+
96
+ it('rejects a Group whose config is not an EntryTree', () => {
97
+ const result = GameProjectSchema.safeParse({
98
+ ...validProject,
99
+ plugins: [{ id: 'invalid-group', name: 'cordis:group', group: true, config: {} }],
100
+ });
101
+
102
+ expect(result.success).toBe(false);
103
+ });
104
+
105
+ it('rejects duplicate Entry ids across a Group ownership path', () => {
106
+ const result = GameProjectSchema.safeParse({
107
+ ...validProject,
108
+ plugins: [
109
+ { id: 'same', name: './root.plugin.ts' },
110
+ {
111
+ id: 'group',
112
+ name: 'cordis:group',
113
+ group: true,
114
+ config: [{ id: 'same', name: './child.plugin.ts' }],
115
+ },
116
+ ],
117
+ });
118
+
119
+ expect(result.success).toBe(false);
120
+ });
121
+
122
+ it('rejects an unknown physical realm before module activation', () => {
123
+ const result = GameProjectSchema.safeParse({
124
+ ...validProject,
125
+ plugins: [{ id: 'unknown', name: './unknown.plugin.ts', realm: 'worker' }],
126
+ });
127
+
128
+ expect(result.success).toBe(false);
129
+ });
130
+ });
@@ -1,99 +1,22 @@
1
- // schema.test.ts — w2: GameProjectSchema + GuidString refinement tests
2
1
  import { describe, expect, it } from 'vitest';
3
2
  import { GameProjectSchema, GuidString } from '../schema.js';
4
3
 
5
- // ── valid full forge.json ──────────────────────────────────────────────────
6
- describe('GameProjectSchema — acceptance', () => {
7
- it('accepts a valid full forge.json', () => {
8
- const result = GameProjectSchema.safeParse({
9
- id: 'hellforge',
10
- name: 'Hellforge',
11
- schemaVersion: '1.0.0',
12
- defaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
13
- physics: '3d',
14
- pointerLock: true,
15
- input: 'fps',
16
- preview: {
17
- skin: {
18
- sceneGuid: '11111111-1111-1111-1111-111111111111',
19
- clipGuids: [],
20
- clipDefault: 'idle',
21
- scale: 1,
22
- pos: [0, 0],
23
- },
24
- },
25
- });
26
- expect(result.success).toBe(true);
27
- });
28
-
29
- it('accepts minimal forge.json with only id+name+schemaVersion', () => {
30
- const result = GameProjectSchema.safeParse({
31
- id: 'minimal',
32
- name: 'Minimal Game',
33
- schemaVersion: '1.0.0',
34
- });
35
- expect(result.success).toBe(true);
36
- });
37
-
38
- it('accepts forge.json without entry (entry is optional)', () => {
39
- const result = GameProjectSchema.safeParse({
40
- id: 'no-entry',
41
- name: 'No Entry Game',
42
- schemaVersion: '1.0.0',
43
- });
44
- expect(result.success).toBe(true);
45
- });
4
+ const base = {
5
+ id: 'test-game',
6
+ name: 'Test Game',
7
+ schemaVersion: '2.0.0',
8
+ plugins: [],
9
+ };
46
10
 
47
- it('accepts forge.json without defaultScene (defaultScene is optional)', () => {
48
- const result = GameProjectSchema.safeParse({
49
- id: 'no-default',
50
- name: 'No Default Game',
51
- schemaVersion: '1.0.0',
52
- });
53
- expect(result.success).toBe(true);
54
- });
55
-
56
- it('keeps forge.json as the engine-project manifest, not a Catalog asset', () => {
57
- const result = GameProjectSchema.safeParse({
58
- id: 'manifest-only',
59
- name: 'Manifest Only',
60
- schemaVersion: '1.0.0',
61
- });
62
- expect(result.success).toBe(true);
63
- if (result.success) {
64
- expect(result.data).not.toHaveProperty('guid');
65
- expect(result.data).not.toHaveProperty('sourceKey');
66
- expect(result.data).not.toHaveProperty('assets');
67
- }
68
- });
69
-
70
- it('accepts forge.json with entry field present', () => {
71
- const result = GameProjectSchema.safeParse({
72
- id: 'with-entry',
73
- name: 'With Entry',
74
- schemaVersion: '1.0.0',
75
- entry: 'main.ts',
76
- });
77
- expect(result.success).toBe(true);
11
+ describe('GameProjectSchema schema 2.0', () => {
12
+ it('accepts a minimal strict manifest', () => {
13
+ expect(GameProjectSchema.safeParse(base).success).toBe(true);
78
14
  });
79
15
 
80
- it('accepts an explicit realm-local execution entry', () => {
16
+ it('accepts defaultScene and a nested native Entry tree', () => {
81
17
  const result = GameProjectSchema.safeParse({
82
- id: 'worker-game',
83
- name: 'Worker Game',
84
- schemaVersion: '1.0.0',
85
- entry: 'main.ts',
86
- executionEntry: 'runtime.ts',
87
- });
88
- expect(result.success).toBe(true);
89
- if (result.success) expect(result.data.executionEntry).toBe('runtime.ts');
90
- });
91
-
92
- it('accepts a DSH-aligned plugin Entry tree with ForgeaX realm placement', () => {
93
- const result = GameProjectSchema.safeParse({
94
- id: 'plugin-game',
95
- name: 'Plugin Game',
96
- schemaVersion: '1.0.0',
18
+ ...base,
19
+ defaultScene: '15acc839-d847-527c-8284-bfb36d7c50de',
97
20
  plugins: [
98
21
  {
99
22
  id: 'engine-features',
@@ -114,11 +37,49 @@ describe('GameProjectSchema — acceptance', () => {
114
37
  expect(result.success).toBe(true);
115
38
  });
116
39
 
117
- it('rejects duplicate plugin Entry ids anywhere in one tree', () => {
40
+ it('rejects every schema version except 2.0.0', () => {
41
+ for (const schemaVersion of ['1.0.0', '2.1.0', 'v2']) {
42
+ expect(GameProjectSchema.safeParse({ ...base, schemaVersion }).success).toBe(false);
43
+ }
44
+ });
45
+
46
+ it('requires plugins and rejects nullable entry controls', () => {
47
+ const { plugins: _plugins, ...withoutPlugins } = base;
48
+ expect(GameProjectSchema.safeParse(withoutPlugins).success).toBe(false);
49
+ expect(GameProjectSchema.safeParse({ ...base, disabled: null }).success).toBe(false);
50
+ expect(
51
+ GameProjectSchema.safeParse({
52
+ ...base,
53
+ plugins: [{ id: 'nullable', name: './x', disabled: null }],
54
+ }).success,
55
+ ).toBe(false);
56
+ });
57
+
58
+ it('rejects every legacy root field without a compatibility parser', () => {
59
+ for (const field of [
60
+ 'entry',
61
+ 'executionEntry',
62
+ 'physics',
63
+ 'pointerLock',
64
+ 'input',
65
+ 'preview',
66
+ 'npc',
67
+ ]) {
68
+ expect(GameProjectSchema.safeParse({ ...base, [field]: 'legacy' }).success).toBe(false);
69
+ }
70
+ });
71
+
72
+ it('rejects unknown top-level fields', () => {
73
+ const result = GameProjectSchema.safeParse({ ...base, scenes: [] });
74
+ expect(result.success).toBe(false);
75
+ if (!result.success) {
76
+ expect(result.error.issues.some((issue) => issue.code === 'unrecognized_keys')).toBe(true);
77
+ }
78
+ });
79
+
80
+ it('rejects duplicate plugin ids across one Entry tree', () => {
118
81
  const result = GameProjectSchema.safeParse({
119
- id: 'duplicate-plugins',
120
- name: 'Duplicate Plugins',
121
- schemaVersion: '1.0.0',
82
+ ...base,
122
83
  plugins: [
123
84
  { id: 'same', name: './one.ts' },
124
85
  {
@@ -133,185 +94,16 @@ describe('GameProjectSchema — acceptance', () => {
133
94
  });
134
95
  });
135
96
 
136
- // ── GuidString refinement ───────────────────────────────────────────────────
137
- describe('GuidString refinement', () => {
138
- it('accepts valid 36-char dash-form UUID v4', () => {
139
- const result = GuidString.safeParse('d953a1db-483a-4b7d-8b71-b8f144488c48');
140
- expect(result.success).toBe(true);
141
- });
142
-
143
- it('accepts valid 36-char dash-form UUID v7', () => {
144
- const result = GuidString.safeParse('15acc839-d847-527c-8284-bfb36d7c50de');
145
- expect(result.success).toBe(true);
146
- });
147
-
148
- it('accepts valid 36-char dash-form UUID (uppercase hex)', () => {
149
- const result = GuidString.safeParse('7B4D43D4-5B19-5903-8966-F89671D21565');
150
- expect(result.success).toBe(true);
151
- });
152
-
153
- it('rejects malformed UUID string (too short)', () => {
154
- const result = GuidString.safeParse('not-a-guid');
155
- expect(result.success).toBe(false);
156
- });
157
-
158
- it('rejects plain slug string (non-UUID)', () => {
159
- const result = GuidString.safeParse('rogue-encampment');
160
- expect(result.success).toBe(false);
161
- });
162
-
163
- it('rejects empty string', () => {
164
- const result = GuidString.safeParse('');
165
- expect(result.success).toBe(false);
97
+ describe('GuidString', () => {
98
+ it('accepts UUID values used by scene assets', () => {
99
+ expect(GuidString.safeParse('d953a1db-483a-4b7d-8b71-b8f144488c48').success).toBe(true);
100
+ expect(GuidString.safeParse('15acc839-d847-527c-8284-bfb36d7c50de').success).toBe(true);
101
+ expect(GuidString.safeParse('7B4D43D4-5B19-5903-8966-F89671D21565').success).toBe(true);
166
102
  });
167
103
 
168
- it('rejects string with only 35 chars', () => {
169
- const result = GuidString.safeParse('a2345678-1234-1234-1234-12345678901');
170
- expect(result.success).toBe(false);
171
- });
172
- });
173
-
174
- // ── strict rejection of unknown fields ──────────────────────────────────────
175
- describe('GameProjectSchema — strict rejection', () => {
176
- it('rejects scenes[] as unknown field', () => {
177
- const result = GameProjectSchema.safeParse({
178
- id: 'has-scenes',
179
- name: 'Has Scenes',
180
- schemaVersion: '1.0.0',
181
- scenes: [{ id: 'l1', name: 'Level 1', pack: 'scenes/level1.pack.json' }],
182
- });
183
- expect(result.success).toBe(false);
184
- if (!result.success) {
185
- // zod puts unrecognized_keys in the first issue
186
- expect(result.error.issues.some((i) => i.code === 'unrecognized_keys')).toBe(true);
104
+ it('rejects malformed or non-UUID strings', () => {
105
+ for (const value of ['', 'not-a-guid', 'rogue-encampment']) {
106
+ expect(GuidString.safeParse(value).success).toBe(false);
187
107
  }
188
108
  });
189
-
190
- it('rejects any unknown top-level field', () => {
191
- const result = GameProjectSchema.safeParse({
192
- id: 'extra-field',
193
- name: 'Extra',
194
- schemaVersion: '1.0.0',
195
- unknownField: 'should not be here',
196
- });
197
- expect(result.success).toBe(false);
198
- });
199
- });
200
-
201
- // ── physics union (D-4: all 5 known values) ─────────────────────────────────
202
- describe('GameProjectSchema — physics union', () => {
203
- const physicsValues = ['3d', '2d', 'rapier-3d', 'rapier-2d', true] as const;
204
-
205
- for (const phys of physicsValues) {
206
- it(`accepts physics=${JSON.stringify(phys)}`, () => {
207
- const result = GameProjectSchema.safeParse({
208
- id: 'phys-test',
209
- name: 'Physics Test',
210
- schemaVersion: '1.0.0',
211
- physics: phys,
212
- });
213
- expect(result.success).toBe(true);
214
- });
215
- }
216
-
217
- it('rejects arbitrary string for physics', () => {
218
- const result = GameProjectSchema.safeParse({
219
- id: 'bad-physics',
220
- name: 'Bad Physics',
221
- schemaVersion: '1.0.0',
222
- physics: 'custom-engine',
223
- });
224
- expect(result.success).toBe(false);
225
- });
226
-
227
- it('rejects number for physics', () => {
228
- const result = GameProjectSchema.safeParse({
229
- id: 'bad-physics-num',
230
- name: 'Bad Physics Num',
231
- schemaVersion: '1.0.0',
232
- physics: 42,
233
- });
234
- expect(result.success).toBe(false);
235
- });
236
-
237
- it('accepts physics=false (boolean true/false both valid per D-4)', () => {
238
- const result = GameProjectSchema.safeParse({
239
- id: 'physics-false',
240
- name: 'Physics False',
241
- schemaVersion: '1.0.0',
242
- physics: false,
243
- });
244
- expect(result.success).toBe(true);
245
- });
246
- });
247
-
248
- // ── pointerLock boolean ─────────────────────────────────────────────────────
249
- describe('GameProjectSchema — pointerLock', () => {
250
- it('accepts pointerLock=false', () => {
251
- const result = GameProjectSchema.safeParse({
252
- id: 'pl-false',
253
- name: 'PL False',
254
- schemaVersion: '1.0.0',
255
- pointerLock: false,
256
- });
257
- expect(result.success).toBe(true);
258
- });
259
-
260
- it('rejects non-boolean pointerLock', () => {
261
- const result = GameProjectSchema.safeParse({
262
- id: 'pl-string',
263
- name: 'PL String',
264
- schemaVersion: '1.0.0',
265
- pointerLock: 'true',
266
- });
267
- expect(result.success).toBe(false);
268
- });
269
- });
270
-
271
- // ── input string ────────────────────────────────────────────────────────────
272
- describe('GameProjectSchema — input', () => {
273
- it('accepts input string', () => {
274
- const result = GameProjectSchema.safeParse({
275
- id: 'with-input',
276
- name: 'With Input',
277
- schemaVersion: '1.0.0',
278
- input: 'fps',
279
- });
280
- expect(result.success).toBe(true);
281
- });
282
- });
283
-
284
- // ── preview.skin nested object ──────────────────────────────────────────────
285
- describe('GameProjectSchema — preview.skin', () => {
286
- it('accepts full preview.skin object', () => {
287
- const result = GameProjectSchema.safeParse({
288
- id: 'with-preview',
289
- name: 'With Preview',
290
- schemaVersion: '1.0.0',
291
- preview: {
292
- skin: {
293
- sceneGuid: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
294
- clipGuids: ['bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'],
295
- clipDefault: 'walk',
296
- scale: 2.5,
297
- pos: [100, 200, 300],
298
- },
299
- },
300
- });
301
- expect(result.success).toBe(true);
302
- });
303
-
304
- it('accepts minimal preview.skin (sceneGuid only)', () => {
305
- const result = GameProjectSchema.safeParse({
306
- id: 'preview-min',
307
- name: 'Preview Min',
308
- schemaVersion: '1.0.0',
309
- preview: {
310
- skin: {
311
- sceneGuid: 'cccccccc-cccc-cccc-cccc-cccccccccccc',
312
- },
313
- },
314
- });
315
- expect(result.success).toBe(true);
316
- });
317
109
  });
@@ -64,7 +64,8 @@ describe('AC-02: exports accessible', () => {
64
64
  const result = mod.GameProjectSchema.safeParse({
65
65
  id: 'test',
66
66
  name: 'Test',
67
- schemaVersion: '1.0.0',
67
+ schemaVersion: '2.0.0',
68
+ plugins: [],
68
69
  });
69
70
  expect(result.success).toBe(true);
70
71
  if (result.success) {
@@ -72,7 +73,7 @@ describe('AC-02: exports accessible', () => {
72
73
  const gp = result.data;
73
74
  expect(gp.id).toBe('test');
74
75
  expect(gp.name).toBe('Test');
75
- expect(gp.schemaVersion).toBe('1.0.0');
76
+ expect(gp.schemaVersion).toBe('2.0.0');
76
77
  }
77
78
  });
78
79
 
package/src/loader.ts CHANGED
@@ -114,7 +114,7 @@ export function validateGameProject(raw: string): GuidResult<GameProject, GamePr
114
114
  return err(
115
115
  'forge-schema-invalid',
116
116
  'forge.json to satisfy GameProjectSchema',
117
- 'check forge.json against GameProjectSchema (z.infer for types); required fields: id, name, schemaVersion (all strings)',
117
+ 'check forge.json against GameProjectSchema (z.infer for types); required fields: id, name, schemaVersion=2.0.0, and plugins',
118
118
  { path: FORGE_JSON, zodErrors },
119
119
  );
120
120
  }
@@ -161,7 +161,7 @@ export async function loadGameProject(
161
161
  return err(
162
162
  'forge-missing',
163
163
  'forge.json file to exist at the game root',
164
- 'verify the game directory contains forge.json; if this is a new game, scaffold via the Studio UI or create a forge.json with id, name, schemaVersion fields',
164
+ 'verify the game directory contains forge.json; if this is a new game, scaffold via the Studio UI or create a schema 2.0 forge.json with id, name, schemaVersion=2.0.0, and plugins',
165
165
  { path: FORGE_JSON },
166
166
  );
167
167
  }
@@ -190,7 +190,7 @@ export function loadGameProjectSync(
190
190
  return err(
191
191
  'forge-missing',
192
192
  'forge.json file to exist at the game root',
193
- 'verify the game directory contains forge.json; if this is a new game, scaffold via the Studio UI or create a forge.json with id, name, schemaVersion fields',
193
+ 'verify the game directory contains forge.json; if this is a new game, scaffold via the Studio UI or create a schema 2.0 forge.json with id, name, schemaVersion=2.0.0, and plugins',
194
194
  { path: FORGE_JSON },
195
195
  );
196
196
  }