@neocompose/cli 0.35.0 → 0.36.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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.36.0] - 2026-08-19
4
+
5
+ ### Added
6
+
7
+ - Add collection `Count` property access, optional-predicate `Count(...)`, and
8
+ list `IndexOf(value)` across NeoScript compilation, IntelliSense, web/CLI
9
+ evaluation, Unity export validation, and the matching Unity runtime.
10
+
11
+ ### Changed
12
+
13
+ - **Breaking:** Unity export schema version is now 26 and the NeoScript
14
+ compiler revision is now 13. Exports containing `indexOf` or predicate
15
+ `count` IR require the matching Unity SDK.
16
+
17
+ ### Fixed
18
+
19
+ - Prevent VS Code completion from replacing an existing identifier when an
20
+ author inserts `this.` before it.
21
+ - Restrict action invocation and `NeoAction.Clear()` to the declaring class
22
+ and its derived classes. Public external callers may still subscribe and
23
+ unsubscribe with `+=` and `-=`.
24
+ - Keep declaration-pattern bindings for indexed expressions such as
25
+ `items[index] is Item item`, so the bound local remains available inside the
26
+ successful branch without incorrectly treating the indexed expression as a
27
+ stable dotted narrowing path.
28
+ - Diagnose `NeoAction<void>` with the canonical zero-argument spelling,
29
+ `NeoAction`, instead of later reporting the misleading invocation arity.
30
+
31
+ ## [0.35.1] - 2026-08-19
32
+
33
+ ### Fixed
34
+
35
+ - Treat `Partial<T>` values as sparse deltas throughout lowering and emission,
36
+ without invoking or reconstructing `T`'s required constructor.
37
+ - Materialize complete Unity defaults for template-less image and audio
38
+ registry entries instead of writing an invalid template override shape.
39
+ - Keep declarations inside variant delegate closures out of the global-child
40
+ registry, so separate `initialize` and `apply` closures may reuse local names.
41
+
3
42
  ## [0.35.0] - 2026-08-19
4
43
 
5
44
  ### Added
package/dist/neo.mjs CHANGED
@@ -2313,7 +2313,7 @@ function formatType(type, project) {
2313
2313
  )
