@waica/cli 0.4.2 → 0.6.0
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/README.md +42 -37
- package/dist/editor/assets/{cssMode-BO8FrG9g.js → cssMode-DfLN2qUF.js} +1 -1
- package/dist/editor/assets/{freemarker2-C6rCz24J.js → freemarker2-BM-wWcXR.js} +1 -1
- package/dist/editor/assets/{handlebars-MZdu9Qw_.js → handlebars-C68SV5M9.js} +1 -1
- package/dist/editor/assets/{html-BrjX1TJ1.js → html-dQgLbIki.js} +1 -1
- package/dist/editor/assets/{htmlMode-cEvpMwiW.js → htmlMode-Dx91fUdd.js} +1 -1
- package/dist/editor/assets/{index-eTEBznqW.js → index-592ABIaz.js} +7154 -3186
- package/dist/editor/assets/{index-Btzc6zxr.css → index-Cy5pmBhN.css} +1 -1
- package/dist/editor/assets/{javascript-DHUgp9_L.js → javascript-BmsSq5ka.js} +1 -1
- package/dist/editor/assets/{jsonMode-D0PmIuaM.js → jsonMode-CWoPtX2M.js} +1 -1
- package/dist/editor/assets/{liquid-UM67NCct.js → liquid-DwnJlNJZ.js} +1 -1
- package/dist/editor/assets/{mdx-D3xXLqrP.js → mdx-DaLkvlR1.js} +1 -1
- package/dist/editor/assets/{python-BhLrcAva.js → python-Cs5X5zFp.js} +1 -1
- package/dist/editor/assets/{razor-DcG373mQ.js → razor-PTwWoRk8.js} +1 -1
- package/dist/editor/assets/{tsMode-ByTYaf45.js → tsMode-BqUQdgsR.js} +1 -1
- package/dist/editor/assets/{typescript-DfJlvcTN.js → typescript-BsJ7r2RJ.js} +1 -1
- package/dist/editor/assets/{xml-22dL0oCq.js → xml-DyyoPey4.js} +1 -1
- package/dist/editor/assets/{yaml-mgCu6yV6.js → yaml-Cjqtlp29.js} +1 -1
- package/dist/editor/index.html +2 -2
- package/dist/mcp/component-metadata.js +23 -0
- package/dist/mcp/introspection.js +4 -14
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +7 -1
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +3 -1
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +1 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +42 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +49 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +82 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +147 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +2 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +30 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +38 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +10 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +26 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +5 -13
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +5 -11
- package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
- package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
- package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.d.ts +11 -0
- package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.js +51 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +15 -1
- package/dist/mcp/node_modules/@waica/engine/dist/component.js +2 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +11 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +8 -1
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +4 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +2 -1
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +7 -1
- package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
- package/dist/mcp/package-resolver.js +10 -2
- package/dist/mcp/project-component-loader.js +553 -0
- package/dist/mcp/project-component-runner.js +237 -0
- package/dist/mcp/runtime-browser.js +336 -0
- package/dist/mcp/runtime-dev-server.js +273 -0
- package/dist/mcp/runtime-preflight.js +249 -0
- package/dist/mcp/runtime-service.js +8 -0
- package/dist/mcp/runtime-session-manager.js +265 -0
- package/dist/mcp/scaffolds.js +126 -6
- package/dist/mcp/server.js +330 -9
- package/dist/mcp/template/src/characters/player.character.json +29 -2
- package/dist/mcp/template/src/characters/slime.character.json +6 -0
- package/dist/mcp/template/src/main.ts +6 -1
- package/dist/mcp/validation.js +240 -24
- package/package.json +3 -2
package/dist/mcp/validation.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { resolveComponentUpdateSchedule, } from '@waica/engine';
|
|
1
2
|
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { discoverArchetypes, pickArchetype } from './archetypes.js';
|
|
5
|
+
import { classDefaults, objectRecord } from './component-metadata.js';
|
|
4
6
|
import { PackageResolver, mixedSourceWarnings, provenanceRows, } from './package-resolver.js';
|
|
7
|
+
import { loadProjectComponents, } from './project-component-loader.js';
|
|
5
8
|
import { directFiles, requireWaicaProject } from './project-path.js';
|
|
6
9
|
function add(context, severity, code, message, file, ref) {
|
|
7
10
|
context.findings.push({ severity, code, message, file, ...(ref ? { ref } : {}) });
|
|
@@ -45,10 +48,63 @@ function componentList(value) {
|
|
|
45
48
|
return [];
|
|
46
49
|
return value.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.type === 'string');
|
|
47
50
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
function classMetadata(Class) {
|
|
52
|
+
// A throwing constructor has no observable defaults, matching list_components.
|
|
53
|
+
return { Class, params: Class.params ?? {}, defaults: classDefaults(Class) };
|
|
54
|
+
}
|
|
55
|
+
/** Clip names declared by the sibling AnimatedSprite, or undefined when there is none (no animation contract to check). */
|
|
56
|
+
function siblingClips(siblings) {
|
|
57
|
+
const animated = siblings.find((component) => component.type === 'AnimatedSprite');
|
|
58
|
+
return animated ? new Set(Object.keys(objectRecord(animated.props?.clips))) : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Whether `action` has at least one real binding — an own key, since bindings is keyed by an untrusted string. */
|
|
61
|
+
function isBoundAction(bindings, action) {
|
|
62
|
+
return Object.hasOwn(bindings, action) && (bindings[action]?.length ?? 0) > 0;
|
|
63
|
+
}
|
|
64
|
+
function validateParamReferences(entries, siblings, file, context) {
|
|
65
|
+
const clips = siblingClips(siblings);
|
|
66
|
+
for (const { component, only } of entries) {
|
|
67
|
+
const metadata = context.componentMetadata.get(component.type);
|
|
68
|
+
if (!metadata)
|
|
69
|
+
continue;
|
|
70
|
+
const props = objectRecord(component.props);
|
|
71
|
+
for (const [param, spec] of Object.entries(metadata.params)) {
|
|
72
|
+
if (only && !only.has(param))
|
|
73
|
+
continue;
|
|
74
|
+
if (!spec.ref || spec.options !== undefined)
|
|
75
|
+
continue;
|
|
76
|
+
const value = Object.hasOwn(props, param) ? props[param] : metadata.defaults[param];
|
|
77
|
+
if (typeof value !== 'string' || value === '')
|
|
78
|
+
continue;
|
|
79
|
+
const field = `${component.type}.${param}`;
|
|
80
|
+
switch (spec.ref) {
|
|
81
|
+
case 'prefab':
|
|
82
|
+
if (!context.prefabRefs.has(value)) {
|
|
83
|
+
add(context, 'error', 'broken-prefab-ref', `Component "${component.type}" param "${param}" references missing prefab "${value}".`, file, field);
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case 'clip':
|
|
87
|
+
if (clips && !clips.has(value)) {
|
|
88
|
+
add(context, 'error', 'missing-clip', `Component "${component.type}" param "${param}" references missing animation clip "${value}".`, file, field);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 'action':
|
|
92
|
+
if (!isBoundAction(context.bindings, value)) {
|
|
93
|
+
add(context,
|
|
94
|
+
// Consistent with the pre-existing state-transition check for the
|
|
95
|
+
// same condition (below): an unbound action is a real gap the
|
|
96
|
+
// agent should look at, but not one that flips ok:false.
|
|
97
|
+
'warning', 'input-action-unbound', `Component "${component.type}" param "${param}" references unbound input action "${value}".`, file, field);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 'stat':
|
|
101
|
+
if (!context.declaredStats.has(value)) {
|
|
102
|
+
add(context, 'warning', 'undeclared-stat', `Component "${component.type}" param "${param}" references undeclared stat "${value}"; runtime writes may still create it.`, file, field);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
52
108
|
}
|
|
53
109
|
function machineStates(component) {
|
|
54
110
|
return objectRecord(component.props?.states);
|
|
@@ -85,10 +141,7 @@ function stateCodeExists(role, state, context) {
|
|
|
85
141
|
(context.roleStateSources.get(role) ?? []).some((source) => roleSourceHasState(source, state)));
|
|
86
142
|
}
|
|
87
143
|
function validateStateMachines(components, file, ref, context) {
|
|
88
|
-
const
|
|
89
|
-
const clips = animated
|
|
90
|
-
? new Set(Object.keys(objectRecord(animated.props?.clips)))
|
|
91
|
-
: undefined;
|
|
144
|
+
const clips = siblingClips(components);
|
|
92
145
|
for (const machine of components.filter((component) => component.type === 'StateMachine')) {
|
|
93
146
|
const states = machineStates(machine);
|
|
94
147
|
const realNames = Object.keys(states).filter((name) => name !== '*');
|
|
@@ -98,9 +151,15 @@ function validateStateMachines(components, file, ref, context) {
|
|
|
98
151
|
: (realNames[0] ?? '');
|
|
99
152
|
for (const name of realNames) {
|
|
100
153
|
const definition = objectRecord(states[name]);
|
|
101
|
-
const
|
|
154
|
+
const explicitClip = typeof definition.clip === 'string' ? definition.clip : undefined;
|
|
155
|
+
const clip = explicitClip ?? name;
|
|
156
|
+
// No "empty means none" special case here: the runtime looks up
|
|
157
|
+
// `this.states[state]?.clip ?? state`, and '' survives that nullish
|
|
158
|
+
// coalesce, so an explicit empty clip is looked up literally and must
|
|
159
|
+
// be validated like any other explicit value (unlike Collectible.stat,
|
|
160
|
+
// which the runtime genuinely treats as unset).
|
|
102
161
|
if (clips && !clips.has(clip)) {
|
|
103
|
-
add(context, 'warning', 'missing-clip', `State "${name}" uses missing animation clip "${clip}".`, file, ref ?? name);
|
|
162
|
+
add(context, explicitClip === undefined ? 'warning' : 'error', 'missing-clip', `State "${name}" uses missing animation clip "${clip}".`, file, explicitClip === undefined ? (ref ?? name) : `StateMachine.states.${name}.clip`);
|
|
104
163
|
}
|
|
105
164
|
const transitions = Array.isArray(definition.transitions) ? definition.transitions : [];
|
|
106
165
|
for (const transition of transitions) {
|
|
@@ -113,7 +172,7 @@ function validateStateMachines(components, file, ref, context) {
|
|
|
113
172
|
}
|
|
114
173
|
if (typeof on === 'string' && on.startsWith('input:')) {
|
|
115
174
|
const action = on.slice('input:'.length);
|
|
116
|
-
if (!context.bindings
|
|
175
|
+
if (!isBoundAction(context.bindings, action)) {
|
|
117
176
|
add(context, 'warning', 'input-action-unbound', `Input action "${action}" has no bindings.`, file, action);
|
|
118
177
|
}
|
|
119
178
|
}
|
|
@@ -141,11 +200,63 @@ function resolvedEntityComponents(entity, prefab) {
|
|
|
141
200
|
}));
|
|
142
201
|
return [...inherited, ...componentList(entity.components)];
|
|
143
202
|
}
|
|
203
|
+
function componentUpdateIssueKey(issue) {
|
|
204
|
+
switch (issue.code) {
|
|
205
|
+
case 'duplicate-component':
|
|
206
|
+
return `${issue.code}:${issue.componentName}:${issue.count}`;
|
|
207
|
+
case 'invalid-update-constraint':
|
|
208
|
+
return `${issue.code}:${issue.reason}:${issue.declarer}:${issue.target ?? ''}`;
|
|
209
|
+
case 'component-update-cycle':
|
|
210
|
+
return `${issue.code}:${issue.componentNames.join('\0')}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function componentUpdateIssues(components, context) {
|
|
214
|
+
const result = resolveComponentUpdateSchedule(components.map((component) => component.type), context.componentRegistry);
|
|
215
|
+
return result.ok ? [] : [...result.issues];
|
|
216
|
+
}
|
|
217
|
+
function addComponentUpdateFinding(issue, file, ref, context) {
|
|
218
|
+
let findingFile = file;
|
|
219
|
+
let findingRef = ref;
|
|
220
|
+
if (issue.code === 'invalid-update-constraint') {
|
|
221
|
+
const source = context.componentMetadata.get(issue.declarer)?.sourceFile;
|
|
222
|
+
if (source) {
|
|
223
|
+
const key = `${source}:${componentUpdateIssueKey(issue)}`;
|
|
224
|
+
if (context.reportedClassConstraints.has(key))
|
|
225
|
+
return;
|
|
226
|
+
context.reportedClassConstraints.add(key);
|
|
227
|
+
findingFile = source;
|
|
228
|
+
findingRef = issue.declarer;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
add(context, 'error', issue.code, issue.cause, findingFile, findingRef);
|
|
232
|
+
}
|
|
233
|
+
function validateComponentUpdateSchedule(components, file, ref, context, inheritedIssues = new Set()) {
|
|
234
|
+
for (const issue of componentUpdateIssues(components, context)) {
|
|
235
|
+
if (inheritedIssues.has(componentUpdateIssueKey(issue)))
|
|
236
|
+
continue;
|
|
237
|
+
addComponentUpdateFinding(issue, file, ref, context);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function validateComponentClassUpdateContracts(context) {
|
|
241
|
+
for (const componentName of Object.keys(context.componentRegistry).sort()) {
|
|
242
|
+
const metadata = context.componentMetadata.get(componentName);
|
|
243
|
+
const result = resolveComponentUpdateSchedule([componentName], context.componentRegistry);
|
|
244
|
+
if (result.ok)
|
|
245
|
+
continue;
|
|
246
|
+
for (const issue of result.issues) {
|
|
247
|
+
if (issue.code !== 'invalid-update-constraint')
|
|
248
|
+
continue;
|
|
249
|
+
addComponentUpdateFinding(issue, metadata?.sourceFile ?? 'package.json', componentName, context);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
144
253
|
function validatePrefab(prefab, file, ref, context) {
|
|
145
254
|
const components = componentList(prefab.components);
|
|
146
255
|
for (const component of components)
|
|
147
256
|
checkComponent(component, file, ref, context);
|
|
257
|
+
validateParamReferences(components.map((component) => ({ component })), components, file, context);
|
|
148
258
|
validateStateMachines(components, file, ref, context);
|
|
259
|
+
validateComponentUpdateSchedule(components, file, ref, context);
|
|
149
260
|
}
|
|
150
261
|
function validateScene(scene, file, prefabs, uiNames, context) {
|
|
151
262
|
const rawEntities = Array.isArray(scene.entities) ? scene.entities : [];
|
|
@@ -186,6 +297,57 @@ function validateScene(scene, file, prefabs, uiNames, context) {
|
|
|
186
297
|
}
|
|
187
298
|
}
|
|
188
299
|
}
|
|
300
|
+
// Lazily resolved: only computed when a consumer below actually needs
|
|
301
|
+
// the merged prefab+override component list (an override that changes a
|
|
302
|
+
// ref param, a clip-context change, an inline ref:clip lookup that needs
|
|
303
|
+
// the effective sibling AnimatedSprite, or state-machine revalidation).
|
|
304
|
+
// A plain "prefab: ref" entity with no overrides and no inline
|
|
305
|
+
// components never pays for building it (restores the pre-typed-refs
|
|
306
|
+
// laziness that only ran this for state-behavior changes).
|
|
307
|
+
let cachedEffectiveComponents;
|
|
308
|
+
const effectiveComponents = () => {
|
|
309
|
+
cachedEffectiveComponents ??= resolvedEntityComponents(entity, prefab);
|
|
310
|
+
return cachedEffectiveComponents;
|
|
311
|
+
};
|
|
312
|
+
const hasRefKind = (type, kind) => Object.values(context.componentMetadata.get(type)?.params ?? {}).some((spec) => spec.ref === kind && spec.options === undefined);
|
|
313
|
+
// Undefined scope means "check every declared ref param" — correct for
|
|
314
|
+
// inline components (never validated elsewhere) and for a component a
|
|
315
|
+
// clip-context change forces a full recheck of. A Set scopes the check
|
|
316
|
+
// to only the override's changed params, so a prefab-level finding for
|
|
317
|
+
// a param the override never touched is not reported a second time
|
|
318
|
+
// under the scene file.
|
|
319
|
+
const referenceScopes = new Map();
|
|
320
|
+
for (const component of inline)
|
|
321
|
+
referenceScopes.set(component, undefined);
|
|
322
|
+
const inlineTypes = new Set(inline.map((component) => component.type));
|
|
323
|
+
for (const [type, rawPatch] of Object.entries(overrides)) {
|
|
324
|
+
if (inlineTypes.has(type))
|
|
325
|
+
continue;
|
|
326
|
+
const metadata = context.componentMetadata.get(type);
|
|
327
|
+
const patch = objectRecord(rawPatch);
|
|
328
|
+
const changedRefParams = new Set(Object.entries(metadata?.params ?? {})
|
|
329
|
+
.filter(([param, spec]) => spec.ref !== undefined && spec.options === undefined && Object.hasOwn(patch, param))
|
|
330
|
+
.map(([param]) => param));
|
|
331
|
+
if (changedRefParams.size === 0)
|
|
332
|
+
continue;
|
|
333
|
+
const effective = effectiveComponents().find((component) => component.type === type);
|
|
334
|
+
if (effective && !referenceScopes.has(effective))
|
|
335
|
+
referenceScopes.set(effective, changedRefParams);
|
|
336
|
+
}
|
|
337
|
+
const changesClipContext = inline.some((component) => component.type === 'AnimatedSprite') ||
|
|
338
|
+
Object.hasOwn(overrides, 'AnimatedSprite');
|
|
339
|
+
if (changesClipContext) {
|
|
340
|
+
for (const component of effectiveComponents()) {
|
|
341
|
+
// A clip-context change can affect params this component didn't
|
|
342
|
+
// itself change, so this always widens to a full check rather than
|
|
343
|
+
// narrowing an already-scoped entry from the override loop above.
|
|
344
|
+
if (hasRefKind(component.type, 'clip'))
|
|
345
|
+
referenceScopes.set(component, undefined);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const needsSiblings = cachedEffectiveComponents !== undefined ||
|
|
349
|
+
[...referenceScopes.keys()].some((component) => hasRefKind(component.type, 'clip'));
|
|
350
|
+
validateParamReferences([...referenceScopes.entries()].map(([component, only]) => ({ component, only })), needsSiblings ? effectiveComponents() : [...referenceScopes.keys()], file, context);
|
|
189
351
|
// Re-evaluate inherited state behavior only when this entity actually
|
|
190
352
|
// changes a StateMachine or AnimatedSprite. Unrelated overrides keep the
|
|
191
353
|
// prefab-level finding as the single source of truth.
|
|
@@ -193,7 +355,14 @@ function validateScene(scene, file, prefabs, uiNames, context) {
|
|
|
193
355
|
const changesStateBehavior = inline.some((component) => stateTypes.has(component.type)) ||
|
|
194
356
|
Object.keys(overrides).some((type) => stateTypes.has(type));
|
|
195
357
|
if (changesStateBehavior) {
|
|
196
|
-
validateStateMachines(
|
|
358
|
+
validateStateMachines(effectiveComponents(), file, entityRef, context);
|
|
359
|
+
}
|
|
360
|
+
// A plain prefab instance inherits the prefab-level result already emitted
|
|
361
|
+
// above. Inline components create a new effective composition; report only
|
|
362
|
+
// the issues they introduce, not every inherited issue again.
|
|
363
|
+
if (inline.length > 0) {
|
|
364
|
+
const inheritedIssues = new Set(componentUpdateIssues(componentList(prefab?.components), context).map(componentUpdateIssueKey));
|
|
365
|
+
validateComponentUpdateSchedule(effectiveComponents(), file, entityRef, context, inheritedIssues);
|
|
197
366
|
}
|
|
198
367
|
}
|
|
199
368
|
}
|
|
@@ -211,7 +380,8 @@ function statBindings(html) {
|
|
|
211
380
|
}
|
|
212
381
|
return names;
|
|
213
382
|
}
|
|
214
|
-
export async function validateProject(projectPath) {
|
|
383
|
+
export async function validateProject(projectPath, options = {}) {
|
|
384
|
+
options.signal?.throwIfAborted();
|
|
215
385
|
const check = await requireWaicaProject(projectPath);
|
|
216
386
|
const findings = [];
|
|
217
387
|
const fixedPaths = [
|
|
@@ -234,15 +404,32 @@ export async function validateProject(projectPath) {
|
|
|
234
404
|
: null;
|
|
235
405
|
const resolver = new PackageResolver(projectPath);
|
|
236
406
|
const discoveryWarnings = [];
|
|
237
|
-
const [engine, behaviors, archetypes, projectComponents, roleStateSources] = await Promise.all([
|
|
407
|
+
const [engine, behaviors, archetypes, projectComponents, roleStateSources, loadedProjectComponents,] = await Promise.all([
|
|
238
408
|
resolver.load('@waica/engine'),
|
|
239
409
|
resolver.load('@waica/behaviors'),
|
|
240
410
|
discoverArchetypes(projectPath, resolver, discoveryWarnings, activeId ? [activeId] : []),
|
|
241
411
|
projectComponentCandidates(projectPath),
|
|
242
412
|
projectRoleStateSources(projectPath),
|
|
413
|
+
options.componentLoader
|
|
414
|
+
? options.componentLoader.load(projectPath, resolver, { signal: options.signal })
|
|
415
|
+
: loadProjectComponents(projectPath, resolver, { signal: options.signal }),
|
|
243
416
|
]);
|
|
417
|
+
for (const failure of loadedProjectComponents.failures) {
|
|
418
|
+
add({ findings },
|
|
419
|
+
// component-load-unsupported means Node's strip-only loader cannot run
|
|
420
|
+
// code that can still be perfectly valid in the project's Vite/browser
|
|
421
|
+
// toolchain (asset imports, TS enums, an old Node host) — that is not a
|
|
422
|
+
// project defect, so it must not flip a healthy project to ok:false.
|
|
423
|
+
failure.code === 'component-load-unsupported' ? 'info' : 'error', failure.code, `Cannot execute project module: ${failure.message}`, failure.file);
|
|
424
|
+
}
|
|
244
425
|
const manifest = pickArchetype(archetypes, activeId, projectPath).manifest;
|
|
245
426
|
const controls = objectRecord(objectRecord(fixed.get('src/controls.json')).bindings);
|
|
427
|
+
// Bound/unbound is decided from controls.json alone: the shipped runtime
|
|
428
|
+
// installs exactly controls.json's bindings (template main.ts passes
|
|
429
|
+
// controls.bindings raw; engine DEFAULT_BINDINGS is {}), so an action an
|
|
430
|
+
// archetype defines by default but controls.json drops is genuinely
|
|
431
|
+
// unbound at runtime even though discoverArchetypes never guarantees
|
|
432
|
+
// manifest.bindings exists (only manifest.id is validated).
|
|
246
433
|
const bindings = {};
|
|
247
434
|
for (const [name, value] of Object.entries(controls)) {
|
|
248
435
|
if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
|
|
@@ -250,16 +437,28 @@ export async function validateProject(projectPath) {
|
|
|
250
437
|
}
|
|
251
438
|
}
|
|
252
439
|
const stateFiles = new Set((await directFiles(path.join(projectPath, 'src/states'), '.ts')).map((file) => file.slice(0, -'.ts'.length)));
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
manifest,
|
|
256
|
-
knownComponents: new Set(Object.keys(manifest.registry.components)),
|
|
257
|
-
projectComponents,
|
|
258
|
-
stateFiles,
|
|
259
|
-
roleStateSources,
|
|
260
|
-
bindings,
|
|
440
|
+
const declaredStats = new Set(Object.keys(objectRecord(objectRecord(fixed.get('src/stats.json')).stats)));
|
|
441
|
+
const componentRegistry = {
|
|
442
|
+
...manifest.registry.components,
|
|
261
443
|
};
|
|
444
|
+
const componentMetadata = new Map(Object.entries(componentRegistry).map(([name, Class]) => [
|
|
445
|
+
name,
|
|
446
|
+
classMetadata(Class),
|
|
447
|
+
]));
|
|
448
|
+
for (const [name, description] of Object.entries(loadedProjectComponents.components)) {
|
|
449
|
+
componentRegistry[name] = description.Class;
|
|
450
|
+
componentMetadata.set(name, {
|
|
451
|
+
Class: description.Class,
|
|
452
|
+
params: description.params,
|
|
453
|
+
defaults: description.defaults,
|
|
454
|
+
sourceFile: description.file,
|
|
455
|
+
});
|
|
456
|
+
projectComponents.add(name);
|
|
457
|
+
}
|
|
458
|
+
// Reconstruct the complete ref set before validating any prefab, so a
|
|
459
|
+
// lexically earlier file can refer to one discovered later in the tree.
|
|
262
460
|
const prefabs = new Map();
|
|
461
|
+
const prefabFiles = [];
|
|
263
462
|
for (const [directory, type] of [
|
|
264
463
|
['characters', 'character'],
|
|
265
464
|
['objects', 'object'],
|
|
@@ -274,12 +473,29 @@ export async function validateProject(projectPath) {
|
|
|
274
473
|
const ref = `${directory}/${file.slice(0, -suffix.length)}`;
|
|
275
474
|
const prefab = parsed;
|
|
276
475
|
prefabs.set(ref, prefab);
|
|
277
|
-
|
|
476
|
+
prefabFiles.push({ prefab, relative, ref });
|
|
278
477
|
}
|
|
279
478
|
}
|
|
479
|
+
const context = {
|
|
480
|
+
findings,
|
|
481
|
+
manifest,
|
|
482
|
+
knownComponents: new Set(Object.keys(manifest.registry.components)),
|
|
483
|
+
projectComponents,
|
|
484
|
+
componentMetadata,
|
|
485
|
+
componentRegistry,
|
|
486
|
+
reportedClassConstraints: new Set(),
|
|
487
|
+
prefabRefs: new Set(prefabs.keys()),
|
|
488
|
+
declaredStats,
|
|
489
|
+
stateFiles,
|
|
490
|
+
roleStateSources,
|
|
491
|
+
bindings,
|
|
492
|
+
};
|
|
493
|
+
validateComponentClassUpdateContracts(context);
|
|
494
|
+
for (const { prefab, relative, ref } of prefabFiles) {
|
|
495
|
+
validatePrefab(prefab, relative, ref, context);
|
|
496
|
+
}
|
|
280
497
|
const uiFiles = await directFiles(path.join(projectPath, 'src/ui'), '.html');
|
|
281
498
|
const uiNames = new Set(uiFiles.map((file) => file.slice(0, -'.html'.length)));
|
|
282
|
-
const declaredStats = new Set(Object.keys(objectRecord(objectRecord(fixed.get('src/stats.json')).stats)));
|
|
283
499
|
for (const file of uiFiles) {
|
|
284
500
|
const relative = `src/ui/${file}`;
|
|
285
501
|
const html = await readFile(path.join(projectPath, relative), 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waica/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Waica in one command — `npx @waica/cli` opens the editor, `waica mcp` serves the MCP server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
22
|
+
"node": ">=22.18"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc -p tsconfig.build.json && node bundle-editor.mjs && node bundle-mcp.mjs",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
30
|
+
"playwright-core": "^1.62.1",
|
|
30
31
|
"three": "^0.185.1"
|
|
31
32
|
}
|
|
32
33
|
}
|