@neocompose/cli 0.29.0 → 0.30.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.0] - 2026-08-13
4
+
5
+ ### Added
6
+
7
+ - P66 sealed classes: author a leaf type as `sealed class` in project source.
8
+ The modifier round-trips through pull and push, and the CLI rejects both
9
+ `sealed abstract` declarations and attempts to extend a sealed class.
10
+
11
+ ### Changed
12
+
13
+ - Sealed classes are omitted from extends-position language completions, and
14
+ the project contract carries optional `isSealed` metadata. Absence and
15
+ explicit `false` are equivalent, so existing projects need no migration.
16
+ - Unity export schema version is 20. Matching SDKs preserve the sealed
17
+ inheritance contract in generated C#.
18
+
3
19
  ## [0.29.0] - 2026-08-12
4
20
 
5
21
  ### Added
package/dist/neo.mjs CHANGED
@@ -17376,7 +17376,7 @@ var init_project_schema_contract_generated = __esm({
17376
17376
  "../packages/neoscript-language/src/project-schema-contract.generated.ts"() {
17377
17377
  "use strict";
17378
17378
  PROJECT_SCHEMA_MANIFEST_FORMAT_VERSION = 3;
17379
- PROJECT_SCHEMA_MANIFEST_CONTRACT_VERSION = "3.10";
17379
+ PROJECT_SCHEMA_MANIFEST_CONTRACT_VERSION = "3.11";
17380
17380
  PROJECT_FILE_UPLOAD_BATCH_SIZE = 32;
17381
17381
  NEO_PROJECT_SOURCE_RECORD_CONTRACT = {
17382
17382
  "recordFields": {
@@ -17437,6 +17437,7 @@ var init_project_schema_contract_generated = __esm({
17437
17437
  "extendsClassId",
17438
17438
  "implementsInterfaceIds",
17439
17439
  "hiddenInMemberSelector",
17440
+ "isSealed",
17440
17441
  "isAbstract",
17441
17442
  "system",
17442
17443
  "allowedStorage",
@@ -26020,6 +26021,7 @@ function projectFallbackCompletions(analysis, document, position) {
26020
26021
  const expectedTypeName = construction?.expectedTypeName ?? recoveredInitializerType;
26021
26022
  const afterNew = expression && projectAfterNewAt(document, position);
26022
26023
  const typePosition = projectTypePositionAt(analysis, document, position);
26024
+ const classBasePosition = projectClassBasePositionAt(document, position);
26023
26025
  const keywords = expression ? afterNew ? [] : ["new"] : typePosition ? [] : [
26024
26026
  "class",
26025
26027
  "interface",
@@ -26058,6 +26060,11 @@ function projectFallbackCompletions(analysis, document, position) {
26058
26060
  continue;
26059
26061
  }
26060
26062
  const isType = symbol.kind === "class" || symbol.kind === "interface" || symbol.kind === "enum" || symbol.kind === "genericParameter";
26063
+ if (classBasePosition && symbol.kind === "class") {
26064
+ const declaration = findTypeDeclaration(analysis, symbol.name);
26065
+ if (declaration?.kind === "class" && declaration.modifiers.includes("sealed"))
26066
+ continue;
26067
+ }
26061
26068
  if (typePosition && !isType) continue;
26062
26069
  if (expression) {
26063
26070
  if (afterNew) {
@@ -26199,6 +26206,49 @@ function projectTypePositionAt(analysis, document, position) {
26199
26206
  line
26200
26207
  );
26201
26208
  }
26209
+ function projectClassBasePositionAt(document, position) {
26210
+ const tokens = tokensBeforePosition(document.text, position);
26211
+ let classIndex = -1;
26212
+ for (let index = tokens.length - 1; index >= 0; index--) {
26213
+ const text = tokens[index]?.text;
26214
+ if (text === "class") {
26215
+ classIndex = index;
26216
+ break;
26217
+ }
26218
+ if (text === "{" || text === "}" || text === ";") return false;
26219
+ }
26220
+ if (classIndex < 0) return false;
26221
+ let angleDepth = 0;
26222
+ let parenthesisDepth = 0;
26223
+ let bracketDepth = 0;
26224
+ let colonIndex = -1;
26225
+ for (let index = classIndex + 1; index < tokens.length; index++) {
26226
+ const text = tokens[index]?.text;
26227
+ if (text === "<") angleDepth++;
26228
+ else if (text === ">") angleDepth = Math.max(0, angleDepth - 1);
26229
+ else if (text === "(") parenthesisDepth++;
26230
+ else if (text === ")") parenthesisDepth = Math.max(0, parenthesisDepth - 1);
26231
+ else if (text === "[") bracketDepth++;
26232
+ else if (text === "]") bracketDepth = Math.max(0, bracketDepth - 1);
26233
+ else if (text === ":" && angleDepth === 0 && parenthesisDepth === 0 && bracketDepth === 0) {
26234
+ colonIndex = index;
26235
+ break;
26236
+ } else if (text === "{" || text === "}" || text === ";") {
26237
+ return false;
26238
+ }
26239
+ }
26240
+ if (colonIndex < 0) return false;
26241
+ angleDepth = 0;
26242
+ for (let index = colonIndex + 1; index < tokens.length; index++) {
26243
+ const text = tokens[index]?.text;
26244
+ if (text === "<") angleDepth++;
26245
+ else if (text === ">") angleDepth = Math.max(0, angleDepth - 1);
26246
+ else if (angleDepth === 0 && (text === "(" || text === "{" || text === "}" || text === ";")) {
26247
+ return false;
26248
+ }
26249
+ }
26250
+ return true;
26251
+ }
26202
26252
  function sourceTypeAt(document, position) {
26203
26253
  const contains2 = (type) => {
26204
26254
  for (const argument2 of type.typeArguments) {
@@ -31520,6 +31570,7 @@ var init_document_contracts = __esm({
31520
31570
  "extendsClassId",
31521
31571
  "implementsInterfaceIds",
31522
31572
  "hiddenInMemberSelector",
31573
+ "isSealed",
31523
31574
  "isAbstract",
31524
31575
  "system",
31525
31576
  "allowedStorage",
@@ -31534,6 +31585,7 @@ var init_document_contracts = __esm({
31534
31585
  derived: [],
31535
31586
  volatile: ["projectId", "createdAt", "updatedAt"],
31536
31587
  normalization: {
31588
+ isSealed: "falseIsAbsent",
31537
31589
  targetMemberId: "nullIsAbsent",
31538
31590
  schemaKeyOrder: "nullIsAbsent",
31539
31591
  extendsClassId: "nullIsAbsent",
@@ -31802,6 +31854,10 @@ function normalizeDocumentFields(recordKind, value) {
31802
31854
  }
31803
31855
  continue;
31804
31856
  }
31857
+ if (rule === "falseIsAbsent") {
31858
+ if (current === false) delete normalized[field];
31859
+ continue;
31860
+ }
31805
31861
  const isFullMember = recordKind !== "member" || typeof normalized.extendsMemberId !== "string";
31806
31862
  if (recordKind === "member" && (current === void 0 || current === null) && rule.startsWith("missingIs") && !memberDefaultApplies(field, normalized.kind)) {
31807
31863
  if (current === null) delete normalized[field];
@@ -34399,6 +34455,7 @@ function schemaClassFromDocument(record3, context) {
34399
34455
  name: requiredString(data, "name", record3),
34400
34456
  ...optionalDocsTextProperty(data.docsText, record3),
34401
34457
  declarationModifier: data.isAbstract === true ? "abstract" : "concrete",
34458
+ ...data.isSealed === void 0 ? {} : { isSealed: booleanValue(data.isSealed, record3, "isSealed") },
34402
34459
  schema: requiredRecord(data, "schema", record3),
34403
34460
  schemaKeyOrder: optionalStringArrayOrNull(data.schemaKeyOrder),
34404
34461
  extendsClassId: optionalStringOrNull(data.extendsClassId),
@@ -34434,6 +34491,7 @@ function schemaClassToDocument(schemaClass2) {
34434
34491
  implementsInterfaceIds: schemaClass2.implementsInterfaceIds,
34435
34492
  hiddenInMemberSelector: schemaClass2.hiddenInMemberSelector,
34436
34493
  isAbstract: schemaClass2.declarationModifier === "abstract",
34494
+ isSealed: schemaClass2.isSealed,
34437
34495
  system: systemToDocument(schemaClass2.system),
34438
34496
  allowedStorage: schemaClass2.allowedStorage,
34439
34497
  allowedStorageKeys: schemaClass2.allowedStorageKeys,
@@ -35708,6 +35766,7 @@ function assertNeoSchemaClass(value, path) {
35708
35766
  ],
35709
35767
  [
35710
35768
  "docsText",
35769
+ "isSealed",
35711
35770
  "constructorProjections",
35712
35771
  "constructorIds",
35713
35772
  "requiredConstructorId"
@@ -35723,6 +35782,9 @@ function assertNeoSchemaClass(value, path) {
35723
35782
  );
35724
35783
  stringRecord(type.schema, `${path}.schema`);
35725
35784
  nullableStringArray(type.schemaKeyOrder, `${path}.schemaKeyOrder`);
35785
+ if (type.isSealed !== void 0) {
35786
+ booleanAt(type.isSealed, `${path}.isSealed`);
35787
+ }
35726
35788
  nullableNonEmptyString(type.extendsClassId, `${path}.extendsClassId`);
35727
35789
  stringArray(type.implementsInterfaceIds, `${path}.implementsInterfaceIds`);
35728
35790
  booleanAt(type.hiddenInMemberSelector, `${path}.hiddenInMemberSelector`);
@@ -39482,7 +39544,7 @@ function isNeoSchemaClassBase(value) {
39482
39544
  const v = value;
39483
39545
  return typeof v?.name === "string" && isValidDocsText(v.docsText) && isClassSchema(v?.schema) && isOptionalSchemaKeyOrder(v?.schemaKeyOrder) && (v.implementsInterfaceIds === void 0 || v.implementsInterfaceIds === null || Array.isArray(v.implementsInterfaceIds) && v.implementsInterfaceIds.every(
39484
39546
  (interfaceId) => typeof interfaceId === "string" && interfaceId.length > 0
39485
- )) && typeof v?.hiddenInMemberSelector === "boolean" && typeof v?.isAbstract === "boolean" && (v.allowedStorage === void 0 || v.allowedStorage === null || isEffectiveMemberStorage(v.allowedStorage)) && (v.allowedStorageKeys === void 0 || v.allowedStorageKeys === null || Array.isArray(v.allowedStorageKeys) && v.allowedStorageKeys.every((key) => typeof key === "string")) && (v.genericParams === void 0 || v.genericParams === null || Array.isArray(v.genericParams) && v.genericParams.every(isGenericParamDeclaration)) && (v.extendsGenericBindings === void 0 || v.extendsGenericBindings === null || isGenericBindingsRecord(v.extendsGenericBindings)) && (v.constructorProjections === void 0 || v.constructorProjections === null || Array.isArray(v.constructorProjections) && v.constructorProjections.every(isNeoClassConstructorProjection) && v.system?.kind === SystemProtectionKind.WorldAuthoring) && (v.constructorIds === void 0 || v.constructorIds === null || Array.isArray(v.constructorIds) && v.constructorIds.every(
39547
+ )) && typeof v?.hiddenInMemberSelector === "boolean" && typeof v?.isAbstract === "boolean" && (v.isSealed === void 0 || typeof v.isSealed === "boolean") && (v.allowedStorage === void 0 || v.allowedStorage === null || isEffectiveMemberStorage(v.allowedStorage)) && (v.allowedStorageKeys === void 0 || v.allowedStorageKeys === null || Array.isArray(v.allowedStorageKeys) && v.allowedStorageKeys.every((key) => typeof key === "string")) && (v.genericParams === void 0 || v.genericParams === null || Array.isArray(v.genericParams) && v.genericParams.every(isGenericParamDeclaration)) && (v.extendsGenericBindings === void 0 || v.extendsGenericBindings === null || isGenericBindingsRecord(v.extendsGenericBindings)) && (v.constructorProjections === void 0 || v.constructorProjections === null || Array.isArray(v.constructorProjections) && v.constructorProjections.every(isNeoClassConstructorProjection) && v.system?.kind === SystemProtectionKind.WorldAuthoring) && (v.constructorIds === void 0 || v.constructorIds === null || Array.isArray(v.constructorIds) && v.constructorIds.every(
39486
39548
  (constructorId) => typeof constructorId === "string" && constructorId.length > 0
39487
39549
  ) && new Set(v.constructorIds).size === v.constructorIds.length) && (v.requiredConstructorId === void 0 || v.requiredConstructorId === null || typeof v.requiredConstructorId === "string" && v.requiredConstructorId.length > 0 && (v.constructorIds ?? []).length === 0) && (v.targetMemberId === void 0 || v.targetMemberId === null || typeof v.targetMemberId === "string" && v.targetMemberId.length > 0) && (v.system === void 0 || v.system === null || isSystemMetadata(v.system));
39488
39550
  }
@@ -46682,7 +46744,7 @@ function emitClass(context, schemaClass2, memberIds) {
46682
46744
  ...schemaClass2.system ? [systemAnnotation2(schemaClass2.system)] : [],
46683
46745
  ...relationsAnnotations(context, schemaClass2)
46684
46746
  ];
46685
- const modifier = schemaClass2.declarationModifier === "abstract" ? "abstract " : "";
46747
+ const modifier = schemaClass2.isSealed ? "sealed " : schemaClass2.declarationModifier === "abstract" ? "abstract " : "";
46686
46748
  const generics = schemaClass2.genericParameters.length ? `<
46687
46749
  ${schemaClass2.genericParameters.map((parameter4) => {
46688
46750
  const constraint = parameter4.constraint ? ` extends ${parameter4.constraint.kind === "class" ? required(
@@ -53034,6 +53096,7 @@ function lowerClass(context, declaration) {
53034
53096
  name: declaration.name,
53035
53097
  ...declaration.docsText === void 0 ? {} : { docsText: declaration.docsText },
53036
53098
  declarationModifier: declaration.modifiers.includes("abstract") ? "abstract" : "concrete",
53099
+ ...declaration.modifiers.includes("sealed") ? { isSealed: true } : {},
53037
53100
  schema,
53038
53101
  schemaKeyOrder: persistedSchemaOrder,
53039
53102
  extendsClassId,
@@ -77015,6 +77078,19 @@ function readClass(value) {
77015
77078
  throw new Error(`Class "${value.id}" schema value is invalid.`);
77016
77079
  }
77017
77080
  }
77081
+ if (value.isSealed !== void 0 && typeof value.isSealed !== "boolean") {
77082
+ throw new Error(`Class "${value.name}" (${value.id}) isSealed is invalid.`);
77083
+ }
77084
+ if (value.isAbstract !== void 0 && typeof value.isAbstract !== "boolean") {
77085
+ throw new Error(
77086
+ `Class "${value.name}" (${value.id}) isAbstract is invalid.`
77087
+ );
77088
+ }
77089
+ if (value.isAbstract === true && value.isSealed === true) {
77090
+ throw new Error(
77091
+ `Class "${value.name}" (${value.id}) cannot be both abstract and sealed.`
77092
+ );
77093
+ }
77018
77094
  if (value.implementsInterfaceIds != null && (!Array.isArray(value.implementsInterfaceIds) || !value.implementsInterfaceIds.every((id2) => typeof id2 === "string"))) {
77019
77095
  throw new Error(`Class "${value.id}" implementsInterfaceIds is invalid.`);
77020
77096
  }
@@ -77287,6 +77363,14 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
77287
77363
  const classesById = new Map(
77288
77364
  classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
77289
77365
  );
77366
+ for (const schemaClass2 of classes) {
77367
+ if (schemaClass2.extendsClassId === void 0) continue;
77368
+ const baseClass = classesById.get(schemaClass2.extendsClassId);
77369
+ if (baseClass?.isSealed !== true) continue;
77370
+ throw new Error(
77371
+ `Class "${schemaClass2.name}" (${schemaClass2.id}) cannot extend sealed class "${baseClass.name}" (${baseClass.id}).`
77372
+ );
77373
+ }
77290
77374
  const genericParamConstraintsById = new Map(
77291
77375
  classes.flatMap(
77292
77376
  (schemaClass2) => (schemaClass2.genericParams ?? []).map(
@@ -104529,8 +104613,8 @@ var init_registry2 = __esm({
104529
104613
  "use strict";
104530
104614
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
104531
104615
  formatVersion: 3,
104532
- contractVersion: "3.10",
104533
- cliVersion: "0.29.0",
104616
+ contractVersion: "3.11",
104617
+ cliVersion: "0.30.0",
104534
104618
  projectFileUploadBatchSize: 32,
104535
104619
  documentRecords: {
104536
104620
  member: {
@@ -104625,6 +104709,7 @@ var init_registry2 = __esm({
104625
104709
  "extendsClassId",
104626
104710
  "implementsInterfaceIds",
104627
104711
  "hiddenInMemberSelector",
104712
+ "isSealed",
104628
104713
  "isAbstract",
104629
104714
  "system",
104630
104715
  "allowedStorage",
@@ -104639,6 +104724,7 @@ var init_registry2 = __esm({
104639
104724
  derived: [],
104640
104725
  volatile: ["projectId", "createdAt", "updatedAt"],
104641
104726
  normalization: {
104727
+ isSealed: "falseIsAbsent",
104642
104728
  targetMemberId: "nullIsAbsent",
104643
104729
  schemaKeyOrder: "nullIsAbsent",
104644
104730
  extendsClassId: "nullIsAbsent",
@@ -111096,7 +111182,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
111096
111182
  async function main() {
111097
111183
  const args = parseArgs(process.argv.slice(2));
111098
111184
  if (args.command === "--version") {
111099
- console.log("0.29.0");
111185
+ console.log("0.30.0");
111100
111186
  return;
111101
111187
  }
111102
111188
  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.29.0",
3
+ "version": "0.30.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.29.0 -->
12
+ <!-- reviewed-through-cli: 0.30.0 -->
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.29.0 -->
86
+ <!-- reviewed-through-cli: 0.30.0 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -47,8 +47,17 @@ abstract class Item<TContext extends SomeClass> : INamed {
47
47
  ```
48
48
 
49
49
  Use ordinary `public`, `protected`, `private`, `static`, `virtual`, `abstract`,
50
- and `override` semantics. Interfaces are non-generic. Do not put `@id` on
51
- inferred list/dictionary type entries or on derived structural descriptors.
50
+ `sealed`, and `override` semantics. A sealed class is an instantiable leaf:
51
+ it may extend another class but cannot be extended, cannot also be abstract,
52
+ and can only be sealed while it has no subclasses. Interfaces are non-generic.
53
+ Do not put `@id` on inferred list/dictionary type entries or on derived
54
+ structural descriptors.
55
+
56
+ ```neo
57
+ sealed class DirtTile : PlaceableTile {
58
+ public bool IsWet = false;
59
+ }
60
+ ```
52
61
 
53
62
  A bodyless function must be `abstract`, `native`, or an interface contract.
54
63
  `async` alone does not make it valid.