2314
2314
  ].join(", ")}>${suffix}`;
2315
2315
  case "action":
2316
- return `NeoAction<${type.parameterTypes.map((parameter4) => formatType(parameter4, project)).join(", ")}>${suffix}`;
2316
+ return `${type.parameterTypes.length === 0 ? "NeoAction" : `NeoAction<${type.parameterTypes.map((parameter4) => formatType(parameter4, project)).join(", ")}>`}${suffix}`;
2317
2317
  }
2318
2318
  }
2319
2319
  var UNKNOWN_TYPE;
@@ -2416,7 +2416,8 @@ function complete(snapshot, position) {
2416
2416
  if (insidePlainString(snapshot.lexed.tokens, offset)) {
2417
2417
  return { isIncomplete: false, items: [] };
2418
2418
  }
2419
- const word = wordRangeAt(snapshot.source.text, offset);
2419
+ const wordAtCursor = wordRangeAt(snapshot.source.text, offset);
2420
+ const word = { start: wordAtCursor.start, end: offset };
2420
2421
  const prefix = snapshot.source.text.slice(word.start, offset);
2421
2422
  const tokens = significantTokensBefore(snapshot.lexed.tokens, word.start);
2422
2423
  const tail = tokens[tokens.length - 1];
@@ -4129,10 +4130,20 @@ function collectionSymbols(kind, type) {
4129
4130
  method(
4130
4131
  "Count",
4131
4132
  { kind: "primitive", name: "int" },
4132
- [],
4133
- "Return the entry count."
4133
+ [optionalCollectionPredicate(kind, entryType, keyType)],
4134
+ "Return the entry count, optionally restricted by a predicate. It may also be read as a property."
4134
4135
  )
4135
4136
  ];
4137
+ if (kind === "list") {
4138
+ symbols.push(
4139
+ method(
4140
+ "IndexOf",
4141
+ { kind: "primitive", name: "int" },
4142
+ [parameter2("value", entryType)],
4143
+ "Return the first matching index, or -1."
4144
+ )
4145
+ );
4146
+ }
4136
4147
  if (!canMutate) return symbols;
4137
4148
  if (kind === "dictionary") {
4138
4149
  symbols.push(
@@ -4192,6 +4203,20 @@ function collectionSymbols(kind, type) {
4192
4203
  function lambdaParameters(kind, entry, key) {
4193
4204
  return kind === "dictionary" ? [parameter2("key", key), parameter2("value", entry)] : [parameter2("item", entry)];
4194
4205
  }
4206
+ function optionalCollectionPredicate(kind, entry, key) {
4207
+ return {
4208
+ name: "predicate",
4209
+ type: {
4210
+ kind: "delegate",
4211
+ returnType: { kind: "primitive", name: "bool" },
4212
+ parameterTypes: lambdaParameters(kind, entry, key).map(
4213
+ (candidate) => candidate.type
4214
+ ),
4215
+ nullable: true
4216
+ },
4217
+ defaultValue: { displayText: "null", value: null }
4218
+ };
4219
+ }
4195
4220
  function method(name, returnType, parameters, documentation) {
4196
4221
  return {
4197
4222
  id: `builtin:collection:${name}`,
@@ -4311,7 +4336,13 @@ function inferLambdaParameterType(local, snapshot) {
4311
4336
  (token) => token.start === snapshot.source.offsetAt(local.nameRange.start)
4312
4337
  );
4313
4338
  if (declarationIndex < 0) return UNKNOWN_TYPE;
4314
- const lambdaMethods = /* @__PURE__ */ new Set(["Where", "First", "FirstOrDefault", "Select"]);
4339
+ const lambdaMethods = /* @__PURE__ */ new Set([
4340
+ "Where",
4341
+ "First",
4342
+ "FirstOrDefault",
4343
+ "Select",
4344
+ "Count"
4345
+ ]);
4315
4346
  for (let cursor = declarationIndex - 1; cursor >= 1; cursor--) {
4316
4347
  const open = tokens[cursor];
4317
4348
  const method2 = tokens[cursor - 1];
@@ -4354,7 +4385,10 @@ function symbolCompletion(symbol, word, snapshot) {
4354
4385
  range: snapshot.source.range(word.start, word.end),
4355
4386
  newText: insertText
4356
4387
  },
4357
- commitCharacters: callable2 ? ["("] : ["."],
4388
+ // `.` is a completion trigger, not a commit character. Treating it as
4389
+ // both lets VS Code accept a stale selection while an author prefixes an
4390
+ // existing identifier (for example, typing `this.` before a member).
4391
+ ...callable2 ? { commitCharacters: ["("] } : {},
4358
4392
  symbolId: symbol.id
4359
4393
  };
4360
4394
  }
@@ -4370,7 +4404,7 @@ function typeCompletion(type, word, snapshot) {
4370
4404
  range: snapshot.source.range(word.start, word.end),
4371
4405
  newText: type.name
4372
4406
  },
4373
- commitCharacters: [".", "<"],
4407
+ commitCharacters: ["<"],
4374
4408
  symbolId: type.id
4375
4409
  };
4376
4410
  }
@@ -4386,7 +4420,7 @@ function indexCompletion(index, word, snapshot) {
4386
4420
  range: snapshot.source.range(word.start, word.end),
4387
4421
  newText: index.name
4388
4422
  },
4389
- commitCharacters: [".", "["],
4423
+ commitCharacters: ["["],
4390
4424
  symbolId: index.id
4391
4425
  };
4392
4426
  }
@@ -8510,7 +8544,7 @@ var NEOSCRIPT_COMPILER_REVISION;
8510
8544
  var init_strict_ir = __esm({
8511
8545
  "../packages/neoscript-language/src/strict-ir.ts"() {
8512
8546
  "use strict";
8513
- NEOSCRIPT_COMPILER_REVISION = 12;
8547
+ NEOSCRIPT_COMPILER_REVISION = 13;
8514
8548
  }
8515
8549
  });
8516
8550
 
@@ -8535,8 +8569,10 @@ function pathRoot(path) {
8535
8569
  }
8536
8570
  function factsFromIsCheck(expression, whenTrue) {
8537
8571
  const path = canonicalPath(expression.operand);
8538
- if (!path) return [];
8539
8572
  const nullCheck = expression.type.kind === "primitive" && expression.type.name === "null";
8573
+ if (path === null && (!whenTrue || nullCheck || !expression.bindingName)) {
8574
+ return [];
8575
+ }
8540
8576
  if (whenTrue) {
8541
8577
  return nullCheck ? [{ expression: expression.operand, path, nullValue: true }] : [
8542
8578
  {
@@ -12156,10 +12192,10 @@ var init_strict_resolver = __esm({
12156
12192
  applyFactsToScope(outerScope, targetScope, facts, mode = "all") {
12157
12193
  for (const fact of facts) {
12158
12194
  if (mode === "bindings-only" && !fact.bindingName) continue;
12159
- const rootEntry = outerScope.lookup(pathRoot(fact.path));
12160
- if (!rootEntry) continue;
12161
12195
  const natural = this.resolveExpression(fact.expression, outerScope);
12196
+ const rootEntry = fact.path === null ? null : outerScope.lookup(pathRoot(fact.path));
12162
12197
  if (fact.nullValue) {
12198
+ if (fact.path === null || rootEntry === null) continue;
12163
12199
  if (fact.loose === true && natural.type.kind === "primitive" && (natural.type.name === "string" || natural.type.name === "int" || natural.type.name === "float" || natural.type.name === "bool")) {
12164
12200
  continue;
12165
12201
  }
@@ -12176,7 +12212,7 @@ var init_strict_resolver = __esm({
12176
12212
  const naturalDefinition = natural.type.kind === "named" ? this.project.typeById.get(natural.type.typeId) : void 0;
12177
12213
  const retainConcreteClass = !fact.bindingName && naturalDefinition?.kind === "class" && targetDefinition?.kind === "interface";
12178
12214
  const narrowed = retainConcreteClass ? { ...natural.type, nullable: false } : { ...target, nullable: false };
12179
- if (mode === "all") {
12215
+ if (mode === "all" && fact.path !== null && rootEntry !== null) {
12180
12216
  targetScope.narrow(fact.path, rootEntry, narrowed);
12181
12217
  }
12182
12218
  if (fact.bindingName) {
@@ -12193,6 +12229,7 @@ var init_strict_resolver = __esm({
12193
12229
  }
12194
12230
  continue;
12195
12231
  }
12232
+ if (fact.path === null || rootEntry === null) continue;
12196
12233
  if (!isNullable(natural.type)) continue;
12197
12234
  targetScope.narrow(fact.path, rootEntry, {
12198
12235
  ...natural.type,
@@ -12650,6 +12687,19 @@ var init_strict_resolver = __esm({
12650
12687
  indexSurface: receiver.type
12651
12688
  };
12652
12689
  }
12690
+ if (name === "Count" && collectionPartsFor(receiver) !== null) {
12691
+ if (nullPropagating) {
12692
+ throw new CompileError(
12693
+ "Null-propagating collection Count access is not supported; coalesce or narrow the collection first.",
12694
+ pos
12695
+ );
12696
+ }
12697
+ return intrinsic(
12698
+ "count" /* Count */,
12699
+ { collectionPointer: receiver.pointer },
12700
+ { kind: "primitive", name: "int" }
12701
+ );
12702
+ }
12653
12703
  const primitiveMember = resolvePrimitiveMember(receiver.type, name);
12654
12704
  if (primitiveMember) {
12655
12705
  if (isPrimitive(receiver.type, "DialogueRef")) {
@@ -14395,10 +14445,50 @@ var init_strict_resolver = __esm({
14395
14445
  const collection = collectionPartsFor(receiver);
14396
14446
  if (!collection) return null;
14397
14447
  if (name === "Count") {
14398
- requireArgCount(name, args, 0, pos);
14448
+ if (args.length > 1) {
14449
+ throw new CompileError(
14450
+ `Count expects zero or one predicate, got ${args.length}.`,
14451
+ pos
14452
+ );
14453
+ }
14454
+ const fn = args[0] ? this.resolveCollectionLambda(
14455
+ args[0],
14456
+ collection,
14457
+ { kind: "primitive", name: "bool" },
14458
+ scope,
14459
+ pos
14460
+ ) : void 0;
14399
14461
  return intrinsic(
14400
14462
  "count" /* Count */,
14401
- { collectionPointer: receiver.pointer },
14463
+ {
14464
+ collectionPointer: receiver.pointer,
14465
+ ...fn ? { function: fn } : {}
14466
+ },
14467
+ { kind: "primitive", name: "int" }
14468
+ );
14469
+ }
14470
+ if (name === "IndexOf" && collection.kind === "list") {
14471
+ requireArgCount(name, args, 1, pos);
14472
+ const expectedValueType = collectionOperationValueType(
14473
+ receiver,
14474
+ collection
14475
+ );
14476
+ const value = this.resolveExpression(
14477
+ requiredAt(args, 0),
14478
+ scope,
14479
+ expectedValueType
14480
+ );
14481
+ this.requireCollectionOperationAssignable(
14482
+ value.type,
14483
+ expectedValueType,
14484
+ receiver,
14485
+ collection,
14486
+ pos,
14487
+ "IndexOf argument"
14488
+ );
14489
+ return intrinsic(
14490
+ "indexOf" /* IndexOf */,
14491
+ { collectionPointer: receiver.pointer, valuePointer: value.pointer },
14402
14492
  { kind: "primitive", name: "int" }
14403
14493
  );
14404
14494
  }
@@ -14790,6 +14880,7 @@ var init_strict_resolver = __esm({
14790
14880
  * return and no optional form to carry.
14791
14881
  */
14792
14882
  resolveActionCall(action, actionType, expression, scope) {
14883
+ this.assertActionControlAccessible(action, "invoke", expression.pos);
14793
14884
  if (expression.callee.kind === "member" && expression.callee.optional) {
14794
14885
  throw new CompileError(
14795
14886
  `${this.describe(actionType)} is never null, so optional-chained invocation is not supported.`,
@@ -15047,6 +15138,7 @@ var init_strict_resolver = __esm({
15047
15138
  }
15048
15139
  const receiver = this.resolveExpression(expression.callee.receiver, scope);
15049
15140
  if (receiver.type.kind !== "action") return null;
15141
+ this.assertActionControlAccessible(receiver, "clear", pos);
15050
15142
  if (expression.callee.optional) {
15051
15143
  throw new CompileError(
15052
15144
  `${this.describe(receiver.type)} is never null, so optional-chained Clear is not supported.`,
@@ -15109,6 +15201,34 @@ var init_strict_resolver = __esm({
15109
15201
  }
15110
15202
  };
15111
15203
  }
15204
+ /**
15205
+ * An action's declared accessibility controls who may subscribe. Invoking
15206
+ * or clearing its listener set is a separate, protected-like capability:
15207
+ * the declaring class and its subclasses may raise/reset the action, while
15208
+ * public callers remain limited to `+=` and `-=`.
15209
+ */
15210
+ assertActionControlAccessible(action, operation, pos) {
15211
+ const symbol = action.symbol;
15212
+ const owner = symbol === void 0 ? void 0 : symbol.inheritedFrom ? this.project.typeById.get(symbol.inheritedFrom.typeId) : [...this.project.typeById.values()].find(
15213
+ (type) => type.members.some(
15214
+ (candidate) => candidate.id === symbol.id && candidate.inheritedFrom === void 0
15215
+ )
15216
+ );
15217
+ if (symbol !== void 0 && owner !== void 0 && memberAccessFailure({
15218
+ project: this.project,
15219
+ context: this.context,
15220
+ owner,
15221
+ symbol: { ...symbol, accessModifier: "protected" }
15222
+ }) === null) {
15223
+ return;
15224
+ }
15225
+ const verb = operation === "invoke" ? "invoked" : "cleared";
15226
+ const actionName = symbol === void 0 ? this.describe(action.type) : `'${owner?.name ?? symbol.inheritedFrom?.typeName ?? "Unknown"}.${symbol.name}'`;
15227
+ throw new CompileError(
15228
+ `Action ${actionName} can only be ${verb} from its declaring class or a derived class; external callers may only subscribe with '+=' or '-='.`,
15229
+ pos
15230
+ );
15231
+ }
15112
15232
  resolveConditionalExpression(expression, scope, expected) {
15113
15233
  const condition = this.toBool(
15114
15234
  this.resolveExpression(expression.condition, scope),
@@ -18907,6 +19027,14 @@ function collectGlobalChildEntries(sourceText, declaration) {
18907
19027
  let pendingAnnotations = [];
18908
19028
  for (let index = 0; index < tokens.length; index++) {
18909
19029
  const token = tokens[index];
19030
+ if (token.text === "(" || token.text === "[") {
19031
+ groupDepth++;
19032
+ continue;
19033
+ }
19034
+ if (token.text === ")" || token.text === "]") {
19035
+ groupDepth--;
19036
+ continue;
19037
+ }
18910
19038
  if (token.text === "{") {
18911
19039
  braceDepth++;
18912
19040
  continue;
@@ -18916,14 +19044,6 @@ function collectGlobalChildEntries(sourceText, declaration) {
18916
19044
  continue;
18917
19045
  }
18918
19046
  if (braceDepth !== 1) continue;
18919
- if (token.text === "(" || token.text === "[") {
18920
- groupDepth++;
18921
- continue;
18922
- }
18923
- if (token.text === ")" || token.text === "]") {
18924
- groupDepth--;
18925
- continue;
18926
- }
18927
19047
  if (groupDepth !== 0) continue;
18928
19048
  if (token.text === ";") {
18929
19049
  pendingAnnotations = [];
@@ -26556,6 +26676,15 @@ function validateTypeApplication(uri, type, genericParameters, environment, diag
26556
26676
  return;
26557
26677
  }
26558
26678
  if (type.name === "NeoAction") {
26679
+ if (type.typeArguments.some((argument2) => argument2.name === "void")) {
26680
+ diagnose(
26681
+ diagnostics,
26682
+ uri,
26683
+ type.range,
26684
+ "invalid-member-type",
26685
+ "Type 'NeoAction' cannot use 'void' as a parameter type; spell a zero-argument action as bare 'NeoAction'."
26686
+ );
26687
+ }
26559
26688
  if (type.typeArguments.length > 16) {
26560
26689
  diagnose(
26561
26690
  diagnostics,
@@ -29087,10 +29216,8 @@ function maskOutsideProjectBody(text, start, end) {
29087
29216
  }
29088
29217
  function projectWordRange(text, offset) {
29089
29218
  let start = offset;
29090
- let end = offset;
29091
29219
  while (start > 0 && /[A-Za-z0-9_]/.test(text[start - 1] ?? "")) start--;
29092
- while (end < text.length && /[A-Za-z0-9_]/.test(text[end] ?? "")) end++;
29093
- return { start, end };
29220
+ return { start, end: offset };
29094
29221
  }
29095
29222
  function isContextualDot(text, wordStart) {
29096
29223
  if (text[wordStart - 1] !== ".") return false;
@@ -42957,6 +43084,13 @@ function isNSFunctionContains(value) {
42957
43084
  if (typeof info !== "object" || info === null) return false;
42958
43085
  return isNSPointer(info.collectionPointer) && isNSPointer(info.valuePointer);
42959
43086
  }
43087
+ function isNSFunctionIndexOf(value) {
43088
+ const v = value;
43089
+ if (v?.type !== "indexOf" /* indexOf */) return false;
43090
+ const info = v.info;
43091
+ if (typeof info !== "object" || info === null) return false;
43092
+ return isNSPointer(info.collectionPointer) && isNSPointer(info.valuePointer);
43093
+ }
42960
43094
  function isNSFunctionStringOp(value) {
42961
43095
  const v = value;
42962
43096
  if (v?.type !== "stringOp" /* stringOp */) return false;
@@ -43010,12 +43144,7 @@ function isNSFunctionListRepeat(value) {
43010
43144
  }
43011
43145
  function isNSFunctionCount(value) {
43012
43146
  const v = value;
43013
- if (v?.type !== "count" /* count */) return false;
43014
- const info = v.info;
43015
- if (typeof info !== "object" || info === null) return false;
43016
- return isNSPointer(
43017
- info.collectionPointer
43018
- );
43147
+ return v?.type === "count" /* count */ && isNSCollectionOptionalBoolFunction(v.info);
43019
43148
  }
43020
43149
  function isNSDialogueMemoryFunctionInfo(value) {
43021
43150
  if (typeof value !== "object" || value === null) return false;
@@ -43052,7 +43181,7 @@ function isNSFunctionImageSlice(value) {
43052
43181
  return isNSPointer(info.sliceIndexPointer);
43053
43182
  }
43054
43183
  function isNSFunction(value) {
43055
- return isNSFunctionClassConstructor(value) || isNSFunctionClassClone(value) || isNSFunctionListIndex(value) || isNSFunctionSelect(value) || isNSFunctionFirst(value) || isNSFunctionFirstOrDefault(value) || isNSFunctionWhere(value) || isNSFunctionContains(value) || // stringOp was missing from this chain since its introduction — a
43184
+ return isNSFunctionClassConstructor(value) || isNSFunctionClassClone(value) || isNSFunctionListIndex(value) || isNSFunctionSelect(value) || isNSFunctionFirst(value) || isNSFunctionFirstOrDefault(value) || isNSFunctionWhere(value) || isNSFunctionContains(value) || isNSFunctionIndexOf(value) || // stringOp was missing from this chain since its introduction — a
43056
43185
  // compiled getter using ToLower()/StartsWith() would fail pointer
43057
43186
  // validation anywhere isNSFunction gates. Fixed alongside decimalOp.
43058
43187
  isNSFunctionStringOp(value) || isNSFunctionDecimalOp(value) || isNSFunctionMathOp(value) || isNSFunctionListRepeat(value) || isNSFunctionCount(value) || isNSFunctionVisitCount(value) || isNSFunctionHasVisited(value) || isNSFunctionVectorConstructor(value) || isNSFunctionImageSlice(value) || isNSFunctionVariantInitialize(value) || isNSFunctionVariantApply(value) || isNSFunctionDeclaredConstructor(value);
@@ -43277,7 +43406,12 @@ function minimumNeoScriptCompilerRevisionForIR(node, visited = /* @__PURE__ */ n
43277
43406
  visited.add(node);
43278
43407
  const discriminatorRevision = "type" in node && typeof node.type === "string" ? MINIMUM_REVISION_BY_IR_DISCRIMINATOR.get(node.type) ?? 1 : 1;
43279
43408
  const fallbackRevision = "missingMemberFallback" in node && node.missingMemberFallback === "valueEquality" ? 12 : 1;
43280
- let minimum = Math.max(discriminatorRevision, fallbackRevision);
43409
+ const countPredicateRevision = "type" in node && node.type === "count" /* count */ && "info" in node && typeof node.info === "object" && node.info !== null && "function" in node.info && node.info.function !== void 0 && node.info.function !== null ? 13 : 1;
43410
+ let minimum = Math.max(
43411
+ discriminatorRevision,
43412
+ fallbackRevision,
43413
+ countPredicateRevision
43414
+ );
43281
43415
  const isLiteralValue = "typeInfo" in node && "value" in node && !("type" in node);
43282
43416
  for (const [key, child] of Object.entries(node)) {
43283
43417
  if (isLiteralValue && key === "value") continue;
@@ -43379,7 +43513,8 @@ var init_neoscript_guards = __esm({
43379
43513
  ["removeActionListener" /* removeActionListener */, 8],
43380
43514
  ["callAction" /* callAction */, 8],
43381
43515
  ["conditional" /* conditional */, 12],
43382
- ["delegateClosure" /* delegateClosure */, 12]
43516
+ ["delegateClosure" /* delegateClosure */, 12],
43517
+ ["indexOf" /* indexOf */, 13]
43383
43518
  ]);
43384
43519
  }
43385
43520
  });
@@ -48137,6 +48272,20 @@ function buildDefaultUnityTexture2DImportSettings() {
48137
48272
  }
48138
48273
  };
48139
48274
  }
48275
+ function buildDefaultUnityAudioClipImportSettings() {
48276
+ return {
48277
+ forceToMono: false,
48278
+ normalize: true,
48279
+ loadInBackground: false,
48280
+ ambisonic: false,
48281
+ loadType: "decompress-on-load" /* DecompressOnLoad */,
48282
+ compressionFormat: "vorbis" /* Vorbis */,
48283
+ quality: 1,
48284
+ sampleRateSetting: "preserve-sample-rate" /* PreserveSampleRate */,
48285
+ overrideSampleRate: null,
48286
+ preloadAudioData: true
48287
+ };
48288
+ }
48140
48289
  function isUnityVector2(value) {
48141
48290
  const v = value;
48142
48291
  if (!isObject(value)) return false;
@@ -53024,6 +53173,20 @@ var init_project_file_registry = __esm({
53024
53173
  });
53025
53174
 
53026
53175
  // src/project-source/project-file-source.ts
53176
+ function projectFileUnityImportSettingsV4(kind, templateId) {
53177
+ if (templateId === null) {
53178
+ return {
53179
+ ...kind === "image" ? buildDefaultUnityTexture2DImportSettings() : buildDefaultUnityAudioClipImportSettings(),
53180
+ templateId: null
53181
+ };
53182
+ }
53183
+ return {
53184
+ ...kind === "image" ? { type: "texture-2d" } : {},
53185
+ templateId,
53186
+ overridePaths: [],
53187
+ values: {}
53188
+ };
53189
+ }
53027
53190
  function emitProjectFileRegistrySourcesV4(records2) {
53028
53191
  const liveFiles = [...records2.values()].filter((record3) => record3.recordKind === "project-file" && !record3.deleted).map((record3) => ({ record: record3, data: requireFileData(record3) }));
53029
53192
  assertUniqueProjectBinaryPaths(liveFiles);
@@ -53142,12 +53305,10 @@ function lowerTrustedProjectFileRegistrySourcesV4(state, analysis, trustedPendin
53142
53305
  });
53143
53306
  }
53144
53307
  if (importSettingsChanged) {
53145
- next[settingsField] = {
53146
- ...declaration.kind === "image" ? { type: "texture-2d" } : {},
53147
- templateId: declaration.templateId,
53148
- overridePaths: [],
53149
- values: {}
53150
- };
53308
+ next[settingsField] = projectFileUnityImportSettingsV4(
53309
+ declaration.kind,
53310
+ declaration.templateId
53311
+ );
53151
53312
  }
53152
53313
  return {
53153
53314
  recordKind: "project-file",
@@ -53445,6 +53606,7 @@ var init_project_file_source = __esm({
53445
53606
  init_source_format();
53446
53607
  init_project_file_registry();
53447
53608
  init_members();
53609
+ init_unity_import_settings();
53448
53610
  REGISTRY_ENTRY = /(?:@id\(\s*("(?:[^"\\]|\\.)*")\s*\)\s*)?(?:@settings\(([\s\S]*?)\)\s*)?(NeoImage|NeoAudioClip)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*new\(\s*("(?:[^"\\]|\\.)*")\s*\)\s*;/gy;
53449
53611
  assignDeterministicFileSymbols = assignDeterministicProjectFileSymbols;
53450
53612
  }
@@ -70393,7 +70555,23 @@ function evalFunction(fn, scope, ctx) {
70393
70555
  }
70394
70556
  case "count" /* count */: {
70395
70557
  const c = evalPointer(fn.info.collectionPointer, scope, ctx);
70396
- return collectionLength(c);
70558
+ const innerFn = fn.info.function ?? null;
70559
+ if (innerFn === null) return collectionLength(c);
70560
+ const isList = isListCollection(c);
70561
+ let count = 0;
70562
+ const callback = prepareCollectionCallback(
70563
+ innerFn,
70564
+ scope,
70565
+ ctx,
70566
+ isList,
70567
+ "predicate",
70568
+ () => {
70569
+ count += 1;
70570
+ return 0 /* Continue */;
70571
+ }
70572
+ );
70573
+ iterateCollection(c, ctx, callback);
70574
+ return count;
70397
70575
  }
70398
70576
  case "contains" /* contains */: {
70399
70577
  const c = evalPointer(fn.info.collectionPointer, scope, ctx);
@@ -70417,6 +70595,23 @@ function evalFunction(fn, scope, ctx) {
70417
70595
  });
70418
70596
  return containsResolvedEntry;
70419
70597
  }
70598
+ case "indexOf" /* indexOf */: {
70599
+ const collection = listArg(
70600
+ evalPointer(fn.info.collectionPointer, scope, ctx),
70601
+ "IndexOf receiver"
70602
+ );
70603
+ const target = evalPointer(fn.info.valuePointer, scope, ctx);
70604
+ const targetReferenceId = typeof target === "string" ? target : findKnownRowIdByValueReference(target, ctx);
70605
+ let found = -1;
70606
+ iterateCollection(collection, ctx, (entry, key, valueId) => {
70607
+ if (valueId !== null && valueId === targetReferenceId || jsEqual(entry, target)) {
70608
+ found = numberArg(key, "IndexOf index");
70609
+ return 1 /* Break */;
70610
+ }
70611
+ return 0 /* Continue */;
70612
+ });
70613
+ return found;
70614
+ }
70420
70615
  case "decimalOp" /* decimalOp */: {
70421
70616
  const info = fn.info;
70422
70617
  const receiverRaw = evalPointer(info.receiverPointer, scope, ctx);
@@ -98181,11 +98376,11 @@ function lowerMemberDefaultSourcesV4(state, analysis, manifest, options = {}) {
98181
98376
  context.parsedInitializers,
98182
98377
  declaration.initializer
98183
98378
  );
98184
- const requiresEvaluation = initializerRequiresEvaluation(
98185
- context.declaredConstructors,
98379
+ const requiresEvaluation = memberInitializerRequiresEvaluation(
98380
+ context,
98381
+ member,
98186
98382
  annotatedValue(expression).expression,
98187
- memberClassName(context, member),
98188
- bindingRuntimeIdentifiers(context, binding)
98383
+ binding
98189
98384
  );
98190
98385
  const baseData3 = stateData(context, "member", memberId);
98191
98386
  const annotatedExpression = annotatedValue(expression).expression;
@@ -98676,12 +98871,7 @@ function declaredCollectionMemberIds(context, collectionMemberId) {
98676
98871
  }
98677
98872
  function defaultRequiresOwnedRows(context, member, sourceExpression, source) {
98678
98873
  const expression = annotatedValue(sourceExpression).expression;
98679
- if (initializerRequiresEvaluation(
98680
- context.declaredConstructors,
98681
- expression,
98682
- memberClassName(context, member),
98683
- bindingRuntimeIdentifiers(context, source)
98684
- )) {
98874
+ if (memberInitializerRequiresEvaluation(context, member, expression, source)) {
98685
98875
  return false;
98686
98876
  }
98687
98877
  if (member.kind === "class") return expression.kind === "new";
@@ -98693,6 +98883,15 @@ function defaultRequiresOwnedRows(context, member, sourceExpression, source) {
98693
98883
  }
98694
98884
  return false;
98695
98885
  }
98886
+ function memberInitializerRequiresEvaluation(context, member, expression, source) {
98887
+ if (member.kind === "class" && member.partial === true) return false;
98888
+ return initializerRequiresEvaluation(
98889
+ context.declaredConstructors,
98890
+ expression,
98891
+ memberClassName(context, member),
98892
+ bindingRuntimeIdentifiers(context, source)
98893
+ );
98894
+ }
98696
98895
  function indexInitAuthoredRowIds(context, initSource, owner) {
98697
98896
  indexAuthoredRowIds(context.initAuthoredRowIds, initSource, owner);
98698
98897
  }
@@ -99240,7 +99439,7 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99240
99439
  member,
99241
99440
  inheritedEnvironment
99242
99441
  );
99243
- if (context.structuralConstruction && resolvedMember.kind === "class" && expression.kind === "new" && structurallyConstructedClassId(context, resolvedMember, expression) !== null) {
99442
+ if (context.structuralConstruction && resolvedMember.kind === "class" && resolvedMember.partial !== true && expression.kind === "new" && structurallyConstructedClassId(context, resolvedMember, expression) !== null) {
99244
99443
  return lowerStructuralConstructionRow(
99245
99444
  context,
99246
99445
  resolvedMember,
@@ -99255,11 +99454,11 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99255
99454
  authoredSlice
99256
99455
  );
99257
99456
  }
99258
- if (!isStoredCallableLiteral(resolvedMember.kind, expression) && initializerRequiresEvaluation(
99259
- context.declaredConstructors,
99457
+ if (!isStoredCallableLiteral(resolvedMember.kind, expression) && memberInitializerRequiresEvaluation(
99458
+ context,
99459
+ resolvedMember,
99260
99460
  expression,
99261
- memberClassName(context, resolvedMember),
99262
- bindingRuntimeIdentifiers(context, source)
99461
+ source
99263
99462
  )) {
99264
99463
  const unattachedId = annotated.id === null ? unattachedAuthoredRowId(authoredSlice) : null;
99265
99464
  if (unattachedId !== null) {
@@ -99321,7 +99520,8 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99321
99520
  if (effectiveClass === void 0) {
99322
99521
  throw new Error(`Unknown value class ${resolvedMember.classId}.`);
99323
99522
  }
99324
- if (effectiveClass.declarationModifier === "abstract") {
99523
+ const partial = resolvedMember.partial === true;
99524
+ if (!partial && effectiveClass.declarationModifier === "abstract") {
99325
99525
  throw new Error(
99326
99526
  `Cannot instantiate abstract class ${effectiveClass.name} for ${path}. Use a concrete descendant instead.`
99327
99527
  );
@@ -99339,13 +99539,15 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99339
99539
  expression,
99340
99540
  declaredEnvironment
99341
99541
  );
99342
- validateConstructedGenericArguments(
99343
- context,
99344
- effectiveClass,
99345
- expression,
99346
- environment,
99347
- path
99348
- );
99542
+ if (!partial) {
99543
+ validateConstructedGenericArguments(
99544
+ context,
99545
+ effectiveClass,
99546
+ expression,
99547
+ environment,
99548
+ path
99549
+ );
99550
+ }
99349
99551
  genericBindings = animationChildOverrideSeedBindings(
99350
99552
  context,
99351
99553
  effectiveClass,
@@ -99355,19 +99557,20 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99355
99557
  );
99356
99558
  const body = {};
99357
99559
  const argumentSlices = constructorArgumentValueSlices(authoredSlice);
99358
- for (const {
99359
- projectedMember,
99360
- schemaKey,
99361
- argument: argument2,
99362
- argumentIndex
99363
- } of resolveConstructorProjectionArguments(
99560
+ const projectedArguments = partial ? [] : resolveConstructorProjectionArguments(
99364
99561
  context,
99365
99562
  effectiveClass,
99366
99563
  expression,
99367
99564
  valueId,
99368
99565
  environment,
99369
99566
  source
99370
- )) {
99567
+ );
99568
+ for (const {
99569
+ projectedMember,
99570
+ schemaKey,
99571
+ argument: argument2,
99572
+ argumentIndex
99573
+ } of projectedArguments) {
99371
99574
  const childPath = `${path}.${schemaKey}`;
99372
99575
  const childValueId = pendingNestedValueId(source, childPath);
99373
99576
  if (rows.has(childValueId)) {
@@ -99413,7 +99616,7 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99413
99616
  effectiveClass.id,
99414
99617
  assignment.name
99415
99618
  );
99416
- if (inheritedLegacyConstructorProjections(
99619
+ if (!partial && inheritedLegacyConstructorProjections(
99417
99620
  context.classes,
99418
99621
  effectiveClass.id
99419
99622
  ).some((projection) => projection.memberId === childMember.id)) {
@@ -99853,11 +100056,11 @@ function lowerValueRow(context, member, sourceExpression, expectedValueId, sourc
99853
100056
  member,
99854
100057
  environment
99855
100058
  );
99856
- if (!isStoredCallableLiteral(resolvedMember.kind, expression) && initializerRequiresEvaluation(
99857
- context.declaredConstructors,
100059
+ if (!isStoredCallableLiteral(resolvedMember.kind, expression) && memberInitializerRequiresEvaluation(
100060
+ context,
100061
+ resolvedMember,
99858
100062
  expression,
99859
- memberClassName(context, resolvedMember),
99860
- bindingRuntimeIdentifiers(context, source)
100063
+ source
99861
100064
  )) {
99862
100065
  const code = initializerExpressionSlice(authoredSlice);
99863
100066
  if (code === void 0) {
@@ -100075,7 +100278,8 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
100075
100278
  const schemaClass2 = context.classes.get(classId);
100076
100279
  if (schemaClass2 === void 0)
100077
100280
  throw new Error(`Unknown value class ${classId}.`);
100078
- if (schemaClass2.declarationModifier === "abstract") {
100281
+ const partial = member.partial === true;
100282
+ if (!partial && schemaClass2.declarationModifier === "abstract") {
100079
100283
  throw new Error(
100080
100284
  `Cannot instantiate abstract class ${schemaClass2.name} for value ${String(base.id)}. Use a concrete descendant instead.`
100081
100285
  );
@@ -100090,13 +100294,15 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
100090
100294
  expression,
100091
100295
  declaredEnvironment
100092
100296
  );
100093
- validateConstructedGenericArguments(
100094
- context,
100095
- schemaClass2,
100096
- expression,
100097
- environment,
100098
- String(base.id ?? member.name)
100099
- );
100297
+ if (!partial) {
100298
+ validateConstructedGenericArguments(
100299
+ context,
100300
+ schemaClass2,
100301
+ expression,
100302
+ environment,
100303
+ String(base.id ?? member.name)
100304
+ );
100305
+ }
100100
100306
  const baseBody = isObjectRecord2(base.value) ? base.value : {};
100101
100307
  const body = retainedStoredClassBody(
100102
100308
  context,
@@ -100104,7 +100310,7 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
100104
100310
  baseBody
100105
100311
  );
100106
100312
  const assignmentSlices = objectInitializerSlices(authoredSlice);
100107
- if (materializedConstruction !== "preserve") {
100313
+ if (!partial && materializedConstruction !== "preserve") {
100108
100314
  lowerConstructorProjections(
100109
100315
  context,
100110
100316
  schemaClass2,
@@ -100119,7 +100325,7 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
100119
100325
  }
100120
100326
  for (const assignment of expression.initializer ?? []) {
100121
100327
  const childMember = classMemberByName(context, classId, assignment.name);
100122
- if (inheritedLegacyConstructorProjections(
100328
+ if (!partial && inheritedLegacyConstructorProjections(
100123
100329
  context.classes,
100124
100330
  schemaClass2.id
100125
100331
  ).some((projection) => projection.memberId === childMember.id)) {
@@ -102173,7 +102379,7 @@ function classValue(context, member, value, visited, targetTyped, outerEnvironme
102173
102379
  const hasStoredConstruction = isObjectRecord2(value.constructorArgs);
102174
102380
  let projection;
102175
102381
  try {
102176
- projection = hasStoredConstruction ? {
102382
+ projection = partial ? { arguments: [], memberIds: /* @__PURE__ */ new Set(), targetValueIds: [] } : hasStoredConstruction ? {
102177
102383
  arguments: [],
102178
102384
  memberIds: new Set(
102179
102385
  effectiveConstructorProjections(
@@ -102210,7 +102416,7 @@ function classValue(context, member, value, visited, targetTyped, outerEnvironme
102210
102416
  const name = classValueTypeName(context, classId, member, environment);
102211
102417
  let storedConstruction;
102212
102418
  try {
102213
- storedConstruction = storedConstructorCallSource(
102419
+ storedConstruction = partial ? null : storedConstructorCallSource(
102214
102420
  context,
102215
102421
  schemaClass2,
102216
102422
  value,
@@ -104166,12 +104372,10 @@ function lowerProjectFileRegistrySourcesV4(root, state, analysis, options = {})
104166
104372
  });
104167
104373
  }
104168
104374
  if (importSettingsChanged) {
104169
- next[settingsField] = {
104170
- ...declaration.kind === "image" ? { type: "texture-2d" } : {},
104171
- templateId: declaration.templateId,
104172
- overridePaths: [],
104173
- values: {}
104174
- };
104375
+ next[settingsField] = projectFileUnityImportSettingsV4(
104376
+ declaration.kind,
104377
+ declaration.templateId
104378
+ );
104175
104379
  }
104176
104380
  return {
104177
104381
  recordKind: "project-file",
@@ -104203,12 +104407,7 @@ function lowerDiscoveredProjectBinariesV4(root, state, analysis) {
104203
104407
  const templateId = binary.kind === "image" ? optionalString3(projectData.defaultTextureTemplateId) : optionalString3(projectData.defaultAudioClipTemplateId);
104204
104408
  const settingsField = binary.kind === "image" ? "unityTextureSettings" : "unityAudioClipSettings";
104205
104409
  const otherSettingsField = binary.kind === "image" ? "unityAudioClipSettings" : "unityTextureSettings";
104206
- const settings = {
104207
- ...binary.kind === "image" ? { type: "texture-2d" } : {},
104208
- templateId,
104209
- overridePaths: [],
104210
- values: {}
104211
- };
104410
+ const settings = projectFileUnityImportSettingsV4(binary.kind, templateId);
104212
104411
  const now = Date.now();
104213
104412
  return {
104214
104413
  recordKind: "project-file",
@@ -104581,6 +104780,7 @@ var init_project_files = __esm({
104581
104780
  init_source_format();
104582
104781
  init_members();
104583
104782
  init_project_file_registry();
104783
+ init_project_file_source();
104584
104784
  SUPPORTED_BINARY_TYPES = /* @__PURE__ */ new Map([
104585
104785
  [".png", { kind: "image", mimeType: "image/png" }],
104586
104786
  [".jpg", { kind: "image", mimeType: "image/jpeg" }],
@@ -113595,7 +113795,7 @@ var init_registry2 = __esm({
113595
113795
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
113596
113796
  formatVersion: 3,
113597
113797
  contractVersion: "3.14",
113598
- cliVersion: "0.35.0",
113798
+ cliVersion: "0.36.0",
113599
113799
  projectFileUploadBatchSize: 32,
113600
113800
  documentRecords: {
113601
113801
  member: {
@@ -120197,7 +120397,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
120197
120397
  async function main() {
120198
120398
  const args = parseArgs(process.argv.slice(2));
120199
120399
  if (args.command === "--version") {
120200
- console.log("0.35.0");
120400
+ console.log("0.36.0");
120201
120401
  return;
120202
120402
  }
120203
120403
  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.35.0",
3
+ "version": "0.36.0",
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.35.0 -->
12
+ <!-- reviewed-through-cli: 0.36.0 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -197,6 +197,8 @@ AudioClipRegistry AudioClips = new() {
197
197
  Use `Images.Sword.Slice(0)` for a sprite and `AudioClips.SwordHit` for audio.
198
198
  Renaming the registry symbol, moving the local presentation, or replacing bytes
199
199
  retains the file ID.
200
+ Omitting `@settings(template: ...)` uses the complete built-in Unity import
201
+ defaults for that file kind.
200
202
 
201
203
  ## Resolve conflicts by identity
202
204
 
@@ -50,7 +50,12 @@ NeoVariant<ExampleObject> Down = new(
50
50
  `() => new ExampleObject(.Down)` works; `apply` is void, so its expression
51
51
  body must be a call or an assignment, as in
52
52
  `(source) => source.FacingDir = .Down`. The server compiles them, so authored
53
- source carries only the code.
53
+ source carries only the code. Each closure has its own lexical scope, so
54
+ local names may be reused between `initialize` and `apply`.
55
+ - `overrides` and each child override's `overrides` value are `Partial<T>`
56
+ deltas over existing instances. They may use inferred `new { ... }` or
57
+ explicit `new Partial<T> { ... }`, and never pass arguments to or invoke
58
+ `T`'s constructor—even when `T` has a required constructor.
54
59
 
55
60
  Folders are records too, declared as their own globals and assigned per
56
61
  variant. Nesting is spelled in the path with `/`; intermediate segments are
@@ -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.35.0 -->
86
+ <!-- reviewed-through-cli: 0.36.0 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -80,9 +80,14 @@ Prefer `--json` for automation.
80
80
 
81
81
  Inside an inline Class body, unqualified names resolve against that Class;
82
82
  invoke delegate and action members directly with `Selector()` or `OnChanged()`.
83
+ Spell a zero-argument action as bare `NeoAction`; type arguments name action
84
+ parameters, so `NeoAction<void>` is invalid rather than a zero-argument alias.
83
85
  Clear every subscribed listener from a stored action with
84
86
  `this.OnChanged.Clear()`. `Clear()` is a mutating statement and follows the
85
- action member's effective storage and writability.
87
+ action member's effective storage and writability. Only the action's declaring
88
+ class and derived classes may invoke it or call `Clear()`; public external
89
+ callers may subscribe and unsubscribe with `+=` and `-=` but cannot raise or
90
+ reset the action.
86
91
 
87
92
  Inline `NeoDelegate` lambdas may be returned or stored and may read surrounding
88
93
  locals and parameters. Those values are captured when the lambda is created,
@@ -172,7 +177,17 @@ enum, parameter, or local may shadow it. Qualified members use their owner's
172
177
  namespace and may be named `Math`; access one as `this.Math` or
173
178
  `SomeValue.Math` without colliding with the builtin qualifier.
174
179
 
175
- ## List builtins
180
+ ## Collection builtins
181
+
182
+ Lists expose their size as either `items.Count` or `items.Count()`. Use
183
+ `items.Count((item) => predicate)` to count only matching entries without
184
+ materializing a filtered list. The same Count forms work on Dictionary and Set
185
+ collections; dictionary predicates may accept `(key, value)` just like the
186
+ other collection callbacks.
187
+
188
+ Use `items.IndexOf(value)` to return the zero-based index of the first equal
189
+ list entry, or `-1` when the value is absent. `IndexOf` is list-only and uses
190
+ the same value/reference equality as `Contains`.
176
191
 
177
192
  Use `List.Repeat(value, count)` to build a list of `count` copies of a value
178
193
  instead of hand-counting a literal or looping around `Add`:
@@ -170,7 +170,8 @@ diff, and dry-run do not rewrite its registry. A successful push creates the
170
170
  record, uploads verified bytes, and materializes its declaration and ID.
171
171
 
172
172
  Edit the typed registry declaration directly when an explicit symbol or
173
- non-default template is needed before push.
173
+ non-default template is needed before push. With no template annotation, the
174
+ CLI persists the complete built-in Unity defaults for the file kind.
174
175
 
175
176
  Pull/push compare server-verified SHA-256, not storage ETags. A divergent
176
177
  binary keeps local bytes and writes the verified remote side under