@neocompose/cli 0.28.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,39 @@
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
+
19
+ ## [0.29.0] - 2026-08-12
20
+
21
+ ### Added
22
+
23
+ - `Reference<T>(id: "...", withProvenance: true)` for executable NeoScript.
24
+ The opt-in flag resolves an authored row id to the nearest clone carrying it
25
+ as `sourceValueId` in the lexical receiver's ownership graph. It defaults to
26
+ `false`, preserving exact-ID behavior for every existing reference.
27
+
28
+ ### Changed
29
+
30
+ - Animation child selectors created by the world UI and the legacy
31
+ `Child`-to-`Selector` reconciler now opt into provenance so clips authored
32
+ against a class default select each placement's cloned child.
33
+ - Unity export schema version is 19 and NeoScript compiler revision is 9.
34
+ Older SDKs reject the export rather than silently ignoring provenance and
35
+ applying an override to the shared authored row.
36
+
3
37
  ## [0.28.0] - 2026-08-12
4
38
 
5
39
  ### Added
package/dist/neo.mjs CHANGED
@@ -7891,7 +7891,7 @@ var NEOSCRIPT_COMPILER_REVISION;
7891
7891
  var init_strict_ir = __esm({
7892
7892
  "../packages/neoscript-language/src/strict-ir.ts"() {
7893
7893
  "use strict";
7894
- NEOSCRIPT_COMPILER_REVISION = 8;
7894
+ NEOSCRIPT_COMPILER_REVISION = 9;
7895
7895
  }
7896
7896
  });
7897
7897
 
@@ -10597,9 +10597,34 @@ var init_strict_resolver = __esm({
10597
10597
  expression.pos
10598
10598
  );
10599
10599
  }
