@neocompose/cli 0.30.1 → 0.30.3

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.3] - 2026-08-13
4
+
5
+ ### Fixed
6
+
7
+ - Identify stored dialogue and migration bodies in `neo script check --all`
8
+ compile failures and print each in-body source position once. The command
9
+ also accepts existing delegate and action types in constructor signatures.
10
+
11
+ ## [0.30.2] - 2026-08-13
12
+
13
+ ### Fixed
14
+
15
+ - Pull instance-bound `NeoAction` listeners through their deterministic
16
+ `root.*` receiver paths and lower those paths back to the same listener
17
+ identity. Web-authored actions such as `[root.Assets.OnDamage]` now pull and
18
+ push without losing the receiver or leaving a phantom member change.
19
+
3
20
  ## [0.30.1] - 2026-08-13
4
21
 
5
22
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -29717,6 +29717,38 @@ function substituteManifestType(type, genericEnvironment, environment, seenParam
29717
29717
  )
29718
29718
  };
29719
29719
  }
29720
+ if (type.kind === "delegate") {
29721
+ return {
29722
+ ...type,
29723
+ returnType: substituteManifestType(
29724
+ type.returnType,
29725
+ genericEnvironment,
29726
+ environment,
29727
+ seenParameters
29728
+ ),
29729
+ parameterTypes: type.parameterTypes.map(
29730
+ (parameter4) => substituteManifestType(
29731
+ parameter4,
29732
+ genericEnvironment,
29733
+ environment,
29734
+ seenParameters
29735
+ )
29736
+ )
29737
+ };
29738
+ }
29739
+ if (type.kind === "action") {
29740
+ return {
29741
+ ...type,
29742
+ parameterTypes: type.parameterTypes.map(
29743
+ (parameter4) => substituteManifestType(
29744
+ parameter4,
29745
+ genericEnvironment,
29746
+ environment,
29747
+ seenParameters
29748
+ )
29749
+ )
29750
+ };
29751
+ }
29720
29752
  return type;
29721
29753
  }
29722
29754
  function substituteManifestSymbol(symbol, genericEnvironment, environment) {
@@ -30040,6 +30072,37 @@ function manifestType2(value, environment, field) {
30040
30072
  nullable
30041
30073
  };
30042
30074
  }
30075
+ if (kind === "delegate") {
30076
+ return {
30077
+ kind: "delegate",
30078
+ returnType: manifestReturnType(
30079
+ type.returnType,
30080
+ environment,
30081
+ `${field}.returnType`
30082
+ ),
30083
+ parameterTypes: records(type.argumentTypes, `${field}.argumentTypes`).map(
30084
+ (argument2, index) => manifestType2(
30085
+ argument2,
30086
+ environment,
30087
+ `${field}.argumentTypes[${index}]`
30088
+ )
30089
+ ),
30090
+ nullable
30091
+ };
30092
+ }
30093
+ if (kind === "action") {
30094
+ return {
30095
+ kind: "action",
30096
+ parameterTypes: records(type.argumentTypes, `${field}.argumentTypes`).map(
30097
+ (argument2, index) => manifestType2(
30098
+ argument2,
30099
+ environment,
30100
+ `${field}.argumentTypes[${index}]`
30101
+ )
30102
+ ),
30103
+ nullable
30104
+ };
30105
+ }
30043
30106
  throw new Error(
30044
30107
  `${field}.kind has unsupported schema type ${JSON.stringify(kind)}.`
30045
30108
  );
@@ -47422,12 +47485,26 @@ function renderActionListeners(context, member, value) {
47422
47485
  `NeoAction member ${member.name} listener ${index} is missing memberId.`
47423
47486
  );
47424
47487
  }
47425
- if (listener.valueId !== null && listener.valueId !== void 0) {
47488
+ const target = required(
47489
+ context.members,
47490
+ listener.memberId,
47491
+ "action listener"
47492
+ );
47493
+ if (listener.valueId === null || listener.valueId === void 0) {
47494
+ return target.name;
47495
+ }
47496
+ if (typeof listener.valueId !== "string") {
47497
+ throw new Error(
47498
+ `NeoAction member ${member.name} listener ${index} has a non-string receiver valueId.`
47499
+ );
47500
+ }
47501
+ const receiver = context.collectionValuePaths.get(listener.valueId);
47502
+ if (receiver === void 0) {
47426
47503
  throw new Error(
47427
- `NeoAction member ${member.name} listener ${index} is instance-bound, which no declaration default can spell.`
47504
+ `NeoAction member ${member.name} listener ${index} is bound to value ${listener.valueId}, which has no deterministic root path.`
47428
47505
  );
47429
47506
  }
47430
- return required(context.members, listener.memberId, "action listener").name;
47507
+ return `${receiver}.${target.name}`;
47431
47508
  });
47432
47509
  return `[${names.join(", ")}]`;
47433
47510
  }
@@ -53431,6 +53508,7 @@ function lowerFieldMember(context, ownerClass, declaration, id2, commonInput, ba
53431
53508
  `NeoAction member ${ownerClass.name}.${declaration.name} cannot be nullable; an action's rest state is an empty listener list.`
53432
53509
  );
53433
53510
  }
