@neocompose/cli 0.30.1 → 0.30.2

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.2] - 2026-08-13
4
+
5
+ ### Fixed
6
+
7
+ - Pull instance-bound `NeoAction` listeners through their deterministic
8
+ `root.*` receiver paths and lower those paths back to the same listener
9
+ identity. Web-authored actions such as `[root.Assets.OnDamage]` now pull and
10
+ push without losing the receiver or leaving a phantom member change.
11
+
3
12
  ## [0.30.1] - 2026-08-13
4
13
 
5
14
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -47422,12 +47422,26 @@ function renderActionListeners(context, member, value) {
47422
47422
  `NeoAction member ${member.name} listener ${index} is missing memberId.`
47423
47423
  );
47424
47424
  }
47425
- if (listener.valueId !== null && listener.valueId !== void 0) {
47425
+ const target = required(
47426
+ context.members,
47427
+ listener.memberId,
47428
+ "action listener"
47429
+ );
47430
+ if (listener.valueId === null || listener.valueId === void 0) {
47431
+ return target.name;
47432
+ }
47433
+ if (typeof listener.valueId !== "string") {
47434
+ throw new Error(
47435
+ `NeoAction member ${member.name} listener ${index} has a non-string receiver valueId.`
47436
+ );
47437
+ }
47438
+ const receiver = context.collectionValuePaths.get(listener.valueId);
47439
+ if (receiver === void 0) {
47426
47440
  throw new Error(
47427
- `NeoAction member ${member.name} listener ${index} is instance-bound, which no declaration default can spell.`
47441
+ `NeoAction member ${member.name} listener ${index} is bound to value ${listener.valueId}, which has no deterministic root path.`
47428
47442
  );
47429
47443
  }
47430
- return required(context.members, listener.memberId, "action listener").name;
47444
+ return `${receiver}.${target.name}`;
47431
47445
  });
47432
47446
  return `[${names.join(", ")}]`;
47433
47447
  }
@@ -53431,6 +53445,7 @@ function lowerFieldMember(context, ownerClass, declaration, id2, commonInput, ba
53431
53445
  `NeoAction member ${ownerClass.name}.${declaration.name} cannot be nullable; an action's rest state is an empty listener list.`
53432
53446
  );
53433
53447
  }