10600
- if (expression.args.length !== 1 || expression.argumentNames?.[0] !== "id" || expression.args[0]?.kind !== "litString" || expression.args[0].value.length === 0) {
10600
+ const hasProvenanceArgument = expression.args.length === 2;
10601
+ if (expression.args.length < 1 || expression.args.length > 2) {
10601
10602
  throw new CompileError(
10602
- "Reference<T> requires exactly one named id: string argument.",
10603
+ "Reference<T> requires id: string and optionally withProvenance: bool.",
10604
+ expression.pos
10605
+ );
10606
+ }
10607
+ if (expression.argumentNames?.[0] !== "id") {
10608
+ throw new CompileError(
10609
+ "Reference<T> requires id: as its first named argument.",
10610
+ expression.pos
10611
+ );
10612
+ }
10613
+ if (expression.args[0]?.kind !== "litString" || expression.args[0].value.length === 0) {
10614
+ throw new CompileError(
10615
+ "Reference<T> id must be a non-empty string literal.",
10616
+ expression.pos
10617
+ );
10618
+ }
10619
+ if (hasProvenanceArgument && expression.argumentNames?.[1] !== "withProvenance") {
10620
+ throw new CompileError(
10621
+ "Reference<T>'s second named argument must be withProvenance: bool.",
10622
+ expression.pos
10623
+ );
10624
+ }
10625
+ if (hasProvenanceArgument && expression.args[1]?.kind !== "litBool") {
10626
+ throw new CompileError(
10627
+ "Reference<T> withProvenance must be a boolean literal.",
10603
10628
  expression.pos
10604
10629
  );
10605
10630
  }
@@ -10613,7 +10638,8 @@ var init_strict_resolver = __esm({
10613
10638
  return {
10614
10639
  pointer: {
10615
10640
  type: "reference" /* Reference */,
10616
- valueId: expression.args[0].value
10641
+ valueId: expression.args[0].value,
10642
+ ...expression.args[1]?.kind === "litBool" && expression.args[1].value ? { withProvenance: true } : {}
10617
10643
  },
10618
10644
  type: referencedType
10619
10645
  };
@@ -17350,7 +17376,7 @@ var init_project_schema_contract_generated = __esm({
17350
17376
  "../packages/neoscript-language/src/project-schema-contract.generated.ts"() {
17351
17377
  "use strict";
17352
17378
  PROJECT_SCHEMA_MANIFEST_FORMAT_VERSION = 3;
17353
- PROJECT_SCHEMA_MANIFEST_CONTRACT_VERSION = "3.10";
17379
+ PROJECT_SCHEMA_MANIFEST_CONTRACT_VERSION = "3.11";
17354
17380
  PROJECT_FILE_UPLOAD_BATCH_SIZE = 32;
17355
17381
  NEO_PROJECT_SOURCE_RECORD_CONTRACT = {
17356
17382
  "recordFields": {
@@ -17411,6 +17437,7 @@ var init_project_schema_contract_generated = __esm({
17411
17437
  "extendsClassId",
17412
17438
  "implementsInterfaceIds",
17413
17439
  "hiddenInMemberSelector",
17440
+ "isSealed",
17414
17441
  "isAbstract",
17415
17442
  "system",
17416
17443
  "allowedStorage",
@@ -21834,7 +21861,9 @@ function validateReferenceExpression(expression, expected, environment, uri, ran
21834
21861
  const typeArguments = expression.typeArguments ?? [];
21835
21862
  const names = expression.argumentNames ?? [];
21836
21863
  const idIndex = names.findIndex((name) => name === "id");
21864
+ const provenanceIndex = names.findIndex((name) => name === "withProvenance");
21837
21865
  const hasId = idIndex >= 0;
21866
+ const hasProvenance = provenanceIndex >= 0;
21838
21867
  const argument2 = expression.args[hasId ? idIndex : 0];
21839
21868
  const malformed = (message) => pushDiagnostic(
21840
21869
  diagnostics,
@@ -21878,9 +21907,11 @@ function validateReferenceExpression(expression, expected, environment, uri, ran
21878
21907
  }
21879
21908
  return;
21880
21909
  }
21881
- if (expression.args.length !== 1 || hasId && (idIndex !== 0 || names.some((name) => name !== "id")) || !hasId && names.some((name) => name !== null)) {
21910
+ const validIdShape = hasId && idIndex === 0 && expression.args.length === (hasProvenance ? 2 : 1) && (!hasProvenance || provenanceIndex === 1) && names.every((name) => name === "id" || name === "withProvenance");
21911
+ const validSymbolShape = !hasId && !hasProvenance && expression.args.length === 1 && names.every((name) => name === null);
21912
+ if (!validIdShape && !validSymbolShape) {
21882
21913
  malformed(
21883
- "Reference requires exactly one symbol argument or one named id: string argument."
21914
+ "Reference requires one symbol argument, or id: string followed by optional withProvenance: bool."
21884
21915
  );
21885
21916
  return;
21886
21917
  }
@@ -21890,6 +21921,10 @@ function validateReferenceExpression(expression, expected, environment, uri, ran
21890
21921
  malformed("Reference id must be one non-empty string literal.");
21891
21922
  return;
21892
21923
  }
21924
+ if (hasProvenance && expression.args[provenanceIndex]?.kind !== "litBool") {
21925
+ malformed("Reference withProvenance must be a boolean literal.");
21926
+ return;
21927
+ }
21893
21928
  if (!explicit && expected?.name !== "string") {
21894
21929
  malformed("Reference(id: ...) requires an explicit generic target type.");
21895
21930
  return;
@@ -25986,6 +26021,7 @@ function projectFallbackCompletions(analysis, document, position) {
25986
26021
  const expectedTypeName = construction?.expectedTypeName ?? recoveredInitializerType;
25987
26022
  const afterNew = expression && projectAfterNewAt(document, position);
25988
26023
  const typePosition = projectTypePositionAt(analysis, document, position);
26024
+ const classBasePosition = projectClassBasePositionAt(document, position);
25989
26025
  const keywords = expression ? afterNew ? [] : ["new"] : typePosition ? [] : [
25990
26026
  "class",
25991
26027
  "interface",
@@ -26024,6 +26060,11 @@ function projectFallbackCompletions(analysis, document, position) {
26024
26060
  continue;
26025
26061
  }
26026
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
+ }
26027
26068
  if (typePosition && !isType) continue;
26028
26069
  if (expression) {
26029
26070
  if (afterNew) {
@@ -26165,6 +26206,49 @@ function projectTypePositionAt(analysis, document, position) {
26165
26206
  line
26166
26207
  );
26167
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
+ }
26168
26252
  function sourceTypeAt(document, position) {
26169
26253
  const contains2 = (type) => {
26170
26254
  for (const argument2 of type.typeArguments) {
@@ -26596,7 +26680,11 @@ function projectSignatureHelp(analysis, document, position) {
26596
26680
  return signatureResult(`@${callee.text}`, parameters, activeParameter);
26597
26681
  }
26598
26682
  if (callee.text === "Reference") {
26599
- return signatureResult("Reference", ["symbol", "id"], activeParameter);
26683
+ return signatureResult(
26684
+ "Reference",
26685
+ ["symbol or id", "withProvenance = false"],
26686
+ activeParameter
26687
+ );
26600
26688
  }
26601
26689
  if (callee.text === "Pause") {
26602
26690
  return signatureResult(
@@ -31482,6 +31570,7 @@ var init_document_contracts = __esm({
31482
31570
  "extendsClassId",
31483
31571
  "implementsInterfaceIds",
31484
31572
  "hiddenInMemberSelector",
31573
+ "isSealed",
31485
31574
  "isAbstract",
31486
31575
  "system",
31487
31576
  "allowedStorage",
@@ -31496,6 +31585,7 @@ var init_document_contracts = __esm({
31496
31585
  derived: [],
31497
31586
  volatile: ["projectId", "createdAt", "updatedAt"],
31498
31587
  normalization: {
31588
+ isSealed: "falseIsAbsent",
31499
31589
  targetMemberId: "nullIsAbsent",
31500
31590
  schemaKeyOrder: "nullIsAbsent",
31501
31591
  extendsClassId: "nullIsAbsent",
@@ -31764,6 +31854,10 @@ function normalizeDocumentFields(recordKind, value) {
31764
31854
  }
31765
31855
  continue;
31766
31856
  }
31857
+ if (rule === "falseIsAbsent") {
31858
+ if (current === false) delete normalized[field];
31859
+ continue;
31860
+ }
31767
31861
  const isFullMember = recordKind !== "member" || typeof normalized.extendsMemberId !== "string";
31768
31862
  if (recordKind === "member" && (current === void 0 || current === null) && rule.startsWith("missingIs") && !memberDefaultApplies(field, normalized.kind)) {
31769
31863
  if (current === null) delete normalized[field];
@@ -34361,6 +34455,7 @@ function schemaClassFromDocument(record3, context) {
34361
34455
  name: requiredString(data, "name", record3),
34362
34456
  ...optionalDocsTextProperty(data.docsText, record3),
34363
34457
  declarationModifier: data.isAbstract === true ? "abstract" : "concrete",
34458
+ ...data.isSealed === void 0 ? {} : { isSealed: booleanValue(data.isSealed, record3, "isSealed") },
34364
34459
  schema: requiredRecord(data, "schema", record3),
34365
34460
  schemaKeyOrder: optionalStringArrayOrNull(data.schemaKeyOrder),
34366
34461
  extendsClassId: optionalStringOrNull(data.extendsClassId),
@@ -34396,6 +34491,7 @@ function schemaClassToDocument(schemaClass2) {
34396
34491
  implementsInterfaceIds: schemaClass2.implementsInterfaceIds,
34397
34492
  hiddenInMemberSelector: schemaClass2.hiddenInMemberSelector,
34398
34493
  isAbstract: schemaClass2.declarationModifier === "abstract",
34494
+ isSealed: schemaClass2.isSealed,
34399
34495
  system: systemToDocument(schemaClass2.system),
34400
34496
  allowedStorage: schemaClass2.allowedStorage,
34401
34497
  allowedStorageKeys: schemaClass2.allowedStorageKeys,
@@ -35670,6 +35766,7 @@ function assertNeoSchemaClass(value, path) {
35670
35766
  ],
35671
35767
  [
35672
35768
  "docsText",
35769
+ "isSealed",
35673
35770
  "constructorProjections",
35674
35771
  "constructorIds",
35675
35772
  "requiredConstructorId"
@@ -35685,6 +35782,9 @@ function assertNeoSchemaClass(value, path) {
35685
35782
  );
35686
35783
  stringRecord(type.schema, `${path}.schema`);
35687
35784
  nullableStringArray(type.schemaKeyOrder, `${path}.schemaKeyOrder`);
35785
+ if (type.isSealed !== void 0) {
35786
+ booleanAt(type.isSealed, `${path}.isSealed`);
35787
+ }
35688
35788
  nullableNonEmptyString(type.extendsClassId, `${path}.extendsClassId`);
35689
35789
  stringArray(type.implementsInterfaceIds, `${path}.implementsInterfaceIds`);
35690
35790
  booleanAt(type.hiddenInMemberSelector, `${path}.hiddenInMemberSelector`);
@@ -38455,7 +38555,9 @@ function isNSKeyOf(value) {
38455
38555
  }
38456
38556
  function isNSPointerReference(value) {
38457
38557
  const v = value;
38458
- return v?.type === "reference" /* reference */ && typeof v?.valueId === "string";
38558
+ if (v?.type !== "reference" /* reference */) return false;
38559
+ if (typeof v.valueId !== "string") return false;
38560
+ return v.withProvenance === void 0 || typeof v.withProvenance === "boolean";
38459
38561
  }
38460
38562
  function isNSPointerVariable(value) {
38461
38563
  const v = value;
@@ -39442,7 +39544,7 @@ function isNeoSchemaClassBase(value) {
39442
39544
  const v = value;
39443
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(
39444
39546
  (interfaceId) => typeof interfaceId === "string" && interfaceId.length > 0
39445
- )) && 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(
39446
39548
  (constructorId) => typeof constructorId === "string" && constructorId.length > 0
39447
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));
39448
39550
  }
@@ -46642,7 +46744,7 @@ function emitClass(context, schemaClass2, memberIds) {
46642
46744
  ...schemaClass2.system ? [systemAnnotation2(schemaClass2.system)] : [],
46643
46745
  ...relationsAnnotations(context, schemaClass2)
46644
46746
  ];
46645
- const modifier = schemaClass2.declarationModifier === "abstract" ? "abstract " : "";
46747
+ const modifier = schemaClass2.isSealed ? "sealed " : schemaClass2.declarationModifier === "abstract" ? "abstract " : "";
46646
46748
  const generics = schemaClass2.genericParameters.length ? `<
46647
46749
  ${schemaClass2.genericParameters.map((parameter4) => {
46648
46750
  const constraint = parameter4.constraint ? ` extends ${parameter4.constraint.kind === "class" ? required(
@@ -51301,9 +51403,14 @@ function collectFunctions(args) {
51301
51403
  for (const action of node.actions) {
51302
51404
  if (!isObjectRecord2(action) || action.type !== 0 || !isObjectRecord2(action.logic))
51303
51405
  continue;
51304
- if (action.logic.sourceInline === true || action.logic.type === 0 && isCompleteUIAction(action.logic.action))
51305
- continue;
51306
51406
  const useId = stringField(action, "id");
51407
+ if (action.logic.sourceInline === true) continue;
51408
+ if (action.logic.type === 0) {
51409
+ if (isCompleteUIAction(action.logic.action)) continue;
51410
+ throw new Error(
51411
+ `Dialogue action "${useId}" on actions node "${nodeId}" is incomplete. Complete or remove the action in Neo Compose before pulling.`
51412
+ );
51413
+ }
51307
51414
  add(action.logic, `action:${useId}`, "void");
51308
51415
  }
51309
51416
  }
@@ -52989,6 +53096,7 @@ function lowerClass(context, declaration) {
52989
53096
  name: declaration.name,
52990
53097
  ...declaration.docsText === void 0 ? {} : { docsText: declaration.docsText },
52991
53098
  declarationModifier: declaration.modifiers.includes("abstract") ? "abstract" : "concrete",
53099
+ ...declaration.modifiers.includes("sealed") ? { isSealed: true } : {},
52992
53100
  schema,
52993
53101
  schemaKeyOrder: persistedSchemaOrder,
52994
53102
  extendsClassId,
@@ -54332,6 +54440,16 @@ function lowerExpressionValue(context, expression, declaredExpected, ownerClass,
54332
54440
  if (expression.callee.kind === "ident" && expression.callee.name === "Reference") {
54333
54441
  const index = expression.argumentNames?.findIndex((name) => name === "id") ?? -1;
54334
54442
  if (index >= 0) {
54443
+ if (expression.argumentNames?.includes("withProvenance") === true) {
54444
+ throw new Error(
54445
+ `${path} writes Reference(id: ..., withProvenance: ...) as a persisted member value. Provenance-aware references are executable NeoScript values and must be used inside a function or delegate closure.`
54446
+ );
54447
+ }
54448
+ if (expression.args.length !== 1 || expression.argumentNames?.some((name) => name !== "id")) {
54449
+ throw new Error(
54450
+ `${path} writes Reference(id: ...) with unsupported arguments.`
54451
+ );
54452
+ }
54335
54453
  const idArgument = expression.args[index];
54336
54454
  if (idArgument?.kind === "litString") return idArgument.value;
54337
54455
  throw new Error(
@@ -59911,13 +60029,14 @@ function evaluatorOwnershipDistances(rowId, indexes) {
59911
60029
  indexes.ownershipDistancesByRowId.set(rowId, distances);
59912
60030
  return distances;
59913
60031
  }
59914
- function resolveRuntimeReferenceRow(sourceValueId, ctx) {
60032
+ function resolveRuntimeReferenceRow(sourceValueId, ctx, withProvenance) {
59915
60033
  const direct = evalValueById(
59916
60034
  ctx,
59917
60035
  sourceValueId,
59918
60036
  ctx.__runtimeSessionValues,
59919
60037
  ctx.__valueOverlay
59920
60038
  );
60039
+ if (!withProvenance) return direct;
59921
60040
  const receiver = trackedRowForValueReference(ctx.thisValue, ctx);
59922
60041
  if (receiver === null) return direct;
59923
60042
  const indexes = evaluatorIndexes(ctx);
@@ -62128,7 +62247,11 @@ function evalPointer(pointer, scope, ctx) {
62128
62247
  return scope.get(pointer.variableId);
62129
62248
  }
62130
62249
  case "reference" /* reference */: {
62131
- const row = resolveRuntimeReferenceRow(pointer.valueId, ctx);
62250
+ const row = resolveRuntimeReferenceRow(
62251
+ pointer.valueId,
62252
+ ctx,
62253
+ pointer.withProvenance === true
62254
+ );
62132
62255
  if (!row) {
62133
62256
  throw new NSGetterRuntimeError(
62134
62257
  `Missing value reference: ${pointer.valueId}`
@@ -76955,6 +77078,19 @@ function readClass(value) {
76955
77078
  throw new Error(`Class "${value.id}" schema value is invalid.`);
76956
77079
  }
76957
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
+ }
76958
77094
  if (value.implementsInterfaceIds != null && (!Array.isArray(value.implementsInterfaceIds) || !value.implementsInterfaceIds.every((id2) => typeof id2 === "string"))) {
76959
77095
  throw new Error(`Class "${value.id}" implementsInterfaceIds is invalid.`);
76960
77096
  }
@@ -77227,6 +77363,14 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
77227
77363
  const classesById = new Map(
77228
77364
  classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
77229
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
+ }
77230
77374
  const genericParamConstraintsById = new Map(
77231
77375
  classes.flatMap(
77232
77376
  (schemaClass2) => (schemaClass2.genericParams ?? []).map(
@@ -104469,8 +104613,8 @@ var init_registry2 = __esm({
104469
104613
  "use strict";
104470
104614
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
104471
104615
  formatVersion: 3,
104472
- contractVersion: "3.10",
104473
- cliVersion: "0.28.0",
104616
+ contractVersion: "3.11",
104617
+ cliVersion: "0.30.0",
104474
104618
  projectFileUploadBatchSize: 32,
104475
104619
  documentRecords: {
104476
104620
  member: {
@@ -104565,6 +104709,7 @@ var init_registry2 = __esm({
104565
104709
  "extendsClassId",
104566
104710
  "implementsInterfaceIds",
104567
104711
  "hiddenInMemberSelector",
104712
+ "isSealed",
104568
104713
  "isAbstract",
104569
104714
  "system",
104570
104715
  "allowedStorage",
@@ -104579,6 +104724,7 @@ var init_registry2 = __esm({
104579
104724
  derived: [],
104580
104725
  volatile: ["projectId", "createdAt", "updatedAt"],
104581
104726
  normalization: {
104727
+ isSealed: "falseIsAbsent",
104582
104728
  targetMemberId: "nullIsAbsent",
104583
104729
  schemaKeyOrder: "nullIsAbsent",
104584
104730
  extendsClassId: "nullIsAbsent",
@@ -111036,7 +111182,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
111036
111182
  async function main() {
111037
111183
  const args = parseArgs(process.argv.slice(2));
111038
111184
  if (args.command === "--version") {
111039
- console.log("0.28.0");
111185
+ console.log("0.30.0");
111040
111186
  return;
111041
111187
  }
111042
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.28.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.28.0 -->
12
+ <!-- reviewed-through-cli: 0.30.0 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -109,12 +109,14 @@ override replaces the whole leaf.
109
109
  Frame overrides may change `Enabled`, `FlipX`, and `SortingOrder`; they may
110
110
  not add, remove, or reorder `Children`.
111
111
 
112
- Address direct children through selector delegates. A selector should implement
113
- the project's own stable identity contract, such as an immutable `Name`, slug,
114
- or semantic position; do not turn a mutable object-row ID back into identity by
115
- returning `Reference<T>(id: "...")`. Missing optional child slots are skipped
116
- with diagnostics when the authored slot is absent, while stale pre-provenance
117
- placements may still fail closed.
112
+ Address direct children through selector delegates. Prefer the project's own
113
+ stable identity contract, such as an immutable `Name`, slug, or semantic
114
+ position. When a selector means one specific authored child slot whose
115
+ placement clones carry `sourceValueId`, return
116
+ `Reference<T>(id: "<authored-child-id>", withProvenance: true)`. The flag is
117
+ opt-in and defaults to `false`; without it the ID is matched exactly. Missing
118
+ optional child slots are skipped with diagnostics when the authored slot is
119
+ absent, while stale pre-provenance placements may still fail closed.
118
120
 
119
121
  A placement row carries its `assetClassId` binding and optional `assetValueId`
120
122
  override beside its schema keys. They are row provenance, not declared schema:
@@ -176,11 +178,11 @@ class LegPart : NeoObject {
176
178
  ```
177
179
 
178
180
  Selectors may also be compatible inline closures. Prefer a named function when
179
- the identity rule is reused or deserves an explicit failure message. Never use
180
- a selector merely to wrap `Reference<T>(id: "...")`; that recreates the
181
- row-ID coupling selectors were introduced to remove. For new track rows, omit
182
- `@id` and let the successful push assign it. Preserve IDs already present on
183
- pulled rows.
181
+ the identity rule is reused or deserves an explicit failure message. An exact
182
+ `Reference<T>(id: "...")` recreates the row-ID coupling selectors were
183
+ introduced to remove; use `withProvenance: true` only for a deliberately
184
+ authored slot identity. For new track rows, omit `@id` and let the successful
185
+ push assign it. Preserve IDs already present on pulled rows.
184
186
 
185
187
  A selector argument such as `this.SelectPants` is evaluated in the lexical
186
188
  scope of the class that declares the clip. Here `this` is the `LegPart`, not
@@ -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.28.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.
@@ -83,6 +83,7 @@ supports:
83
83
  Reference(Assets.Capitol)
84
84
  Reference(root.Assets.Cosmetics.Pants)
85
85
  Reference<Outpost>(id: "capitol-value-id")
86
+ Reference<NeoSpriteObject>(id: "authored-child-id", withProvenance: true)
86
87
  Reference<PantsAsset>(key: "pants.long")
87
88
  Reference<PantsAsset>(key: "pants.long", index: Slug)
88
89
  Reference<Dialogue>(id: "capitol-dialogue-id")
@@ -95,14 +96,20 @@ Reference<Dialogue>(id: "capitol-dialogue-id")
95
96
  - Use the generic `id:` form when the ID is the only target information. A
96
97
  `Reference` call whose argument is neither `id:` nor `key:` is rejected by
97
98
  name rather than read as an ID.
99
+ - Inside executable NeoScript, add `withProvenance: true` when the ID names an
100
+ authored row and the reference must select its nearest `sourceValueId` clone
101
+ in the lexical receiver's ownership graph. The parameter defaults to `false`;
102
+ absent or explicit `false` keeps exact-ID behavior. This option is not a
103
+ persisted member-default form.
98
104
  - Both forms are legal in a member's declaration default, not only inside a
99
105
  value graph. A key written there resolves after every pass has run, so it may
100
106
  name a row the same push creates.
101
107
  - Keep genuine structural references ID-based when their collection has no
102
108
  stable symbol, path, or key surface. Animation track and frame-override
103
109
  targets are selectors, not structural references: encode a stable
104
- project-owned identity such as `Name` or slug in the selector instead of
105
- wrapping a row ID in `Reference<T>(id: "...")`.
110
+ project-owned identity such as `Name` or slug. The exception is an authored
111
+ child slot whose placement clones retain `sourceValueId`: select that slot
112
+ with `Reference<T>(id: "...", withProvenance: true)`.
106
113
 
107
114
  Declaration and file order do not affect resolution within one push. Cyclic
108
115
  identity references are legal because they resolve IDs rather than evaluate a