53511
+ const baseAction = base?.kind === "action" ? base : void 0;
53434
53512
  return {
53435
53513
  ...common,
53436
53514
  kind: "action",
@@ -53439,7 +53517,10 @@ function lowerFieldMember(context, ownerClass, declaration, id2, commonInput, ba
53439
53517
  // fixed false because nullability has no meaning here (P62 §2.1).
53440
53518
  required: false,
53441
53519
  arguments: fieldType.arguments.map((argumentType, index) => ({
53442
- name: `p${index + 1}`,
53520
+ // NeoAction's generic spelling carries types but no parameter names.
53521
+ // Preserve a pulled name when this stable declaration already exists;
53522
+ // `pN` remains the deterministic name for a newly authored action.
53523
+ name: baseAction?.arguments[index]?.name ?? `p${index + 1}`,
53443
53524
  type: lowerType(context, argumentType, ownerClass),
53444
53525
  default: null,
53445
53526
  source: span(declaration),
@@ -54168,23 +54249,24 @@ function lowerActionDefault(context, expression, ownerClass, path) {
54168
54249
  const listeners = [];
54169
54250
  const identities = /* @__PURE__ */ new Set();
54170
54251
  elements.forEach((element, index) => {
54171
- const memberId = lowerActionListenerId(
54252
+ const listener = lowerActionListener(
54172
54253
  context,
54173
54254
  element,
54174
54255
  ownerClass,
54175
54256
  `${path}[${index}]`
54176
54257
  );
54177
- if (identities.has(memberId)) {
54258
+ const identity2 = `${listener.memberId}\0${listener.valueId ?? ""}`;
54259
+ if (identities.has(identity2)) {
54178
54260
  throw new Error(
54179
54261
  `NeoAction listener ${path}[${index}] repeats an earlier listener; a listener set holds each target once.`
54180
54262
  );
54181
54263
  }
54182
- identities.add(memberId);
54183
- listeners.push({ memberId, valueId: null });
54264
+ identities.add(identity2);
54265
+ listeners.push(listener);
54184
54266
  });
54185
54267
  return { listeners };
54186
54268
  }
54187
- function lowerActionListenerId(context, element, ownerClass, path) {
54269
+ function lowerActionListener(context, element, ownerClass, path) {
54188
54270
  let current = element;
54189
54271
  while (current.kind === "annotated") current = current.expression;
54190
54272
  if (current.kind === "lambda") {
@@ -54192,21 +54274,26 @@ function lowerActionListenerId(context, element, ownerClass, path) {
54192
54274
  `NeoAction listener ${path} is a lambda. A listener must be a member reference: extract the body to an NSFunction member and subscribe that member instead.`
54193
54275
  );
54194
54276
  }
54195
- const name = actionListenerSymbol(current);
54196
- if (name === null) {
54277
+ const target = actionListenerTarget(context, current, ownerClass);
54278
+ if (target === null) {
54197
54279
  throw new Error(
54198
- `NeoAction listener ${path} must name a callable member on the current Class.`
54280
+ `NeoAction listener ${path} must name a callable member on the current Class or a receiver reachable through root.`
54199
54281
  );
54200
54282
  }
54201
- const classId = materializedId(ownerClass, "class", ownerClass.name);
54202
- const memberId = effectiveMemberIdByName(context, classId, name);
54283
+ const memberId = effectiveMemberIdByName(
54284
+ context,
54285
+ target.classId,
54286
+ target.memberName
54287
+ );
54203
54288
  if (memberId === null) {
54204
54289
  throw new Error(
54205
- `NeoAction listener ${path} names ${JSON.stringify(name)}, which ${ownerClass.name} does not declare or inherit.`
54290
+ `NeoAction listener ${path} names ${JSON.stringify(target.memberName)}, which its receiver Class does not declare or inherit.`
54206
54291
  );
54207
54292
  }
54208
54293
  const declaration = context.sourceMembersById.get(memberId)?.member;
54209
- if (declaration === void 0) return memberId;
54294
+ if (declaration === void 0) {
54295
+ return { memberId, valueId: target.valueId };
54296
+ }
54210
54297
  if (declaration.kind === "function") {
54211
54298
  if (declaration.type.name !== "void") {
54212
54299
  throw new Error(
@@ -54218,21 +54305,46 @@ function lowerActionListenerId(context, element, ownerClass, path) {
54218
54305
  `NeoAction listener ${path} targets deferred ${declaration.name}; listeners are immediate-only.`
54219
54306
  );
54220
54307
  }
54221
- return memberId;
54308
+ return { memberId, valueId: target.valueId };
54222
54309
  }
54223
54310
  if (declaration.type.name !== "NeoAction" && declaration.type.name !== "NeoDelegate") {
54224
54311
  throw new Error(
54225
54312
  `NeoAction listener ${path} targets ${declaration.name}, which is not a function, NSFunction, NSDelegate, or NeoAction member.`
54226
54313
  );
54227
54314
  }
54228
- return memberId;
54315
+ return { memberId, valueId: target.valueId };
54229
54316
  }
54230
- function actionListenerSymbol(expression) {
54317
+ function actionListenerTarget(context, expression, ownerClass) {
54318
+ const expressionPath2 = actionListenerPath(expression);
54319
+ if (expressionPath2 === null) return null;
54320
+ if (!expressionPath2.includes(".")) {
54321
+ return {
54322
+ classId: materializedId(ownerClass, "class", ownerClass.name),
54323
+ memberName: expressionPath2,
54324
+ valueId: null
54325
+ };
54326
+ }
54327
+ if (expressionPath2.startsWith("this.")) {
54328
+ const memberName2 = expressionPath2.slice("this.".length);
54329
+ if (memberName2.includes(".")) return null;
54330
+ return {
54331
+ classId: materializedId(ownerClass, "class", ownerClass.name),
54332
+ memberName: memberName2,
54333
+ valueId: null
54334
+ };
54335
+ }
54336
+ const separator = expressionPath2.lastIndexOf(".");
54337
+ const receiverPath = expressionPath2.slice(0, separator);
54338
+ const memberName = expressionPath2.slice(separator + 1);
54339
+ const receiver = context.rootValueTargetsByPath.get(receiverPath);
54340
+ if (receiver === void 0) return null;
54341
+ return { ...receiver, memberName };
54342
+ }
54343
+ function actionListenerPath(expression) {
54231
54344
  if (expression.kind === "ident") return expression.name;
54232
54345
  if (expression.kind !== "member") return null;
54233
- if (expression.receiver.kind !== "ident") return null;
54234
- if (expression.receiver.name !== "this") return null;
54235
- return expression.name;
54346
+ const receiver = actionListenerPath(expression.receiver);
54347
+ return receiver === null ? null : `${receiver}.${expression.name}`;
54236
54348
  }
54237
54349
  function lowerExpressionValue(context, expression, declaredExpected, ownerClass, path, genericEnvironment = EMPTY_GENERIC_TYPE_ENVIRONMENT, sourceText) {
54238
54350
  const expected = substituteGenericTypeNames(
@@ -55283,6 +55395,81 @@ var init_lower_members = __esm({
55283
55395
  }
55284
55396
  });
55285
55397
 
55398
+ // src/project-source/root-value-paths.ts
55399
+ function rootValuePathsByValueId(records2) {
55400
+ const indexed = indexRecords(records2);
55401
+ const paths = /* @__PURE__ */ new Map();
55402
+ const queue = [];
55403
+ for (const [field, slot] of ROOT_PROJECT_FIELDS) {
55404
+ const memberId = indexed.project?.[field];
55405
+ const member = typeof memberId === "string" ? indexed.members.get(memberId) : void 0;
55406
+ const valueId = member?.valueId;
55407
+ if (typeof valueId === "string") {
55408
+ queue.push({ id: valueId, path: `root.${slot}` });
55409
+ }
55410
+ }
55411
+ while (queue.length > 0) {
55412
+ const next = queue.shift();
55413
+ if (next === void 0) break;
55414
+ if (paths.has(next.id)) continue;
55415
+ paths.set(next.id, next.path);
55416
+ const body = indexed.values.get(next.id)?.value;
55417
+ if (!isObjectRecord3(body)) continue;
55418
+ for (const [schemaKey, child] of Object.entries(body)) {
55419
+ if (typeof child !== "string") continue;
55420
+ if (!indexed.values.has(child)) continue;
55421
+ queue.push({ id: child, path: `${next.path}.${schemaKey}` });
55422
+ }
55423
+ }
55424
+ return paths;
55425
+ }
55426
+ function rootValueTargetsByPath(records2) {
55427
+ const allRecords = [...records2];
55428
+ const paths = rootValuePathsByValueId(allRecords);
55429
+ const values = new Map(
55430
+ allRecords.flatMap(
55431
+ (record3) => !record3.deleted && record3.recordKind === "value" && isObjectRecord3(record3.data) ? [[record3.recordId, record3.data]] : []
55432
+ )
55433
+ );
55434
+ const targets = /* @__PURE__ */ new Map();
55435
+ for (const [valueId, path] of paths) {
55436
+ const classId = values.get(valueId)?.classId;
55437
+ if (typeof classId === "string") {
55438
+ targets.set(path, { valueId, classId });
55439
+ }
55440
+ }
55441
+ return targets;
55442
+ }
55443
+ function indexRecords(records2) {
55444
+ let project;
55445
+ const members = /* @__PURE__ */ new Map();
55446
+ const values = /* @__PURE__ */ new Map();
55447
+ for (const record3 of records2) {
55448
+ if (record3.deleted || !isObjectRecord3(record3.data)) continue;
55449
+ if (record3.recordKind === "project") project ??= record3.data;
55450
+ else if (record3.recordKind === "member") {
55451
+ members.set(record3.recordId, record3.data);
55452
+ } else if (record3.recordKind === "value") {
55453
+ values.set(record3.recordId, record3.data);
55454
+ }
55455
+ }
55456
+ return { project, members, values };
55457
+ }
55458
+ function isObjectRecord3(value) {
55459
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55460
+ }
55461
+ var ROOT_PROJECT_FIELDS;
55462
+ var init_root_value_paths = __esm({
55463
+ "src/project-source/root-value-paths.ts"() {
55464
+ "use strict";
55465
+ ROOT_PROJECT_FIELDS = [
55466
+ ["rootAssetsMemberId", "Assets"],
55467
+ ["rootSaveFileMemberId", "Save"],
55468
+ ["rootSessionMemberId", "Session"]
55469
+ ];
55470
+ }
55471
+ });
55472
+
55286
55473
  // ../src/models/members/member-value-id.ts
55287
55474
  function derivedMemberValueId(memberId) {
55288
55475
  return inSystemRecordNamespaceOf(
@@ -70905,6 +71092,7 @@ function lowerProjectSchemaV4(base, analysis, options = {}) {
70905
71092
  ),
70906
71093
  projectFileIdsBySymbol: options.projectFileIdsBySymbol ?? /* @__PURE__ */ new Map(),
70907
71094
  rowBackedDefaultMemberIds: options.rowBackedDefaultMemberIds ?? /* @__PURE__ */ new Set(),
71095
+ rootValueTargetsByPath: options.rootValueTargetsByPath ?? /* @__PURE__ */ new Map(),
70908
71096
  declaredConstructors: buildDeclaredConstructorIndex(analysis),
70909
71097
  compiledFunctionBodiesByMemberId: new Map(
70910
71098
  analysis.compiledBodies.filter((body) => body.unit === "function").map((body) => [body.memberId, body.ir])
@@ -73404,7 +73592,7 @@ function validateProjectRootEnvelopeV4(state, analysis) {
73404
73592
  return { expectedIds, global, slots };
73405
73593
  }
73406
73594
  function rootMemberIds(project) {
73407
- return ROOT_PROJECT_FIELDS.map((field) => {
73595
+ return ROOT_PROJECT_FIELDS2.map((field) => {
73408
73596
  const id2 = project[field];
73409
73597
  if (typeof id2 !== "string" || id2.length === 0) {
73410
73598
  throw new Error(`Project record has no ${field}.`);
@@ -73507,47 +73695,16 @@ function walkRootValuePath(path, state, registry, fail) {
73507
73695
  }
73508
73696
  return cursor;
73509
73697
  }
73510
- function rootValuePathsByValueId(records2) {
73511
- const paths = /* @__PURE__ */ new Map();
73512
- const project = [...records2.values()].find(
73513
- (record3) => record3.recordKind === "project"
73514
- );
73515
- if (project === void 0 || !isObjectRecord2(project.data)) return paths;
73516
- const queue = [];
73517
- ROOT_PROJECT_FIELDS.forEach((field, index) => {
73518
- const slot = PROJECT_ROOT_SOURCE_SLOTS[index];
73519
- const memberId = isObjectRecord2(project.data) ? project.data[field] : void 0;
73520
- if (slot === void 0 || typeof memberId !== "string") return;
73521
- const member = records2.get(`member:${memberId}`);
73522
- const valueId = isObjectRecord2(member?.data) ? member.data.valueId : void 0;
73523
- if (typeof valueId !== "string") return;
73524
- queue.push({ id: valueId, path: `root.${slot.name}` });
73525
- });
73526
- while (queue.length > 0) {
73527
- const next = queue.shift();
73528
- if (next === void 0) break;
73529
- if (paths.has(next.id)) continue;
73530
- paths.set(next.id, next.path);
73531
- const row = records2.get(`value:${next.id}`);
73532
- const body = isObjectRecord2(row?.data) ? row.data.value : void 0;
73533
- if (!isObjectRecord2(body)) continue;
73534
- for (const [schemaKey, child] of Object.entries(body)) {
73535
- if (typeof child !== "string") continue;
73536
- if (!records2.has(`value:${child}`)) continue;
73537
- queue.push({ id: child, path: `${next.path}.${schemaKey}` });
73538
- }
73539
- }
73540
- return paths;
73541
- }
73542
- var ROOT_PROJECT_FIELDS, ROOT_PATH_PENDING_PREFIX;
73698
+ var ROOT_PROJECT_FIELDS2, ROOT_PATH_PENDING_PREFIX;
73543
73699
  var init_root_source = __esm({
73544
73700
  "src/project-source/root-source.ts"() {
73545
73701
  "use strict";
73546
73702
  init_src();
73547
73703
  init_projection();
73548
73704
  init_value_sources();
73705
+ init_root_value_paths();
73549
73706
  init_source_format();
73550
- ROOT_PROJECT_FIELDS = [
73707
+ ROOT_PROJECT_FIELDS2 = [
73551
73708
  "rootAssetsMemberId",
73552
73709
  "rootSaveFileMemberId",
73553
73710
  "rootSessionMemberId"
@@ -75329,19 +75486,19 @@ function neoScriptSchemaContractProjection(kind, value) {
75329
75486
  if (kind === "enum") ignored.add("optionKeyOrder");
75330
75487
  if (kind === "interface") ignored.add("memberKeyOrder");
75331
75488
  const projected = withoutFields(value, ignored);
75332
- if (kind === "enum" && isObjectRecord3(projected.options)) {
75489
+ if (kind === "enum" && isObjectRecord4(projected.options)) {
75333
75490
  projected.options = Object.fromEntries(
75334
75491
  Object.entries(projected.options).map(([id2, option]) => [
75335
75492
  id2,
75336
- isObjectRecord3(option) ? { id: option.id, name: option.name } : option
75493
+ isObjectRecord4(option) ? { id: option.id, name: option.name } : option
75337
75494
  ])
75338
75495
  );
75339
75496
  }
75340
- if (kind === "interface" && isObjectRecord3(projected.members)) {
75497
+ if (kind === "interface" && isObjectRecord4(projected.members)) {
75341
75498
  projected.members = Object.fromEntries(
75342
75499
  Object.entries(projected.members).map(([key, member]) => [
75343
75500
  key,
75344
- isObjectRecord3(member) ? withoutFields(member, /* @__PURE__ */ new Set(["docsText"])) : member
75501
+ isObjectRecord4(member) ? withoutFields(member, /* @__PURE__ */ new Set(["docsText"])) : member
75345
75502
  ])
75346
75503
  );
75347
75504
  }
@@ -75496,7 +75653,7 @@ function selectedRecordIds(records2, explicitIds, impactedIds, impactedTypeNames
75496
75653
  return selected;
75497
75654
  }
75498
75655
  function recordDependsOnChangedContract(record3, impactedIds, impactedTypeNames, constructedClassIds, impactedSourceNames) {
75499
- const recordId = isObjectRecord3(record3) ? record3.id : void 0;
75656
+ const recordId = isObjectRecord4(record3) ? record3.id : void 0;
75500
75657
  if (typeof recordId === "string" && impactedIds.has(recordId)) return true;
75501
75658
  if (intersects(collectCompilerReferenceIds(record3), impactedIds)) return true;
75502
75659
  if (intersects(collectCompilerReferenceNames(record3), impactedTypeNames)) {
@@ -75572,7 +75729,7 @@ function collectDeclaredSourceNames(value, names) {
75572
75729
  for (const child of value) collectDeclaredSourceNames(child, names);
75573
75730
  return;
75574
75731
  }
75575
- if (!isObjectRecord3(value)) return;
75732
+ if (!isObjectRecord4(value)) return;
75576
75733
  for (const [field, child] of Object.entries(value)) {
75577
75734
  if (typeof child === "string" && (field === "name" || field === "sourceName" || field === "sourceFunctionName" || field === "declaredTypeName" || field === "typeName")) {
75578
75735
  names.add(child);
@@ -75589,7 +75746,7 @@ function collectAuthoredNeoScriptSourceIdentifiers(value) {
75589
75746
  for (const entry of child) collect(entry);
75590
75747
  return;
75591
75748
  }
75592
- if (!isObjectRecord3(child)) return;
75749
+ if (!isObjectRecord4(child)) return;
75593
75750
  for (const [field, nested] of Object.entries(child)) {
75594
75751
  if ((field === "code" || field === "setterCode") && typeof nested === "string") {
75595
75752
  const result = lex(nested);
@@ -75613,7 +75770,7 @@ function containsAuthoredNeoScriptSource(value) {
75613
75770
  if (Array.isArray(value)) {
75614
75771
  return value.some(containsAuthoredNeoScriptSource);
75615
75772
  }
75616
- if (!isObjectRecord3(value)) return false;
75773
+ if (!isObjectRecord4(value)) return false;
75617
75774
  for (const [field, child] of Object.entries(value)) {
75618
75775
  if ((field === "code" || field === "setterCode") && typeof child === "string" && child.trim().length > 0) {
75619
75776
  return true;
@@ -75624,13 +75781,13 @@ function containsAuthoredNeoScriptSource(value) {
75624
75781
  }
75625
75782
  function containsCompiledNeoScriptBody(value) {
75626
75783
  if (Array.isArray(value)) return value.some(containsCompiledNeoScriptBody);
75627
- if (!isObjectRecord3(value)) return false;
75628
- if (Array.isArray(value.parameters) && Array.isArray(value.instructions) && isObjectRecord3(value.typeInfo)) {
75784
+ if (!isObjectRecord4(value)) return false;
75785
+ if (Array.isArray(value.parameters) && Array.isArray(value.instructions) && isObjectRecord4(value.typeInfo)) {
75629
75786
  return true;
75630
75787
  }
75631
75788
  return Object.values(value).some(containsCompiledNeoScriptBody);
75632
75789
  }
75633
- function isObjectRecord3(value) {
75790
+ function isObjectRecord4(value) {
75634
75791
  return value !== null && typeof value === "object" && !Array.isArray(value);
75635
75792
  }
75636
75793
  function constructorOwners(document) {
@@ -86799,6 +86956,9 @@ function computeWorkspaceStatus(workspace, options) {
86799
86956
  ),
86800
86957
  rowBackedDefaultMemberIds: rowBackedDefaultMemberIdsV4(
86801
86958
  workspace.state.records
86959
+ ),
86960
+ rootValueTargetsByPath: rootValueTargetsByPath(
86961
+ Object.values(workspace.state.records)
86802
86962
  )
86803
86963
  });
86804
86964
  memberDefaults = lowerMemberDefaultSourcesV4(
@@ -87828,6 +87988,7 @@ var init_workspace_status_core = __esm({
87828
87988
  init_core();
87829
87989
  init_value_sources();
87830
87990
  init_root_source();
87991
+ init_root_value_paths();
87831
87992
  init_project_documents();
87832
87993
  init_animation_clips();
87833
87994
  init_classes();
@@ -89089,6 +89250,7 @@ function buildValueEmitContext(records2, manifest) {
89089
89250
  localizedTexts,
89090
89251
  mainLocale: projectMainLocale(records2),
89091
89252
  symbolsByValueId,
89253
+ rootPathsByValueId: rootValuePathsByValueId(records2.values()),
89092
89254
  symbolsByDialogueId: new Map(
89093
89255
  [...dialogues].flatMap(
89094
89256
  ([id2, dialogue]) => typeof dialogue.sourceName === "string" ? [[id2, dialogue.sourceName]] : []
@@ -89458,6 +89620,7 @@ function buildValueLowerContext(state, manifest, options = {}) {
89458
89620
  ])
89459
89621
  ),
89460
89622
  staticValueIdsBySymbol: staticTargets,
89623
+ rootValueTargetsByPath: rootValueTargetsByPath(Object.values(state)),
89461
89624
  dialogueTargetsBySymbol: dialogueTargetsBySymbol(options.analysis),
89462
89625
  projectFileIdsBySymbol: projectFileIdsBySymbol(state, options.analysis),
89463
89626
  mainLocale: projectMainLocaleFromState(state),
@@ -93127,18 +93290,19 @@ function lowerActionValue(context, expression, ownerClassId) {
93127
93290
  const listeners = [];
93128
93291
  const identities = /* @__PURE__ */ new Set();
93129
93292
  elements.forEach((element, index) => {
93130
- const listener = lowerActionListener(context, element, ownerClassId, index);
93131
- if (identities.has(listener.memberId)) {
93293
+ const listener = lowerActionListener2(context, element, ownerClassId, index);
93294
+ const identity2 = `${listener.memberId}\0${listener.valueId ?? ""}`;
93295
+ if (identities.has(identity2)) {
93132
93296
  throw new Error(
93133
93297
  `NeoAction listener ${index} repeats an earlier listener; a listener set holds each target once.`
93134
93298
  );
93135
93299
  }
93136
- identities.add(listener.memberId);
93300
+ identities.add(identity2);
93137
93301
  listeners.push(listener);
93138
93302
  });
93139
93303
  return { listeners };
93140
93304
  }
93141
- function lowerActionListener(context, element, ownerClassId, index) {
93305
+ function lowerActionListener2(context, element, ownerClassId, index) {
93142
93306
  const { expression } = annotatedValue(element);
93143
93307
  if (expression.kind === "lambda") {
93144
93308
  throw new Error(
@@ -93146,35 +93310,51 @@ function lowerActionListener(context, element, ownerClassId, index) {
93146
93310
  );
93147
93311
  }
93148
93312
  const path = memberPath(expression);
93149
- const symbol = path?.startsWith("this.") ? path.slice("this.".length) : path;
93150
- if (symbol === null || symbol === void 0 || symbol.includes(".")) {
93313
+ if (path === null) {
93151
93314
  throw new Error(
93152
- `NeoAction listener ${index} must name a function, NSFunction, NSDelegate, or NeoAction member on the current Class.`
93315
+ `NeoAction listener ${index} must name a function, NSFunction, NSDelegate, or NeoAction member on the current Class or a receiver reachable through root.`
93153
93316
  );
93154
93317
  }
93155
- if (ownerClassId === void 0) {
93156
- throw new Error(
93157
- `NeoAction listener ${symbol} cannot be resolved without its owning Class.`
93158
- );
93318
+ const localSymbol2 = path.startsWith("this.") ? path.slice("this.".length) : path;
93319
+ let target;
93320
+ let valueId = null;
93321
+ if (!localSymbol2.includes(".")) {
93322
+ if (ownerClassId === void 0) {
93323
+ throw new Error(
93324
+ `NeoAction listener ${localSymbol2} cannot be resolved without its owning Class.`
93325
+ );
93326
+ }
93327
+ target = classMemberByName(context, ownerClassId, localSymbol2);
93328
+ } else {
93329
+ const separator = path.lastIndexOf(".");
93330
+ const receiverPath = path.slice(0, separator);
93331
+ const memberName = path.slice(separator + 1);
93332
+ const receiver = context.rootValueTargetsByPath.get(receiverPath);
93333
+ if (receiver === void 0) {
93334
+ throw new Error(
93335
+ `NeoAction listener ${index} receiver ${receiverPath} is not reachable through a deterministic root path.`
93336
+ );
93337
+ }
93338
+ target = classMemberByName(context, receiver.classId, memberName);
93339
+ valueId = receiver.valueId;
93159
93340
  }
93160
- const target = classMemberByName(context, ownerClassId, symbol);
93161
- if (target.kind === "action") return { memberId: target.id, valueId: null };
93341
+ if (target.kind === "action") return { memberId: target.id, valueId };
93162
93342
  if (target.kind !== "function" && target.kind !== "scriptFunction" && target.kind !== "delegate") {
93163
93343
  throw new Error(
93164
- `NeoAction listener ${symbol} does not resolve to a function, NSFunction, NSDelegate, or NeoAction member.`
93344
+ `NeoAction listener ${path} does not resolve to a function, NSFunction, NSDelegate, or NeoAction member.`
93165
93345
  );
93166
93346
  }
93167
93347
  if (target.returnType.kind !== "void") {
93168
93348
  throw new Error(
93169
- `NeoAction listener ${symbol} returns a value; a listener must be void.`
93349
+ `NeoAction listener ${path} returns a value; a listener must be void.`
93170
93350
  );
93171
93351
  }
93172
93352
  if (target.kind !== "delegate" && target.deferred) {
93173
93353
  throw new Error(
93174
- `NeoAction listener ${symbol} is deferred; listeners are immediate-only.`
93354
+ `NeoAction listener ${path} is deferred; listeners are immediate-only.`
93175
93355
  );
93176
93356
  }
93177
- return { memberId: target.id, valueId: null };
93357
+ return { memberId: target.id, valueId };
93178
93358
  }
93179
93359
  function lowerDelegateValue(context, expression, ownerClassId, authoredSlice) {
93180
93360
  if (expression.kind === "lambda") {
@@ -94723,11 +94903,6 @@ function actionListenerName(context, listener, index) {
94723
94903
  if (typeof listener.memberId !== "string") {
94724
94904
  throw new Error(`NSAction listener ${index} is missing memberId.`);
94725
94905
  }
94726
- if (listener.valueId !== null && listener.valueId !== void 0) {
94727
- throw new Error(
94728
- `NSAction listener ${index} is instance-bound and cannot be represented by current project source.`
94729
- );
94730
- }
94731
94906
  const target = context.members.get(listener.memberId);
94732
94907
  if (target === void 0 || typeof target.name !== "string") {
94733
94908
  throw new Error(`NSAction listener ${listener.memberId} was not pulled.`);
@@ -94736,7 +94911,19 @@ function actionListenerName(context, listener, index) {
94736
94911
  if (kind !== 13 && kind !== 23 && kind !== 25 && kind !== 26) {
94737
94912
  throw new Error(`NSAction listener ${listener.memberId} is not callable.`);
94738
94913
  }
94739
- return target.name;
94914
+ if (listener.valueId === null || listener.valueId === void 0) {
94915
+ return target.name;
94916
+ }
94917
+ if (typeof listener.valueId !== "string") {
94918
+ throw new Error(`NSAction listener ${index} has a non-string valueId.`);
94919
+ }
94920
+ const receiver = context.rootPathsByValueId.get(listener.valueId);
94921
+ if (receiver === void 0) {
94922
+ throw new Error(
94923
+ `NSAction listener ${index} is bound to value ${listener.valueId}, which has no deterministic root path.`
94924
+ );
94925
+ }
94926
+ return `${receiver}.${target.name}`;
94740
94927
  }
94741
94928
  function fileValue(context, kind, label, body, required2) {
94742
94929
  if (body === null || body === void 0) {
@@ -94939,6 +95126,7 @@ var init_value_sources = __esm({
94939
95126
  init_declared_constructors2();
94940
95127
  init_init_source();
94941
95128
  init_source_format();
95129
+ init_root_value_paths();
94942
95130
  init_world_system_classes();
94943
95131
  init_member_value_id();
94944
95132
  init_members();
@@ -94981,7 +95169,7 @@ function emitProjectDocumentFilesV4(records2) {
94981
95169
  defaultInitializers: memberDefaults.initializers,
94982
95170
  fileSymbols: qualifiedProjectFileSymbolsV4(records2),
94983
95171
  relationEndpointExpressions: relationEndpointExpressions(records2),
94984
- collectionValuePaths: rootValuePathsByValueId(records2)
95172
+ collectionValuePaths: rootValuePathsByValueId(records2.values())
94985
95173
  });
94986
95174
  const source = {
94987
95175
  ...baseSource,
@@ -95110,6 +95298,7 @@ var init_project_documents = __esm({
95110
95298
  init_dialogue_sources();
95111
95299
  init_value_sources();
95112
95300
  init_root_source();
95301
+ init_root_value_paths();
95113
95302
  init_source_diagnostics();
95114
95303
  init_source_format();
95115
95304
  PROJECT_SOURCE_ANALYSIS_V4_CACHE_PATH = ".neo/project-source-analysis-v4.json";
@@ -99388,6 +99577,9 @@ function describeBodySite(site) {
99388
99577
  return `Migration '${site.memberName}' (target ${site.ownerName})`;
99389
99578
  }
99390
99579
  const unit = unitLabel(site.unit);
99580
+ if ("recordName" in site) {
99581
+ return `${unit} '${site.recordName}'`;
99582
+ }
99391
99583
  if (site.ownerName === null) {
99392
99584
  return `${unit} '${site.memberName}' (member ${site.recordId}, whose declaring class is not in this push's schema)`;
99393
99585
  }
@@ -100413,6 +100605,13 @@ function buildStoredNeoScriptChecks(document) {
100413
100605
  name: nodeId,
100414
100606
  unit: "condition",
100415
100607
  path: `dialogueNodes.${nodeId}`,
100608
+ bodySite: {
100609
+ recordKind: "dialogue-node",
100610
+ recordId: nodeId,
100611
+ recordName: nodeId,
100612
+ unit: "getter",
100613
+ code: ""
100614
+ },
100416
100615
  run: () => {
100417
100616
  throw new Error(
100418
100617
  `Dialogue node "${nodeId}" references missing dialogue "${String(node.dialogueId)}".`
@@ -100443,6 +100642,13 @@ function buildStoredNeoScriptChecks(document) {
100443
100642
  name: recordName,
100444
100643
  unit: "condition",
100445
100644
  path: `${path}.${index}`,
100645
+ bodySite: {
100646
+ recordKind: "dialogue-node",
100647
+ recordId: nodeId,
100648
+ recordName,
100649
+ unit: "getter",
100650
+ code: typeof conditionValue.code === "string" ? conditionValue.code : ""
100651
+ },
100446
100652
  run: () => {
100447
100653
  compileDialogueLogicWithProjectData(
100448
100654
  {
@@ -100469,6 +100675,13 @@ function buildStoredNeoScriptChecks(document) {
100469
100675
  name: recordName,
100470
100676
  unit: "text-variable",
100471
100677
  path: `${path}.${variableKey}`,
100678
+ bodySite: {
100679
+ recordKind: "dialogue-node",
100680
+ recordId: nodeId,
100681
+ recordName,
100682
+ unit: "getter",
100683
+ code: ""
100684
+ },
100472
100685
  run: () => {
100473
100686
  compileDialogueLogicWithProjectData(
100474
100687
  {
@@ -100544,6 +100757,13 @@ function buildStoredNeoScriptChecks(document) {
100544
100757
  name: recordName,
100545
100758
  unit: "action",
100546
100759
  path: `${nodePath}.actions.${actionId}.logic`,
100760
+ bodySite: {
100761
+ recordKind: "dialogue-node",
100762
+ recordId: nodeId,
100763
+ recordName,
100764
+ unit: "action",
100765
+ code: typeof logic.code === "string" ? logic.code : ""
100766
+ },
100547
100767
  run: () => {
100548
100768
  compileDialogueLogicWithProjectData(
100549
100769
  {
@@ -100588,6 +100808,13 @@ function buildStoredNeoScriptChecks(document) {
100588
100808
  name: groupName,
100589
100809
  unit: "condition",
100590
100810
  path: `dialogueGroups.${groupId}.conditions.${index}`,
100811
+ bodySite: {
100812
+ recordKind: "dialogue-group",
100813
+ recordId: groupId,
100814
+ recordName: groupName,
100815
+ unit: "getter",
100816
+ code: typeof conditionValue.code === "string" ? conditionValue.code : ""
100817
+ },
100591
100818
  run: () => {
100592
100819
  compileDialogueLogicWithProjectData(
100593
100820
  {
@@ -100615,6 +100842,14 @@ function buildStoredNeoScriptChecks(document) {
100615
100842
  name: migrationName,
100616
100843
  unit: "migration",
100617
100844
  path: `migrations.${migrationId}.code`,
100845
+ bodySite: {
100846
+ recordKind: "migration",
100847
+ recordId: migrationId,
100848
+ ownerName: null,
100849
+ memberName: migrationName,
100850
+ unit: "migration",
100851
+ code: typeof migration.code === "string" ? migration.code : ""
100852
+ },
100618
100853
  run: () => {
100619
100854
  if (typeof migration.code !== "string") {
100620
100855
  throw new Error(`Migration "${migrationId}" is missing source code.`);
@@ -100659,9 +100894,6 @@ function optionalString4(value) {
100659
100894
  return typeof value === "string" ? value : null;
100660
100895
  }
100661
100896
  function neoScriptCheckErrorMessage(error) {
100662
- if (error instanceof CompileError) {
100663
- return `${error.line}:${error.column} ${error.message}`;
100664
- }
100665
100897
  return error instanceof Error ? error.message : String(error);
100666
100898
  }
100667
100899
  function resolveLocalScriptMember(manifest, memberRef) {
@@ -100906,7 +101138,7 @@ function runCheckAll(document, manifest, options) {
100906
101138
  for (const check of buildStoredNeoScriptChecks(document)) {
100907
101139
  checked += 1;
100908
101140
  try {
100909
- check.run();
101141
+ compileNeoScriptBodyOrThrow(check.run, check.bodySite, null);
100910
101142
  results.push({
100911
101143
  id: check.id,
100912
101144
  name: check.name,
@@ -101668,11 +101900,11 @@ function assertProjectFileAuthoringRecordIdentifiers(recordKind, data) {
101668
101900
  if (recordKind !== "unity-texture-template" && recordKind !== "unity-audio-clip-template") {
101669
101901
  return;
101670
101902
  }
101671
- if (!isObjectRecord4(data) || typeof data.name !== "string") return;
101903
+ if (!isObjectRecord5(data) || typeof data.name !== "string") return;
101672
101904
  const label = recordKind === "unity-texture-template" ? "Unity texture template name" : "Unity audio clip template name";
101673
101905
  assertProjectFileAuthoringIdentifier(data.name, label);
101674
101906
  }
101675
- function isObjectRecord4(value) {
101907
+ function isObjectRecord5(value) {
101676
101908
  return typeof value === "object" && value !== null && !Array.isArray(value);
101677
101909
  }
101678
101910
  var init_project_file_authoring_identifiers = __esm({
@@ -104614,7 +104846,7 @@ var init_registry2 = __esm({
104614
104846
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
104615
104847
  formatVersion: 3,
104616
104848
  contractVersion: "3.11",
104617
- cliVersion: "0.30.1",
104849
+ cliVersion: "0.30.3",
104618
104850
  projectFileUploadBatchSize: 32,
104619
104851
  documentRecords: {
104620
104852
  member: {
@@ -111182,7 +111414,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
111182
111414
  async function main() {
111183
111415
  const args = parseArgs(process.argv.slice(2));
111184
111416
  if (args.command === "--version") {
111185
- console.log("0.30.1");
111417
+ console.log("0.30.3");
111186
111418
  return;
111187
111419
  }
111188
111420
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.3",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.30.1 -->
12
+ <!-- reviewed-through-cli: 0.30.3 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -83,7 +83,7 @@ wrappers.
83
83
  The marker near the top of `SKILL.md` must exactly match the package version:
84
84
 
85
85
  ```html
86
- <!-- reviewed-through-cli: 0.30.1 -->
86
+ <!-- reviewed-through-cli: 0.30.3 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -110,6 +110,12 @@ empty set.
110
110
  public NeoAction<int> OnDamaged = [PlayHitFlash, ApplyDamage];
111
111
  ```
112
112
 
113
+ An instance-bound listener reachable through a stable project root keeps its
114
+ receiver path, for example `public NeoAction<int> OnDamaged =
115
+ [root.Assets.OnDamage];`. Pull refuses a bound receiver that has no
116
+ deterministic `root.*` path instead of emitting a source expression that would
117
+ silently change the target.
118
+
113
119
  Subscribe and unsubscribe with `+=` and `-=` against the member reference
114
120
  itself; both are durable writes deduplicated by target identity, so
115
121
  re-running a subscription cannot double-subscribe. Invoke it as a statement —