@neocompose/cli 0.31.7 → 0.31.9

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.31.9] - 2026-08-17
4
+
5
+ ### Added
6
+
7
+ - Surface `SpriteInfo.Empty` in project-source and NeoScript completion and
8
+ hover with documentation for its canonical empty-sprite behavior.
9
+
10
+ ### Fixed
11
+
12
+ - Accept the exact `SpriteInfo.Empty` sentinel as the non-null result of a
13
+ required Sprite function while continuing to reject malformed empty sprites
14
+ and empty Audio values.
15
+
16
+ ## [0.31.8] - 2026-08-17
17
+
18
+ ### Fixed
19
+
20
+ - Propagate `?.` through following required member and index accesses in the
21
+ same expression chain, so a nullable parent safely short-circuits a deep
22
+ read while independently nullable descendants still require explicit null
23
+ handling.
24
+
3
25
  ## [0.31.7] - 2026-08-15
4
26
 
5
27
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -1663,13 +1663,14 @@ var init_language_spec = __esm({
1663
1663
  id: "builtin:SpriteInfo",
1664
1664
  name: "SpriteInfo",
1665
1665
  kind: "builtin",
1666
+ documentation: "A sprite asset reference and slice index. Use SpriteInfo.Empty when a required sprite should render no image.",
1666
1667
  members: [
1667
1668
  {
1668
1669
  ...property(
1669
1670
  "SpriteInfo",
1670
1671
  "Empty",
1671
1672
  SPRITE_INFO_TYPE,
1672
- "An authored empty sprite at slice zero."
1673
+ "The canonical non-null empty sprite. Use it when a required SpriteInfo should render no image; it has no backing file and uses slice index 0."
1673
1674
  ),
1674
1675
  static: true
1675
1676
  },
@@ -2319,7 +2320,11 @@ function complete(snapshot, position) {
2319
2320
  word
2320
2321
  );
2321
2322
  } else {
2322
- const resolved = resolveChain(snapshot, receiverTokens, offset);
2323
+ const directStaticType = receiverTokens.length === 1 && receiverTokens[0]?.kind === "type" ? snapshot.project.typeByName.get(receiverTokens[0].text) : void 0;
2324
+ const resolved = directStaticType ? {
2325
+ type: { kind: "named", typeId: directStaticType.id },
2326
+ staticType: directStaticType
2327
+ } : resolveChain(snapshot, receiverTokens, offset);
2323
2328
  candidates = completionItemsForResolution(snapshot, resolved, word);
2324
2329
  }
2325
2330
  } else if (tail?.kind === "identifier" && tail.text === NEOSCRIPT_CONSTRUCTOR_KEYWORD) {
@@ -2343,27 +2348,14 @@ function complete(snapshot, position) {
2343
2348
  function hover(snapshot, position) {
2344
2349
  const offset = snapshot.source.offsetAt(position);
2345
2350
  const reference2 = referenceAt(snapshot.parsed.references, offset);
2346
- if (!reference2) return null;
2351
+ if (!reference2) return builtinStaticMemberHoverAt(snapshot, offset);
2347
2352
  const resolution = resolveReference(snapshot, reference2);
2348
- if (!resolution?.symbol && !resolution?.staticType) return null;
2353
+ if (!resolution?.symbol && !resolution?.staticType) {
2354
+ return builtinStaticMemberHoverAt(snapshot, offset);
2355
+ }
2349
2356
  const symbol = resolution.symbol;
2350
2357
  if (symbol) {
2351
- const signature = formatSymbolSignature(symbol, snapshot.project);
2352
- const origin = symbol.inheritedFrom ? `
2353
-
2354
- Inherited from \`${symbol.inheritedFrom.typeName}\`.` : "";
2355
- const identity2 = symbol.id.startsWith("local:") || symbol.id.startsWith("parameter:") ? "" : `
2356
-
2357
- Schema id: \`${symbol.id}\``;
2358
- const docs2 = symbol.documentation ? `
2359
-
2360
- ${symbol.documentation}` : "";
2361
- return {
2362
- range: reference2.range,
2363
- markdown: `**${symbol.name}** _${displaySymbolKind(symbol)}_
2364
-
2365
- \`${signature}\`${docs2}${origin}${identity2}`
2366
- };
2358
+ return symbolHover(symbol, reference2.range, snapshot.project);
2367
2359
  }
2368
2360
  const type = resolution.staticType;
2369
2361
  if (!type) return null;
@@ -2380,6 +2372,39 @@ ${type.documentation}` : "";
2380
2372
  Schema id: \`${type.id}\`${docs}`
2381
2373
  };
2382
2374
  }
2375
+ function symbolHover(symbol, range2, project) {
2376
+ const signature = formatSymbolSignature(symbol, project);
2377
+ const origin = symbol.inheritedFrom ? `
2378
+
2379
+ Inherited from \`${symbol.inheritedFrom.typeName}\`.` : "";
2380
+ const identity2 = symbol.id.startsWith("local:") || symbol.id.startsWith("parameter:") ? "" : `
2381
+
2382
+ Schema id: \`${symbol.id}\``;
2383
+ const docs = symbol.documentation ? `
2384
+
2385
+ ${symbol.documentation}` : "";
2386
+ return {
2387
+ range: range2,
2388
+ markdown: `**${symbol.name}** _${displaySymbolKind(symbol)}_
2389
+
2390
+ \`${signature}\`${docs}${origin}${identity2}`
2391
+ };
2392
+ }
2393
+ function builtinStaticMemberHoverAt(snapshot, offset) {
2394
+ const tokens = snapshot.lexed.tokens;
2395
+ const memberIndex = tokens.findIndex(
2396
+ (token) => token.kind === "identifier" && token.start <= offset && offset <= token.end
2397
+ );
2398
+ if (memberIndex < 2 || tokens[memberIndex - 1]?.text !== ".") return null;
2399
+ const owner = tokens[memberIndex - 2];
2400
+ const memberToken = tokens[memberIndex];
2401
+ if (owner?.kind !== "type" || !memberToken) return null;
2402
+ const type = snapshot.project.typeByName.get(owner.text);
2403
+ const member = type?.members.find(
2404
+ (candidate) => candidate.static === true && candidate.name === memberToken.text
2405
+ );
2406
+ return member ? symbolHover(member, memberToken.range, snapshot.project) : null;
2407
+ }
2383
2408
  function definition(snapshot, position) {
2384
2409
  const offset = snapshot.source.offsetAt(position);
2385
2410
  const reference2 = referenceAt(snapshot.parsed.references, offset);
@@ -11838,13 +11863,15 @@ var init_strict_resolver = __esm({
11838
11863
  };
11839
11864
  }
11840
11865
  }
11841
- const receiver = this.resolveExpression(receiverAst, scope);
11842
- if (optional && !isNullable(receiver.type)) {
11866
+ const resolvedReceiver = this.resolveExpression(receiverAst, scope);
11867
+ if (optional && !isNullable(resolvedReceiver.type)) {
11843
11868
  throw new CompileError(
11844
- `\`?.\` requires an optional receiver; got ${this.describe(receiver.type)}`,
11869
+ `\`?.\` requires an optional receiver; got ${this.describe(resolvedReceiver.type)}`,
11845
11870
  pos
11846
11871
  );
11847
11872
  }
11873
+ const receiver = resolvedReceiver.optionalChainType ? { ...resolvedReceiver, type: resolvedReceiver.optionalChainType } : resolvedReceiver;
11874
+ const nullPropagating = optional || resolvedReceiver.optionalChainType !== void 0;
11848
11875
  if (!optional && isNullable(receiver.type)) {
11849
11876
  throw new CompileError(
11850
11877
  `Member '${name}' is read on ${this.describe(receiver.type)}, which may be null. Handle null before reading through it \u2014 use \`!\` if it is always set, or \`?.\`, \`??\`, or a null check that narrows it.`,
@@ -11852,7 +11879,7 @@ var init_strict_resolver = __esm({
11852
11879
  );
11853
11880
  }
11854
11881
  if (receiver.staticType?.kind === "enum") {
11855
- if (optional) {
11882
+ if (nullPropagating) {
11856
11883
  throw new CompileError(
11857
11884
  `\`?.\` is not valid on enum type '${receiver.staticType.name}'; enum option access is always defined`,
11858
11885
  pos
@@ -11924,9 +11951,10 @@ var init_strict_resolver = __esm({
11924
11951
  pointer: receiver.pointer,
11925
11952
  key: numberPointer(0)
11926
11953
  },
11927
- ...optional ? { optional: true } : {}
11954
+ ...nullPropagating ? { optional: true } : {}
11928
11955
  },
11929
- type: optional ? { ...primitiveMember.type, nullable: true } : primitiveMember.type,
11956
+ type: nullPropagating ? { ...primitiveMember.type, nullable: true } : primitiveMember.type,
11957
+ ...nullPropagating ? { optionalChainType: primitiveMember.type } : {},
11930
11958
  ...receiver.writability ? { writability: receiver.writability } : {}
11931
11959
  };
11932
11960
  }
@@ -11934,7 +11962,7 @@ var init_strict_resolver = __esm({
11934
11962
  receiver,
11935
11963
  primitiveMember.key ?? name,
11936
11964
  primitiveMember.type,
11937
- optional
11965
+ nullPropagating
11938
11966
  );
11939
11967
  if (!primitiveMember.derived) return resolved;
11940
11968
  return {
@@ -11968,7 +11996,7 @@ var init_strict_resolver = __esm({
11968
11996
  receiver,
11969
11997
  name,
11970
11998
  { kind: "primitive", name: "string" },
11971
- optional
11999
+ nullPropagating
11972
12000
  );
11973
12001
  }
11974
12002
  const member = type.members.find((candidate) => candidate.name === name);
@@ -12002,9 +12030,10 @@ var init_strict_resolver = __esm({
12002
12030
  type: "callGetter" /* CallGetter */,
12003
12031
  memberId: member.id,
12004
12032
  receiver: { kind: "instance", pointer: receiver.pointer },
12005
- ...optional ? { optional: true } : {}
12033
+ ...nullPropagating ? { optional: true } : {}
12006
12034
  },
12007
- type: optional ? { ...memberType2, nullable: true } : memberType2,
12035
+ type: nullPropagating ? { ...memberType2, nullable: true } : memberType2,
12036
+ ...nullPropagating ? { optionalChainType: memberType2 } : {},
12008
12037
  symbol: member,
12009
12038
  ...member.writable ? {
12010
12039
  writability: member.writability ? toWritability(member.writability) : "setter" /* Setter */
@@ -12028,7 +12057,7 @@ var init_strict_resolver = __esm({
12028
12057
  receiver,
12029
12058
  member.schemaKey ?? member.name,
12030
12059
  memberType2,
12031
- optional,
12060
+ nullPropagating,
12032
12061
  member.id
12033
12062
  ),
12034
12063
  symbol: member,
@@ -12136,14 +12165,23 @@ var init_strict_resolver = __esm({
12136
12165
  ...optional ? { optional: true } : {}
12137
12166
  },
12138
12167
  type: optional ? { ...type, nullable: true } : type,
12168
+ ...optional ? { optionalChainType: type } : {},
12139
12169
  ...receiver.writability ? { writability: receiver.writability } : {}
12140
12170
  };
12141
12171
  }
12142
12172
  resolveIndex(receiverAst, indexAst, scope, pos, optional) {
12143
- const receiver = this.resolveExpression(receiverAst, scope);
12144
- if (optional && !isNullable(receiver.type)) {
12173
+ const resolvedReceiver = this.resolveExpression(receiverAst, scope);
12174
+ if (optional && !isNullable(resolvedReceiver.type)) {
12175
+ throw new CompileError(
12176
+ `\`?.[]\` requires an optional receiver; got ${this.describe(resolvedReceiver.type)}`,
12177
+ pos
12178
+ );
12179
+ }
12180
+ const receiver = resolvedReceiver.optionalChainType ? { ...resolvedReceiver, type: resolvedReceiver.optionalChainType } : resolvedReceiver;
12181
+ const nullPropagating = optional || resolvedReceiver.optionalChainType !== void 0;
12182
+ if (!optional && isNullable(receiver.type)) {
12145
12183
  throw new CompileError(
12146
- `\`?.[]\` requires an optional receiver; got ${this.describe(receiver.type)}`,
12184
+ `Index access is read on ${this.describe(receiver.type)}, which may be null. Handle null before indexing it \u2014 use \`!\` if it is always set, or \`?.[]\`, \`??\`, or a null check that narrows it.`,
12147
12185
  pos
12148
12186
  );
12149
12187
  }
@@ -12185,9 +12223,10 @@ var init_strict_resolver = __esm({
12185
12223
  pointer: {
12186
12224
  type: "keyOf" /* KeyOf */,
12187
12225
  keyOf: { pointer: receiver.pointer, key: requested.pointer },
12188
- ...optional ? { optional: true } : {}
12226
+ ...nullPropagating ? { optional: true } : {}
12189
12227
  },
12190
- type: optional ? { ...receiver.type.elementType, nullable: true } : receiver.type.elementType,
12228
+ type: nullPropagating ? { ...receiver.type.elementType, nullable: true } : receiver.type.elementType,
12229
+ ...nullPropagating ? { optionalChainType: receiver.type.elementType } : {},
12191
12230
  ...receiver.writability ? { writability: receiver.writability } : {}
12192
12231
  };
12193
12232
  }
@@ -12218,9 +12257,10 @@ var init_strict_resolver = __esm({
12218
12257
  pointer: {
12219
12258
  type: "keyOf" /* KeyOf */,
12220
12259
  keyOf: { pointer: receiver.pointer, key: index.pointer },
12221
- ...optional ? { optional: true } : {}
12260
+ ...nullPropagating ? { optional: true } : {}
12222
12261
  },
12223
- type: optional ? { ...valueType, nullable: true } : valueType,
12262
+ type: nullPropagating ? { ...valueType, nullable: true } : valueType,
12263
+ ...nullPropagating ? { optionalChainType: valueType } : {},
12224
12264
  ...receiver.writability ? { writability: receiver.writability } : {}
12225
12265
  };
12226
12266
  }
@@ -27285,6 +27325,16 @@ function projectCompletions(analysis, document, position) {
27285
27325
  position
27286
27326
  );
27287
27327
  if (annotations) return { isIncomplete: false, items: annotations };
27328
+ const builtinMembers = projectBuiltinStaticMemberCompletions(
27329
+ document,
27330
+ position
27331
+ );
27332
+ if (builtinMembers) {
27333
+ return {
27334
+ isIncomplete: false,
27335
+ items: builtinMembers.map(projectBuiltinCompletionItem)
27336
+ };
27337
+ }
27288
27338
  const members = projectMemberCompletions(analysis, document, position);
27289
27339
  const variantScope = variantScopeInBody;
27290
27340
  if (members || variantScope) {
@@ -27785,20 +27835,8 @@ function sourceTypeAt(document, position) {
27785
27835
  return null;
27786
27836
  }
27787
27837
  function projectMemberCompletions(analysis, document, position) {
27788
- const tokens = projectTokens(document).filter(
27789
- (token) => token.kind !== "eof" && token.kind !== "comment" && // Auto-inserted/future punctuation at the cursor has not been authored
27790
- // yet for completion purposes. Including a semicolon that starts at the
27791
- // cursor makes `Device.|;` lose the dot it is completing.
27792
- positionCompare2(token.range.start, position) < 0
27793
- );
27794
- let cursor = tokens.length - 1;
27795
- const current = tokens[cursor];
27796
- if (current?.kind === "identifier" && current.range.start.line === position.line) {
27797
- cursor--;
27798
- }
27799
- if (tokens[cursor]?.text !== ".") return null;
27800
- const owner = tokens[cursor - 1];
27801
- if (!owner || owner.kind !== "identifier") return null;
27838
+ const owner = projectMemberCompletionOwnerToken(document, position);
27839
+ if (!owner) return null;
27802
27840
  const resolvedOwner = projectSymbolAt(analysis, document, owner.range.start);
27803
27841
  if (!resolvedOwner) return null;
27804
27842
  const ownerNames = projectReceiverOwnerNames(analysis, resolvedOwner.symbol);
@@ -27816,6 +27854,71 @@ function projectMemberCompletions(analysis, document, position) {
27816
27854
  }
27817
27855
  return members.length > 0 ? members : null;
27818
27856
  }
27857
+ function projectMemberCompletionOwnerToken(document, position) {
27858
+ const tokens = projectTokens(document).filter(
27859
+ (token) => token.kind !== "eof" && token.kind !== "comment" && // Auto-inserted/future punctuation at the cursor has not been authored
27860
+ // yet for completion purposes. Including a semicolon that starts at the
27861
+ // cursor makes `Device.|;` lose the dot it is completing.
27862
+ positionCompare2(token.range.start, position) < 0
27863
+ );
27864
+ let cursor = tokens.length - 1;
27865
+ const current = tokens[cursor];
27866
+ if (current?.kind === "identifier" && current.range.start.line === position.line) {
27867
+ cursor--;
27868
+ }
27869
+ if (tokens[cursor]?.text !== ".") return null;
27870
+ const owner = tokens[cursor - 1];
27871
+ if (!owner || owner.kind !== "identifier" && owner.kind !== "type") {
27872
+ return null;
27873
+ }
27874
+ return owner;
27875
+ }
27876
+ function projectBuiltinStaticMemberCompletions(document, position) {
27877
+ const owner = projectMemberCompletionOwnerToken(document, position);
27878
+ if (!owner) return null;
27879
+ const type = NEOSCRIPT_BUILTIN_TYPE_SYMBOLS.find(
27880
+ (candidate) => candidate.name === owner.text
27881
+ );
27882
+ if (!type) return null;
27883
+ const members = type.members.filter((member) => member.static === true);
27884
+ return members.length > 0 ? members : null;
27885
+ }
27886
+ function projectBuiltinCompletionItem(symbol) {
27887
+ return {
27888
+ label: symbol.name,
27889
+ kind: projectBuiltinCompletionKind(symbol),
27890
+ detail: projectBuiltinSymbolSignature(symbol),
27891
+ ...symbol.documentation ? { documentation: symbol.documentation } : {},
27892
+ insertText: symbol.name,
27893
+ symbolId: symbol.id
27894
+ };
27895
+ }
27896
+ function projectBuiltinCompletionKind(symbol) {
27897
+ switch (symbol.kind) {
27898
+ case "property":
27899
+ case "field":
27900
+ return "property";
27901
+ case "method":
27902
+ return "method";
27903
+ case "function":
27904
+ return "function";
27905
+ case "enumMember":
27906
+ return "enumMember";
27907
+ default:
27908
+ return "value";
27909
+ }
27910
+ }
27911
+ function projectBuiltinSymbolSignature(symbol) {
27912
+ if (symbol.parameters !== void 0) {
27913
+ const parameters = symbol.parameters.map((parameter4) => `${formatType(parameter4.type)} ${parameter4.name}`).join(", ");
27914
+ return `${formatType(symbol.returnType ?? symbol.type)} ${symbol.name}(${parameters})`;
27915
+ }
27916
+ if (symbol.kind === "property" || symbol.kind === "field") {
27917
+ const setter = symbol.writable ? " set;" : "";
27918
+ return `${formatType(symbol.type)} ${symbol.name} { get;${setter} }`;
27919
+ }
27920
+ return `${formatType(symbol.type)} ${symbol.name}`;
27921
+ }
27819
27922
  function projectConstructorArgumentCompletions(analysis, document, position) {
27820
27923
  const tokens = tokensBeforePosition(document.text, position);
27821
27924
  const openIndex = activeCallOpenIndex(tokens, position);
@@ -28187,6 +28290,31 @@ function projectHover(analysis, document, position) {
28187
28290
  }
28188
28291
  const body = projectBodySnapshotAt(analysis, document, position);
28189
28292
  if (body) return hover(body, position);
28293
+ const builtin = projectBuiltinSymbolAt(document, position);
28294
+ if (builtin) {
28295
+ if (builtin.symbol) {
28296
+ const documentation3 = builtin.symbol.documentation ? `
28297
+
28298
+ ${builtin.symbol.documentation}` : "";
28299
+ return {
28300
+ range: builtin.token.range,
28301
+ markdown: `**${builtin.symbol.name}** _${builtin.symbol.kind}_
28302
+
28303
+ \`${projectBuiltinSymbolSignature(builtin.symbol)}\`${documentation3}
28304
+
28305
+ Schema id: \`${builtin.symbol.id}\``
28306
+ };
28307
+ }
28308
+ const documentation2 = builtin.type.documentation ? `
28309
+
28310
+ ${builtin.type.documentation}` : "";
28311
+ return {
28312
+ range: builtin.token.range,
28313
+ markdown: `**${builtin.type.name}** _${builtin.type.kind}_${documentation2}
28314
+
28315
+ Schema id: \`${builtin.type.id}\``
28316
+ };
28317
+ }
28190
28318
  const resolved = projectSymbolAt(analysis, document, position);
28191
28319
  if (!resolved) return null;
28192
28320
  const owner = resolved.symbol.ownerName ? ` on \`${resolved.symbol.ownerName}\`` : "";
@@ -28203,6 +28331,30 @@ ID: \`${resolved.symbol.id}\`` : "\n\nPending ID";
28203
28331
  markdown: `**${resolved.symbol.kind}** \`${resolved.symbol.name}\`${detail}${owner}${documentation}${contract}${id2}`
28204
28332
  };
28205
28333
  }
28334
+ function projectBuiltinSymbolAt(document, position) {
28335
+ const token = projectTokenAt(document, position);
28336
+ if (!token || token.kind !== "identifier" && token.kind !== "type") {
28337
+ return null;
28338
+ }
28339
+ const tokens = projectTokens(document);
28340
+ const tokenIndex = tokens.findIndex(
28341
+ (candidate) => candidate.start === token.start && candidate.end === token.end
28342
+ );
28343
+ const owner = tokens[tokenIndex - 1]?.text === "." ? tokens[tokenIndex - 2] : void 0;
28344
+ if (owner?.kind === "identifier" || owner?.kind === "type") {
28345
+ const type2 = NEOSCRIPT_BUILTIN_TYPE_SYMBOLS.find(
28346
+ (candidate) => candidate.name === owner.text
28347
+ );
28348
+ const symbol = type2?.members.find(
28349
+ (candidate) => candidate.static === true && candidate.name === token.text
28350
+ );
28351
+ if (type2 && symbol) return { token, type: type2, symbol };
28352
+ }
28353
+ const type = NEOSCRIPT_BUILTIN_TYPE_SYMBOLS.find(
28354
+ (candidate) => candidate.name === token.text
28355
+ );
28356
+ return type ? { token, type } : null;
28357
+ }
28206
28358
  function projectConstructionContractMarkdown(analysis, className) {
28207
28359
  const index = projectConstructionIndex(analysis);
28208
28360
  const contract = classConstructionContract(index, className);
@@ -29358,6 +29510,7 @@ var init_project_source_language_features = __esm({
29358
29510
  init_language_spec();
29359
29511
  init_project_source_semantics();
29360
29512
  init_project_source_variants();
29513
+ init_project();
29361
29514
  init_project_source_semantics();
29362
29515
  init_project_source_settlement();
29363
29516
  init_project_source_tokens();
@@ -67143,6 +67296,7 @@ function isRuntimeFileValue(value) {
67143
67296
  return typeof fileId === "string" && fileId.length > 0;
67144
67297
  }
67145
67298
  function isRuntimeSpriteValue(value) {
67299
+ if (isEmptySpriteValue(value)) return true;
67146
67300
  if (!isRuntimeFileValue(value)) return false;
67147
67301
  const sliceIndex = value.sliceIndex;
67148
67302
  return typeof sliceIndex === "number" && Number.isInteger(sliceIndex) && sliceIndex >= 0;
@@ -111261,7 +111415,7 @@ var init_registry2 = __esm({
111261
111415
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
111262
111416
  formatVersion: 3,
111263
111417
  contractVersion: "3.13",
111264
- cliVersion: "0.31.7",
111418
+ cliVersion: "0.31.9",
111265
111419
  projectFileUploadBatchSize: 32,
111266
111420
  documentRecords: {
111267
111421
  member: {
@@ -117856,7 +118010,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
117856
118010
  async function main() {
117857
118011
  const args = parseArgs(process.argv.slice(2));
117858
118012
  if (args.command === "--version") {
117859
- console.log("0.31.7");
118013
+ console.log("0.31.9");
117860
118014
  return;
117861
118015
  }
117862
118016
  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.31.7",
3
+ "version": "0.31.9",
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.31.7 -->
12
+ <!-- reviewed-through-cli: 0.31.9 -->
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.31.7 -->
86
+ <!-- reviewed-through-cli: 0.31.9 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -49,8 +49,17 @@ if (player.Target != null) {
49
49
  return player.Target?.Name ?? "Unknown";
50
50
  ```
51
51
 
52
+ Null propagation remains active across following required accesses in the same
53
+ chain, so `config.Hat?.Head.Idle.Down.Frames` safely returns null when `Hat` is
54
+ null. A descendant that is itself nullable still needs its own `?.`, `??`, `!`,
55
+ or narrowing guard before the next access.
56
+
52
57
  Treat lookup-return values with the same nullable narrowing rules.
53
58
 
59
+ Use `SpriteInfo.Empty` when a required sprite should intentionally render no
60
+ image. It is the canonical non-null empty sprite: it has no backing file and
61
+ uses slice index zero.
62
+
54
63
  Let runtime ownership—not body kind—decide whether a write target is Immutable,
55
64
  Save, Session, Setter, or otherwise writable. A constructor writes only its
56
65
  `this` instance. A writable structured-leaf field still inherits the receiver's