53448
+ const baseAction = base?.kind === "action" ? base : void 0;
53434
53449
  return {
53435
53450
  ...common,
53436
53451
  kind: "action",
@@ -53439,7 +53454,10 @@ function lowerFieldMember(context, ownerClass, declaration, id2, commonInput, ba
53439
53454
  // fixed false because nullability has no meaning here (P62 §2.1).
53440
53455
  required: false,
53441
53456
  arguments: fieldType.arguments.map((argumentType, index) => ({
53442
- name: `p${index + 1}`,
53457
+ // NeoAction's generic spelling carries types but no parameter names.
53458
+ // Preserve a pulled name when this stable declaration already exists;
53459
+ // `pN` remains the deterministic name for a newly authored action.
53460
+ name: baseAction?.arguments[index]?.name ?? `p${index + 1}`,
53443
53461
  type: lowerType(context, argumentType, ownerClass),
53444
53462
  default: null,
53445
53463
  source: span(declaration),
@@ -54168,23 +54186,24 @@ function lowerActionDefault(context, expression, ownerClass, path) {
54168
54186
  const listeners = [];
54169
54187
  const identities = /* @__PURE__ */ new Set();
54170
54188
  elements.forEach((element, index) => {
54171
- const memberId = lowerActionListenerId(
54189
+ const listener = lowerActionListener(
54172
54190
  context,
54173
54191
  element,
54174
54192
  ownerClass,
54175
54193
  `${path}[${index}]`
54176
54194
  );
54177
- if (identities.has(memberId)) {
54195
+ const identity2 = `${listener.memberId}\0${listener.valueId ?? ""}`;
54196
+ if (identities.has(identity2)) {
54178
54197
  throw new Error(
54179
54198
  `NeoAction listener ${path}[${index}] repeats an earlier listener; a listener set holds each target once.`
54180
54199
  );
54181
54200
  }
54182
- identities.add(memberId);
54183
- listeners.push({ memberId, valueId: null });
54201
+ identities.add(identity2);
54202
+ listeners.push(listener);
54184
54203
  });
54185
54204
  return { listeners };
54186
54205
  }
54187
- function lowerActionListenerId(context, element, ownerClass, path) {
54206
+ function lowerActionListener(context, element, ownerClass, path) {
54188
54207
  let current = element;
54189
54208
  while (current.kind === "annotated") current = current.expression;
54190
54209
  if (current.kind === "lambda") {
@@ -54192,21 +54211,26 @@ function lowerActionListenerId(context, element, ownerClass, path) {
54192
54211
  `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
54212
  );
54194
54213
  }
54195
- const name = actionListenerSymbol(current);
54196
- if (name === null) {
54214
+ const target = actionListenerTarget(context, current, ownerClass);
54215
+ if (target === null) {
54197
54216
  throw new Error(
54198
- `NeoAction listener ${path} must name a callable member on the current Class.`
54217
+ `NeoAction listener ${path} must name a callable member on the current Class or a receiver reachable through root.`
54199
54218
  );
54200
54219
  }
54201
- const classId = materializedId(ownerClass, "class", ownerClass.name);
54202
- const memberId = effectiveMemberIdByName(context, classId, name);
54220
+ const memberId = effectiveMemberIdByName(
54221
+ context,
54222
+ target.classId,
54223
+ target.memberName
54224
+ );
54203
54225
  if (memberId === null) {
54204
54226
  throw new Error(
54205
- `NeoAction listener ${path} names ${JSON.stringify(name)}, which ${ownerClass.name} does not declare or inherit.`
54227
+ `NeoAction listener ${path} names ${JSON.stringify(target.memberName)}, which its receiver Class does not declare or inherit.`
54206
54228
  );
54207
54229
  }
54208
54230
  const declaration = context.sourceMembersById.get(memberId)?.member;
54209
- if (declaration === void 0) return memberId;
54231
+ if (declaration === void 0) {
54232
+ return { memberId, valueId: target.valueId };
54233
+ }
54210
54234
  if (declaration.kind === "function") {
54211
54235
  if (declaration.type.name !== "void") {
54212
54236
  throw new Error(
@@ -54218,21 +54242,46 @@ function lowerActionListenerId(context, element, ownerClass, path) {
54218
54242
  `NeoAction listener ${path} targets deferred ${declaration.name}; listeners are immediate-only.`
54219
54243
  );
54220
54244
  }
54221
- return memberId;
54245
+ return { memberId, valueId: target.valueId };
54222
54246
  }
54223
54247
  if (declaration.type.name !== "NeoAction" && declaration.type.name !== "NeoDelegate") {
54224
54248
  throw new Error(
54225
54249
  `NeoAction listener ${path} targets ${declaration.name}, which is not a function, NSFunction, NSDelegate, or NeoAction member.`
54226
54250
  );
54227
54251
  }
54228
- return memberId;
54252
+ return { memberId, valueId: target.valueId };
54229
54253
  }
54230
- function actionListenerSymbol(expression) {
54254
+ function actionListenerTarget(context, expression, ownerClass) {
54255
+ const expressionPath2 = actionListenerPath(expression);
54256
+ if (expressionPath2 === null) return null;
54257
+ if (!expressionPath2.includes(".")) {
54258
+ return {
54259
+ classId: materializedId(ownerClass, "class", ownerClass.name),
54260
+ memberName: expressionPath2,
54261
+ valueId: null
54262
+ };
54263
+ }
54264
+ if (expressionPath2.startsWith("this.")) {
54265
+ const memberName2 = expressionPath2.slice("this.".length);
54266
+ if (memberName2.includes(".")) return null;
54267
+ return {
54268
+ classId: materializedId(ownerClass, "class", ownerClass.name),
54269
+ memberName: memberName2,
54270
+ valueId: null
54271
+ };
54272
+ }
54273
+ const separator = expressionPath2.lastIndexOf(".");
54274
+ const receiverPath = expressionPath2.slice(0, separator);
54275
+ const memberName = expressionPath2.slice(separator + 1);
54276
+ const receiver = context.rootValueTargetsByPath.get(receiverPath);
54277
+ if (receiver === void 0) return null;
54278
+ return { ...receiver, memberName };
54279
+ }
54280
+ function actionListenerPath(expression) {
54231
54281
  if (expression.kind === "ident") return expression.name;
54232
54282
  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;
54283
+ const receiver = actionListenerPath(expression.receiver);
54284
+ return receiver === null ? null : `${receiver}.${expression.name}`;
54236
54285
  }
54237
54286
  function lowerExpressionValue(context, expression, declaredExpected, ownerClass, path, genericEnvironment = EMPTY_GENERIC_TYPE_ENVIRONMENT, sourceText) {
54238
54287
  const expected = substituteGenericTypeNames(
@@ -55283,6 +55332,81 @@ var init_lower_members = __esm({
55283
55332
  }
55284
55333
  });
55285
55334
 
55335
+ // src/project-source/root-value-paths.ts
55336
+ function rootValuePathsByValueId(records2) {
55337
+ const indexed = indexRecords(records2);
55338
+ const paths = /* @__PURE__ */ new Map();
55339
+ const queue = [];
55340
+ for (const [field, slot] of ROOT_PROJECT_FIELDS) {
55341
+ const memberId = indexed.project?.[field];
55342
+ const member = typeof memberId === "string" ? indexed.members.get(memberId) : void 0;
55343
+ const valueId = member?.valueId;
55344
+ if (typeof valueId === "string") {
55345
+ queue.push({ id: valueId, path: `root.${slot}` });
55346
+ }
55347
+ }
55348
+ while (queue.length > 0) {
55349
+ const next = queue.shift();
55350
+ if (next === void 0) break;
55351
+ if (paths.has(next.id)) continue;
55352
+ paths.set(next.id, next.path);
55353
+ const body = indexed.values.get(next.id)?.value;
55354
+ if (!isObjectRecord3(body)) continue;
55355
+ for (const [schemaKey, child] of Object.entries(body)) {
55356
+ if (typeof child !== "string") continue;
55357
+ if (!indexed.values.has(child)) continue;
55358
+ queue.push({ id: child, path: `${next.path}.${schemaKey}` });
55359
+ }
55360
+ }
55361
+ return paths;
55362
+ }
55363
+ function rootValueTargetsByPath(records2) {
55364
+ const allRecords = [...records2];
55365
+ const paths = rootValuePathsByValueId(allRecords);
55366
+ const values = new Map(
55367
+ allRecords.flatMap(
55368
+ (record3) => !record3.deleted && record3.recordKind === "value" && isObjectRecord3(record3.data) ? [[record3.recordId, record3.data]] : []
55369
+ )
55370
+ );
55371
+ const targets = /* @__PURE__ */ new Map();
55372
+ for (const [valueId, path] of paths) {
55373
+ const classId = values.get(valueId)?.classId;
55374
+ if (typeof classId === "string") {
55375
+ targets.set(path, { valueId, classId });
55376
+ }
55377
+ }
55378
+ return targets;
55379
+ }
55380
+ function indexRecords(records2) {
55381
+ let project;
55382
+ const members = /* @__PURE__ */ new Map();
55383
+ const values = /* @__PURE__ */ new Map();
55384
+ for (const record3 of records2) {
55385
+ if (record3.deleted || !isObjectRecord3(record3.data)) continue;
55386
+ if (record3.recordKind === "project") project ??= record3.data;
55387
+ else if (record3.recordKind === "member") {
55388
+ members.set(record3.recordId, record3.data);
55389
+ } else if (record3.recordKind === "value") {
55390
+ values.set(record3.recordId, record3.data);
55391
+ }
55392
+ }
55393
+ return { project, members, values };
55394
+ }
55395
+ function isObjectRecord3(value) {
55396
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55397
+ }
55398
+ var ROOT_PROJECT_FIELDS;
55399
+ var init_root_value_paths = __esm({
55400
+ "src/project-source/root-value-paths.ts"() {
55401
+ "use strict";
55402
+ ROOT_PROJECT_FIELDS = [
55403
+ ["rootAssetsMemberId", "Assets"],
55404
+ ["rootSaveFileMemberId", "Save"],
55405
+ ["rootSessionMemberId", "Session"]
55406
+ ];
55407
+ }
55408
+ });
55409
+
55286
55410
  // ../src/models/members/member-value-id.ts
55287
55411
  function derivedMemberValueId(memberId) {
55288
55412
  return inSystemRecordNamespaceOf(
@@ -70905,6 +71029,7 @@ function lowerProjectSchemaV4(base, analysis, options = {}) {
70905
71029
  ),
70906
71030
  projectFileIdsBySymbol: options.projectFileIdsBySymbol ?? /* @__PURE__ */ new Map(),
70907
71031
  rowBackedDefaultMemberIds: options.rowBackedDefaultMemberIds ?? /* @__PURE__ */ new Set(),
71032
+ rootValueTargetsByPath: options.rootValueTargetsByPath ?? /* @__PURE__ */ new Map(),
70908
71033
  declaredConstructors: buildDeclaredConstructorIndex(analysis),
70909
71034
  compiledFunctionBodiesByMemberId: new Map(
70910
71035
  analysis.compiledBodies.filter((body) => body.unit === "function").map((body) => [body.memberId, body.ir])
@@ -73404,7 +73529,7 @@ function validateProjectRootEnvelopeV4(state, analysis) {
73404
73529
  return { expectedIds, global, slots };
73405
73530
  }
73406
73531
  function rootMemberIds(project) {
73407
- return ROOT_PROJECT_FIELDS.map((field) => {
73532
+ return ROOT_PROJECT_FIELDS2.map((field) => {
73408
73533
  const id2 = project[field];
73409
73534
  if (typeof id2 !== "string" || id2.length === 0) {
73410
73535
  throw new Error(`Project record has no ${field}.`);
@@ -73507,47 +73632,16 @@ function walkRootValuePath(path, state, registry, fail) {
73507
73632
  }
73508
73633
  return cursor;
73509
73634
  }
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;
73635
+ var ROOT_PROJECT_FIELDS2, ROOT_PATH_PENDING_PREFIX;
73543
73636
  var init_root_source = __esm({
73544
73637
  "src/project-source/root-source.ts"() {
73545
73638
  "use strict";
73546
73639
  init_src();
73547
73640
  init_projection();
73548
73641
  init_value_sources();
73642
+ init_root_value_paths();
73549
73643
  init_source_format();
73550
- ROOT_PROJECT_FIELDS = [
73644
+ ROOT_PROJECT_FIELDS2 = [
73551
73645
  "rootAssetsMemberId",
73552
73646
  "rootSaveFileMemberId",
73553
73647
  "rootSessionMemberId"
@@ -75329,19 +75423,19 @@ function neoScriptSchemaContractProjection(kind, value) {
75329
75423
  if (kind === "enum") ignored.add("optionKeyOrder");
75330
75424
  if (kind === "interface") ignored.add("memberKeyOrder");
75331
75425
  const projected = withoutFields(value, ignored);
75332
- if (kind === "enum" && isObjectRecord3(projected.options)) {
75426
+ if (kind === "enum" && isObjectRecord4(projected.options)) {
75333
75427
  projected.options = Object.fromEntries(
75334
75428
  Object.entries(projected.options).map(([id2, option]) => [
75335
75429
  id2,
75336
- isObjectRecord3(option) ? { id: option.id, name: option.name } : option
75430
+ isObjectRecord4(option) ? { id: option.id, name: option.name } : option
75337
75431
  ])
75338
75432
  );
75339
75433
  }
75340
- if (kind === "interface" && isObjectRecord3(projected.members)) {
75434
+ if (kind === "interface" && isObjectRecord4(projected.members)) {
75341
75435
  projected.members = Object.fromEntries(
75342
75436
  Object.entries(projected.members).map(([key, member]) => [
75343
75437
  key,
75344
- isObjectRecord3(member) ? withoutFields(member, /* @__PURE__ */ new Set(["docsText"])) : member
75438
+ isObjectRecord4(member) ? withoutFields(member, /* @__PURE__ */ new Set(["docsText"])) : member
75345
75439
  ])
75346
75440
  );
75347
75441
  }
@@ -75496,7 +75590,7 @@ function selectedRecordIds(records2, explicitIds, impactedIds, impactedTypeNames
75496
75590
  return selected;
75497
75591
  }
75498
75592
  function recordDependsOnChangedContract(record3, impactedIds, impactedTypeNames, constructedClassIds, impactedSourceNames) {
75499
- const recordId = isObjectRecord3(record3) ? record3.id : void 0;
75593
+ const recordId = isObjectRecord4(record3) ? record3.id : void 0;
75500
75594
  if (typeof recordId === "string" && impactedIds.has(recordId)) return true;
75501
75595
  if (intersects(collectCompilerReferenceIds(record3), impactedIds)) return true;
75502
75596
  if (intersects(collectCompilerReferenceNames(record3), impactedTypeNames)) {
@@ -75572,7 +75666,7 @@ function collectDeclaredSourceNames(value, names) {
75572
75666
  for (const child of value) collectDeclaredSourceNames(child, names);
75573
75667
  return;
75574
75668
  }
75575
- if (!isObjectRecord3(value)) return;
75669
+ if (!isObjectRecord4(value)) return;
75576
75670
  for (const [field, child] of Object.entries(value)) {
75577
75671
  if (typeof child === "string" && (field === "name" || field === "sourceName" || field === "sourceFunctionName" || field === "declaredTypeName" || field === "typeName")) {
75578
75672
  names.add(child);
@@ -75589,7 +75683,7 @@ function collectAuthoredNeoScriptSourceIdentifiers(value) {
75589
75683
  for (const entry of child) collect(entry);
75590
75684
  return;
75591
75685
  }
75592
- if (!isObjectRecord3(child)) return;
75686
+ if (!isObjectRecord4(child)) return;
75593
75687
  for (const [field, nested] of Object.entries(child)) {
75594
75688
  if ((field === "code" || field === "setterCode") && typeof nested === "string") {
75595
75689
  const result = lex(nested);
@@ -75613,7 +75707,7 @@ function containsAuthoredNeoScriptSource(value) {
75613
75707
  if (Array.isArray(value)) {
75614
75708
  return value.some(containsAuthoredNeoScriptSource);
75615
75709
  }
75616
- if (!isObjectRecord3(value)) return false;
75710
+ if (!isObjectRecord4(value)) return false;
75617
75711
  for (const [field, child] of Object.entries(value)) {
75618
75712
  if ((field === "code" || field === "setterCode") && typeof child === "string" && child.trim().length > 0) {
75619
75713
  return true;
@@ -75624,13 +75718,13 @@ function containsAuthoredNeoScriptSource(value) {
75624
75718
  }
75625
75719
  function containsCompiledNeoScriptBody(value) {
75626
75720
  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)) {
75721
+ if (!isObjectRecord4(value)) return false;
75722
+ if (Array.isArray(value.parameters) && Array.isArray(value.instructions) && isObjectRecord4(value.typeInfo)) {
75629
75723
  return true;
75630
75724
  }
75631
75725
  return Object.values(value).some(containsCompiledNeoScriptBody);
75632
75726
  }
75633
- function isObjectRecord3(value) {
75727
+ function isObjectRecord4(value) {
75634
75728
  return value !== null && typeof value === "object" && !Array.isArray(value);
75635
75729
  }
75636
75730
  function constructorOwners(document) {
@@ -86799,6 +86893,9 @@ function computeWorkspaceStatus(workspace, options) {
86799
86893
  ),
86800
86894
  rowBackedDefaultMemberIds: rowBackedDefaultMemberIdsV4(
86801
86895
  workspace.state.records
86896
+ ),
86897
+ rootValueTargetsByPath: rootValueTargetsByPath(
86898
+ Object.values(workspace.state.records)
86802
86899
  )
86803
86900
  });
86804
86901
  memberDefaults = lowerMemberDefaultSourcesV4(
@@ -87828,6 +87925,7 @@ var init_workspace_status_core = __esm({
87828
87925
  init_core();
87829
87926
  init_value_sources();
87830
87927
  init_root_source();
87928
+ init_root_value_paths();
87831
87929
  init_project_documents();
87832
87930
  init_animation_clips();
87833
87931
  init_classes();
@@ -89089,6 +89187,7 @@ function buildValueEmitContext(records2, manifest) {
89089
89187
  localizedTexts,
89090
89188
  mainLocale: projectMainLocale(records2),
89091
89189
  symbolsByValueId,
89190
+ rootPathsByValueId: rootValuePathsByValueId(records2.values()),
89092
89191
  symbolsByDialogueId: new Map(
89093
89192
  [...dialogues].flatMap(
89094
89193
  ([id2, dialogue]) => typeof dialogue.sourceName === "string" ? [[id2, dialogue.sourceName]] : []
@@ -89458,6 +89557,7 @@ function buildValueLowerContext(state, manifest, options = {}) {
89458
89557
  ])
89459
89558
  ),
89460
89559
  staticValueIdsBySymbol: staticTargets,
89560
+ rootValueTargetsByPath: rootValueTargetsByPath(Object.values(state)),
89461
89561
  dialogueTargetsBySymbol: dialogueTargetsBySymbol(options.analysis),
89462
89562
  projectFileIdsBySymbol: projectFileIdsBySymbol(state, options.analysis),
89463
89563
  mainLocale: projectMainLocaleFromState(state),
@@ -93127,18 +93227,19 @@ function lowerActionValue(context, expression, ownerClassId) {
93127
93227
  const listeners = [];
93128
93228
  const identities = /* @__PURE__ */ new Set();
93129
93229
  elements.forEach((element, index) => {
93130
- const listener = lowerActionListener(context, element, ownerClassId, index);
93131
- if (identities.has(listener.memberId)) {
93230
+ const listener = lowerActionListener2(context, element, ownerClassId, index);
93231
+ const identity2 = `${listener.memberId}\0${listener.valueId ?? ""}`;
93232
+ if (identities.has(identity2)) {
93132
93233
  throw new Error(
93133
93234
  `NeoAction listener ${index} repeats an earlier listener; a listener set holds each target once.`
93134
93235
  );
93135
93236
  }
93136
- identities.add(listener.memberId);
93237
+ identities.add(identity2);
93137
93238
  listeners.push(listener);
93138
93239
  });
93139
93240
  return { listeners };
93140
93241
  }
93141
- function lowerActionListener(context, element, ownerClassId, index) {
93242
+ function lowerActionListener2(context, element, ownerClassId, index) {
93142
93243
  const { expression } = annotatedValue(element);
93143
93244
  if (expression.kind === "lambda") {
93144
93245
  throw new Error(
@@ -93146,35 +93247,51 @@ function lowerActionListener(context, element, ownerClassId, index) {
93146
93247
  );
93147
93248
  }
93148
93249
  const path = memberPath(expression);
93149
- const symbol = path?.startsWith("this.") ? path.slice("this.".length) : path;
93150
- if (symbol === null || symbol === void 0 || symbol.includes(".")) {
93250
+ if (path === null) {
93151
93251
  throw new Error(
93152
- `NeoAction listener ${index} must name a function, NSFunction, NSDelegate, or NeoAction member on the current Class.`
93252
+ `NeoAction listener ${index} must name a function, NSFunction, NSDelegate, or NeoAction member on the current Class or a receiver reachable through root.`
93153
93253
  );
93154
93254
  }
93155
- if (ownerClassId === void 0) {
93156
- throw new Error(
93157
- `NeoAction listener ${symbol} cannot be resolved without its owning Class.`
93158
- );
93255
+ const localSymbol2 = path.startsWith("this.") ? path.slice("this.".length) : path;
93256
+ let target;
93257
+ let valueId = null;
93258
+ if (!localSymbol2.includes(".")) {
93259
+ if (ownerClassId === void 0) {
93260
+ throw new Error(
93261
+ `NeoAction listener ${localSymbol2} cannot be resolved without its owning Class.`
93262
+ );
93263
+ }
93264
+ target = classMemberByName(context, ownerClassId, localSymbol2);
93265
+ } else {
93266
+ const separator = path.lastIndexOf(".");
93267
+ const receiverPath = path.slice(0, separator);
93268
+ const memberName = path.slice(separator + 1);
93269
+ const receiver = context.rootValueTargetsByPath.get(receiverPath);
93270
+ if (receiver === void 0) {
93271
+ throw new Error(
93272
+ `NeoAction listener ${index} receiver ${receiverPath} is not reachable through a deterministic root path.`
93273
+ );
93274
+ }
93275
+ target = classMemberByName(context, receiver.classId, memberName);
93276
+ valueId = receiver.valueId;
93159
93277
  }
93160
- const target = classMemberByName(context, ownerClassId, symbol);
93161
- if (target.kind === "action") return { memberId: target.id, valueId: null };
93278
+ if (target.kind === "action") return { memberId: target.id, valueId };
93162
93279
  if (target.kind !== "function" && target.kind !== "scriptFunction" && target.kind !== "delegate") {
93163
93280
  throw new Error(
93164
- `NeoAction listener ${symbol} does not resolve to a function, NSFunction, NSDelegate, or NeoAction member.`
93281
+ `NeoAction listener ${path} does not resolve to a function, NSFunction, NSDelegate, or NeoAction member.`
93165
93282
  );
93166
93283
  }
93167
93284
  if (target.returnType.kind !== "void") {
93168
93285
  throw new Error(
93169
- `NeoAction listener ${symbol} returns a value; a listener must be void.`
93286
+ `NeoAction listener ${path} returns a value; a listener must be void.`
93170
93287
  );
93171
93288
  }
93172
93289
  if (target.kind !== "delegate" && target.deferred) {
93173
93290
  throw new Error(
93174
- `NeoAction listener ${symbol} is deferred; listeners are immediate-only.`
93291
+ `NeoAction listener ${path} is deferred; listeners are immediate-only.`
93175
93292
  );
93176
93293
  }
93177
- return { memberId: target.id, valueId: null };
93294
+ return { memberId: target.id, valueId };
93178
93295
  }
93179
93296
  function lowerDelegateValue(context, expression, ownerClassId, authoredSlice) {
93180
93297
  if (expression.kind === "lambda") {
@@ -94723,11 +94840,6 @@ function actionListenerName(context, listener, index) {
94723
94840
  if (typeof listener.memberId !== "string") {
94724
94841
  throw new Error(`NSAction listener ${index} is missing memberId.`);
94725
94842
  }
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
94843
  const target = context.members.get(listener.memberId);
94732
94844
  if (target === void 0 || typeof target.name !== "string") {
94733
94845
  throw new Error(`NSAction listener ${listener.memberId} was not pulled.`);
@@ -94736,7 +94848,19 @@ function actionListenerName(context, listener, index) {
94736
94848
  if (kind !== 13 && kind !== 23 && kind !== 25 && kind !== 26) {
94737
94849
  throw new Error(`NSAction listener ${listener.memberId} is not callable.`);
94738
94850
  }
94739
- return target.name;
94851
+ if (listener.valueId === null || listener.valueId === void 0) {
94852
+ return target.name;
94853
+ }
94854
+ if (typeof listener.valueId !== "string") {
94855
+ throw new Error(`NSAction listener ${index} has a non-string valueId.`);
94856
+ }
94857
+ const receiver = context.rootPathsByValueId.get(listener.valueId);
94858
+ if (receiver === void 0) {
94859
+ throw new Error(
94860
+ `NSAction listener ${index} is bound to value ${listener.valueId}, which has no deterministic root path.`
94861
+ );
94862
+ }
94863
+ return `${receiver}.${target.name}`;
94740
94864
  }
94741
94865
  function fileValue(context, kind, label, body, required2) {
94742
94866
  if (body === null || body === void 0) {
@@ -94939,6 +95063,7 @@ var init_value_sources = __esm({
94939
95063
  init_declared_constructors2();
94940
95064
  init_init_source();
94941
95065
  init_source_format();
95066
+ init_root_value_paths();
94942
95067
  init_world_system_classes();
94943
95068
  init_member_value_id();
94944
95069
  init_members();
@@ -94981,7 +95106,7 @@ function emitProjectDocumentFilesV4(records2) {
94981
95106
  defaultInitializers: memberDefaults.initializers,
94982
95107
  fileSymbols: qualifiedProjectFileSymbolsV4(records2),
94983
95108
  relationEndpointExpressions: relationEndpointExpressions(records2),
94984
- collectionValuePaths: rootValuePathsByValueId(records2)
95109
+ collectionValuePaths: rootValuePathsByValueId(records2.values())
94985
95110
  });
94986
95111
  const source = {
94987
95112
  ...baseSource,
@@ -95110,6 +95235,7 @@ var init_project_documents = __esm({
95110
95235
  init_dialogue_sources();
95111
95236
  init_value_sources();
95112
95237
  init_root_source();
95238
+ init_root_value_paths();
95113
95239
  init_source_diagnostics();
95114
95240
  init_source_format();
95115
95241
  PROJECT_SOURCE_ANALYSIS_V4_CACHE_PATH = ".neo/project-source-analysis-v4.json";
@@ -101668,11 +101794,11 @@ function assertProjectFileAuthoringRecordIdentifiers(recordKind, data) {
101668
101794
  if (recordKind !== "unity-texture-template" && recordKind !== "unity-audio-clip-template") {
101669
101795
  return;
101670
101796
  }
101671
- if (!isObjectRecord4(data) || typeof data.name !== "string") return;
101797
+ if (!isObjectRecord5(data) || typeof data.name !== "string") return;
101672
101798
  const label = recordKind === "unity-texture-template" ? "Unity texture template name" : "Unity audio clip template name";
101673
101799
  assertProjectFileAuthoringIdentifier(data.name, label);
101674
101800
  }
101675
- function isObjectRecord4(value) {
101801
+ function isObjectRecord5(value) {
101676
101802
  return typeof value === "object" && value !== null && !Array.isArray(value);
101677
101803
  }
101678
101804
  var init_project_file_authoring_identifiers = __esm({
@@ -104614,7 +104740,7 @@ var init_registry2 = __esm({
104614
104740
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
104615
104741
  formatVersion: 3,
104616
104742
  contractVersion: "3.11",
104617
- cliVersion: "0.30.1",
104743
+ cliVersion: "0.30.2",
104618
104744
  projectFileUploadBatchSize: 32,
104619
104745
  documentRecords: {
104620
104746
  member: {
@@ -111182,7 +111308,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
111182
111308
  async function main() {
111183
111309
  const args = parseArgs(process.argv.slice(2));
111184
111310
  if (args.command === "--version") {
111185
- console.log("0.30.1");
111311
+ console.log("0.30.2");
111186
111312
  return;
111187
111313
  }
111188
111314
  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.2",
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.2 -->
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.2 -->
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 —