@neocompose/cli 0.55.0 → 0.56.1

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.56.1] - 2026-09-23
4
+
5
+ - Changing a member that a variant's `initialize` or `apply` closure uses, such as its storage, recompiles the closure. The closure used to keep the code compiled against the old member, so after a member changed from `.Session` to `.Writable`, a variant that writes it failed in the game with "Member '…' is not Session-owned." To repair closures an earlier push left stale, add `--force-recompile` to your next push.
6
+
7
+ ## [0.56.0] - 2026-09-23
8
+
9
+ - `@storage(allowed: .Writable)` declares a member that is always writable at runtime and persists wherever its owner persists: Save under a Save parent, Session otherwise. NeoScript can write it through any receiver, including one read from Assets, and animation clips can target it. `neo pull` writes it back in the same form. Earlier CLIs fail on a project that uses it, so upgrade before pulling.
10
+ - `@storage(allowed: .Writable)` on a class requires every placement of the class to resolve to Save or Session.
11
+ - An override below a `.Writable` member may narrow it to `.Save` or `.Session`. Declaring `.Immutable` or `.Inherit` on it fails at the member's position and names the `.Writable` ancestor. `.Writable` on a static member also fails at its position. A `readonly` member under or declaring `.Writable` fails with the existing "Read-only field … requires resolved @storage(allowed: .Immutable)" error.
12
+
3
13
  ## [0.55.0] - 2026-09-23
4
14
 
5
15
  - `@settings` before a `List` element or `Dictionary` value type configures the collection's entries, such as `List<@settings(format: .Plain) string>` or `Dictionary<string, @settings(min: 0) int>`. `neo pull` writes entry settings in this form. Switching entries between Localized and Plain converts every entry's text exactly as the same change on a string member does. Annotations on a Dictionary key, another generic's type argument, or an override's element type are reported at their position. Run `neo pull` before your first push so entry settings set in the web app appear in source.
package/dist/neo.mjs CHANGED
@@ -18769,13 +18769,15 @@ var init_project_schema_contract_generated = __esm({
18769
18769
  "Inherit",
18770
18770
  "Immutable",
18771
18771
  "Save",
18772
- "Session"
18772
+ "Session",
18773
+ "Writable"
18773
18774
  ],
18774
18775
  "NeoAllowedStorage": [
18775
18776
  "Any",
18776
18777
  "Immutable",
18777
18778
  "Save",
18778
- "Session"
18779
+ "Session",
18780
+ "Writable"
18779
18781
  ],
18780
18782
  "NeoListKind": [
18781
18783
  "Ordered",
@@ -32457,10 +32459,22 @@ function resolvedAllowedStorage(classId, environment) {
32457
32459
  }
32458
32460
  return null;
32459
32461
  }
32460
- function staticDefaultWritability(classId, environment) {
32461
- const allowed = resolvedAllowedStorage(classId, environment);
32462
+ function constraintDefaultWritability(allowed) {
32462
32463
  return allowed === "immutable" || allowed === "save" || allowed === "session" ? allowed : "session";
32463
32464
  }
32465
+ function declaredStorageWritability(storage, staticOwnerClassId, environment) {
32466
+ if (storage === "immutable" || storage === "save" || storage === "session") {
32467
+ return storage;
32468
+ }
32469
+ if (storage === "writable") return "runtime";
32470
+ if (!staticOwnerClassId) return void 0;
32471
+ return staticDefaultWritability(staticOwnerClassId, environment);
32472
+ }
32473
+ function staticDefaultWritability(classId, environment) {
32474
+ return constraintDefaultWritability(
32475
+ resolvedAllowedStorage(classId, environment)
32476
+ );
32477
+ }
32464
32478
  function createRootType(ids, environment) {
32465
32479
  const definitions = [
32466
32480
  ["Assets", ids.assets, false],
@@ -32522,7 +32536,7 @@ function effectiveMemberWritability(members, classes, roots) {
32522
32536
  seen.add(memberId);
32523
32537
  const member = memberById2.get(memberId);
32524
32538
  if (!member) return null;
32525
- if (member.storage === "immutable" || member.storage === "save" || member.storage === "session") {
32539
+ if (member.storage === "immutable" || member.storage === "save" || member.storage === "session" || member.storage === "writable") {
32526
32540
  return member.storage;
32527
32541
  }
32528
32542
  return typeof member.overrideOf === "string" ? declaredStorage(member.overrideOf, seen) : null;
@@ -32545,8 +32559,8 @@ function effectiveMemberWritability(members, classes, roots) {
32545
32559
  seen.add(currentId);
32546
32560
  const current = classById.get(currentId);
32547
32561
  if (!current) break;
32548
- if (current.allowedStorage === "immutable" || current.allowedStorage === "save" || current.allowedStorage === "session") {
32549
- return current.allowedStorage;
32562
+ if (typeof current.allowedStorage === "string") {
32563
+ return constraintDefaultWritability(current.allowedStorage);
32550
32564
  }
32551
32565
  currentId = typeof current.extendsClassId === "string" ? current.extendsClassId : null;
32552
32566
  }
@@ -32581,6 +32595,7 @@ function effectiveMemberWritability(members, classes, roots) {
32581
32595
  }
32582
32596
  const effective = /* @__PURE__ */ new Map();
32583
32597
  const anchored = /* @__PURE__ */ new Set();
32598
+ const writable = /* @__PURE__ */ new Set();
32584
32599
  const rootStorage = /* @__PURE__ */ new Map();
32585
32600
  if (typeof roots.assets === "string") {
32586
32601
  rootStorage.set(roots.assets, "immutable");
@@ -32590,16 +32605,19 @@ function effectiveMemberWritability(members, classes, roots) {
32590
32605
  rootStorage.set(roots.session, "session");
32591
32606
  }
32592
32607
  for (const [memberId, member] of memberById2) {
32593
- const root = rootStorage.get(memberId);
32594
- const declared = declaredStorage(memberId);
32595
- let storage = root ?? declared;
32596
- if (storage === void 0 || storage === null) {
32608
+ const declared = rootStorage.get(memberId) ?? declaredStorage(memberId);
32609
+ if (declared === "writable") {
32610
+ writable.add(memberId);
32611
+ continue;
32612
+ }
32613
+ let storage = declared;
32614
+ if (storage === null) {
32597
32615
  const owner = optionalRecord(member.owner);
32598
32616
  if (member.modifier === "static" && owner?.kind === "classMember" && typeof owner.classId === "string") {
32599
32617
  storage = staticDefault(owner.classId);
32600
32618
  }
32601
32619
  }
32602
- if (storage === void 0 || storage === null) continue;
32620
+ if (storage === null) continue;
32603
32621
  effective.set(memberId, /* @__PURE__ */ new Set([storage]));
32604
32622
  anchored.add(memberId);
32605
32623
  }
@@ -32609,8 +32627,11 @@ function effectiveMemberWritability(members, classes, roots) {
32609
32627
  for (const memberId of memberById2.keys()) {
32610
32628
  if (anchored.has(memberId)) continue;
32611
32629
  const next = new Set(effective.get(memberId) ?? []);
32630
+ const isWritable = writable.has(memberId);
32612
32631
  for (const parentId of parents.get(memberId) ?? []) {
32613
- for (const storage of effective.get(parentId) ?? []) next.add(storage);
32632
+ for (const storage of effective.get(parentId) ?? []) {
32633
+ next.add(isWritable && storage === "immutable" ? "session" : storage);
32634
+ }
32614
32635
  }
32615
32636
  const current = effective.get(memberId);
32616
32637
  if (current?.size === next.size || next.size === 0) continue;
@@ -32741,7 +32762,16 @@ function memberSymbol(member, schemaKey, environment, field) {
32741
32762
  }
32742
32763
  const writable = isReadOnly ? false : kind === "computed" ? computedHasSetter(member, field) : true;
32743
32764
  const storage = typeof member.storage === "string" ? member.storage : void 0;
32744
- const writability = isReadOnly ? "readOnly" : kind === "computed" ? writable ? "setter" : "readOnly" : !writable ? "readOnly" : storage === "immutable" || storage === "save" || storage === "session" ? storage : isStatic && ownerClassId ? staticDefaultWritability(ownerClassId, environment) : void 0;
32765
+ let writability;
32766
+ if (!writable) writability = "readOnly";
32767
+ else if (kind === "computed") writability = "setter";
32768
+ else {
32769
+ writability = declaredStorageWritability(
32770
+ storage,
32771
+ isStatic ? ownerClassId : void 0,
32772
+ environment
32773
+ );
32774
+ }
32745
32775
  const lookupCollectionMemberId = kind === "lookup" ? string2(member.collectionMemberId, `${field}.collectionMemberId`) : null;
32746
32776
  const lookupCollectionWritability = lookupCollectionMemberId === null ? void 0 : environment.effectiveWritabilityByMemberId.get(
32747
32777
  lookupCollectionMemberId
@@ -40103,7 +40133,13 @@ var init_codecs = __esm({
40103
40133
  };
40104
40134
  MEMBER_MODIFIERS = ["virtual", "sealed", "abstract", "static"];
40105
40135
  MEMBER_ACCESS = ["public", "protected", "private"];
40106
- MEMBER_STORAGE = ["inherit", "immutable", "save", "session"];
40136
+ MEMBER_STORAGE = [
40137
+ "inherit",
40138
+ "immutable",
40139
+ "save",
40140
+ "session",
40141
+ "writable"
40142
+ ];
40107
40143
  MEMBER_DISTRIBUTION = ["sparse", "packed"];
40108
40144
  STRING_FORMAT = ["localized", "plain"];
40109
40145
  MEMBER_SEARCH_BY = ["none", "memberKey"];
@@ -40823,7 +40859,7 @@ function schemaClassToDocument(schemaClass2) {
40823
40859
  uiVisibility: schemaClass2.hiddenInMemberSelector ? 1 : void 0,
40824
40860
  modifier: schemaClass2.declarationModifier === "abstract" ? 1 : schemaClass2.declarationModifier === "sealed" ? 2 : void 0,
40825
40861
  system: systemToDocument(schemaClass2.system),
40826
- allowedStorage: schemaClass2.allowedStorage === null ? void 0 : ["immutable", "save", "session"].indexOf(schemaClass2.allowedStorage) + 1,
40862
+ allowedStorage: schemaClass2.allowedStorage === null ? void 0 : CLASS_ALLOWED_STORAGE.indexOf(schemaClass2.allowedStorage),
40827
40863
  allowedStorageKeys: schemaClass2.allowedStorageKeys,
40828
40864
  // An unconstrained param is stored with no `constraint` key. The manifest
40829
40865
  // spells that absence `null` for a uniform in-memory shape, so emitting
@@ -40846,7 +40882,7 @@ function schemaClassToDocument(schemaClass2) {
40846
40882
  function allowedStorageFromDocument(value, record6) {
40847
40883
  const storage = ordinalValue(
40848
40884
  value ?? 0,
40849
- ["any", "immutable", "save", "session"],
40885
+ CLASS_ALLOWED_STORAGE,
40850
40886
  record6,
40851
40887
  "allowedStorage"
40852
40888
  );
@@ -40893,12 +40929,19 @@ function optionalDocsTextProperty(value, record6) {
40893
40929
  const docsText = optionalDocsText(value, record6, "docsText");
40894
40930
  return docsText === void 0 ? {} : { docsText };
40895
40931
  }
40896
- var CLASS_SCHEMA_RECORD_ADAPTER;
40932
+ var CLASS_ALLOWED_STORAGE, CLASS_SCHEMA_RECORD_ADAPTER;
40897
40933
  var init_class = __esm({
40898
40934
  "src/project-manifest/record-adapters/class.ts"() {
40899
40935
  "use strict";
40900
40936
  init_contracts();
40901
40937
  init_codecs();
40938
+ CLASS_ALLOWED_STORAGE = [
40939
+ "any",
40940
+ "immutable",
40941
+ "save",
40942
+ "session",
40943
+ "writable"
40944
+ ];
40902
40945
  CLASS_SCHEMA_RECORD_ADAPTER = {
40903
40946
  recordKind: "class",
40904
40947
  manifestCollection: "classes",
@@ -42999,7 +43042,7 @@ var init_schema_member = __esm({
42999
43042
  defaultValue: nullable(schemaDefaultValueSchema),
43000
43043
  overrideOf: nullableId2,
43001
43044
  system: nullable(schemaSystemMetadataSchema),
43002
- storage: nullable(picklist(["immutable", "save", "session"])),
43045
+ storage: nullable(picklist(["immutable", "save", "session", "writable"])),
43003
43046
  distribution: nullable(picklist(["sparse", "packed"])),
43004
43047
  storageKey: nullableId2
43005
43048
  };
@@ -44665,7 +44708,7 @@ function booleanAt(value, path) {
44665
44708
  }
44666
44709
  function nullableStorage(value, path) {
44667
44710
  if (value === null) return;
44668
- stringIn(value, /* @__PURE__ */ new Set(["immutable", "save", "session"]), path);
44711
+ stringIn(value, /* @__PURE__ */ new Set(["immutable", "save", "session", "writable"]), path);
44669
44712
  }
44670
44713
  function stringIn(value, allowed, path) {
44671
44714
  const result = stringAt(value, path);
@@ -45434,15 +45477,59 @@ var init_class_generics = __esm({
45434
45477
 
45435
45478
  // ../src/models/members/member-storage.ts
45436
45479
  function isMemberStorageKind(value) {
45437
- return value === 0 /* Inherit */ || value === 1 /* Immutable */ || value === 2 /* Save */ || value === 3 /* Session */;
45480
+ return value === 0 /* Inherit */ || value === 1 /* Immutable */ || value === 2 /* Save */ || value === 3 /* Session */ || value === 4 /* Writable */;
45438
45481
  }
45439
45482
  function isEffectiveMemberStorage(value) {
45483
+ return value === 1 /* Immutable */ || value === 2 /* Save */ || value === 3 /* Session */;
45484
+ }
45485
+ function isClassAllowedStorage(value) {
45440
45486
  if (!isMemberStorageKind(value)) return false;
45441
45487
  return value !== 0 /* Inherit */;
45442
45488
  }
45489
+ function childEffectiveStorage(declared, parent) {
45490
+ switch (declared) {
45491
+ case 0 /* Inherit */:
45492
+ return parent;
45493
+ case 4 /* Writable */:
45494
+ return parent === 2 /* Save */ ? 2 /* Save */ : 3 /* Session */;
45495
+ default:
45496
+ return declared;
45497
+ }
45498
+ }
45499
+ function constraintDefaultStorage(constraint) {
45500
+ if (constraint === null) return 3 /* Session */;
45501
+ if (constraint === 4 /* Writable */) {
45502
+ return 3 /* Session */;
45503
+ }
45504
+ return constraint;
45505
+ }
45506
+ function createWritableAncestorLookup(memberById2) {
45507
+ const memo = /* @__PURE__ */ new Map();
45508
+ const lookup = (member) => {
45509
+ const cached = memo.get(member.id);
45510
+ if (cached !== void 0) return cached;
45511
+ memo.set(member.id, null);
45512
+ const baseId = member.extendsMemberId;
45513
+ const base = baseId === void 0 ? void 0 : memberById2(baseId);
45514
+ if (base === void 0) return null;
45515
+ const result = base.storage === 4 /* Writable */ ? base : lookup(base);
45516
+ memo.set(member.id, result);
45517
+ return result;
45518
+ };
45519
+ return lookup;
45520
+ }
45521
+ var MemberStorageKind;
45443
45522
  var init_member_storage = __esm({
45444
45523
  "../src/models/members/member-storage.ts"() {
45445
45524
  "use strict";
45525
+ MemberStorageKind = /* @__PURE__ */ ((MemberStorageKind3) => {
45526
+ MemberStorageKind3[MemberStorageKind3["Inherit"] = 0] = "Inherit";
45527
+ MemberStorageKind3[MemberStorageKind3["Immutable"] = 1] = "Immutable";
45528
+ MemberStorageKind3[MemberStorageKind3["Save"] = 2] = "Save";
45529
+ MemberStorageKind3[MemberStorageKind3["Session"] = 3] = "Session";
45530
+ MemberStorageKind3[MemberStorageKind3["Writable"] = 4] = "Writable";
45531
+ return MemberStorageKind3;
45532
+ })(MemberStorageKind || {});
45446
45533
  }
45447
45534
  });
45448
45535
 
@@ -47419,7 +47506,7 @@ function isNeoSchemaClassBase(value) {
47419
47506
  const v = value;
47420
47507
  return typeof v?.name === "string" && isValidDocsText(v.docsText) && isClassSchema(v?.schema) && isOptionalSchemaKeyOrder(v?.schemaKeyOrder) && (v.implementsInterfaceIds === void 0 || Array.isArray(v.implementsInterfaceIds) && v.implementsInterfaceIds.every(
47421
47508
  (interfaceId) => typeof interfaceId === "string" && interfaceId.length > 0
47422
- )) && (v.uiVisibility === void 0 || v.uiVisibility === 0 /* Visible */ || v.uiVisibility === 1 /* Hidden */) && (v.modifier === void 0 || v.modifier === 0 /* Open */ || v.modifier === 1 /* Abstract */ || v.modifier === 2 /* Sealed */) && (v.allowedStorage === void 0 || isEffectiveMemberStorage(v.allowedStorage)) && (v.allowedStorageKeys === void 0 || Array.isArray(v.allowedStorageKeys) && v.allowedStorageKeys.every((key) => typeof key === "string")) && (v.genericParams === void 0 || Array.isArray(v.genericParams) && v.genericParams.every(isGenericParamDeclaration)) && (v.extendsGenericBindings === void 0 || isGenericBindingsRecord(v.extendsGenericBindings)) && (v.constructorIds === void 0 || Array.isArray(v.constructorIds) && v.constructorIds.every(
47509
+ )) && (v.uiVisibility === void 0 || v.uiVisibility === 0 /* Visible */ || v.uiVisibility === 1 /* Hidden */) && (v.modifier === void 0 || v.modifier === 0 /* Open */ || v.modifier === 1 /* Abstract */ || v.modifier === 2 /* Sealed */) && (v.allowedStorage === void 0 || isClassAllowedStorage(v.allowedStorage)) && (v.allowedStorageKeys === void 0 || Array.isArray(v.allowedStorageKeys) && v.allowedStorageKeys.every((key) => typeof key === "string")) && (v.genericParams === void 0 || Array.isArray(v.genericParams) && v.genericParams.every(isGenericParamDeclaration)) && (v.extendsGenericBindings === void 0 || isGenericBindingsRecord(v.extendsGenericBindings)) && (v.constructorIds === void 0 || Array.isArray(v.constructorIds) && v.constructorIds.every(
47423
47510
  (constructorId) => typeof constructorId === "string" && constructorId.length > 0
47424
47511
  ) && new Set(v.constructorIds).size === v.constructorIds.length) && (v.requiredConstructorId === void 0 || typeof v.requiredConstructorId === "string" && v.requiredConstructorId.length > 0 && (v.constructorIds ?? []).length === 0) && (v.targetMemberId === void 0 || typeof v.targetMemberId === "string" && v.targetMemberId.length > 0) && (v.system === void 0 || isSystemMetadata(v.system));
47425
47512
  }
@@ -53020,7 +53107,7 @@ function assertOwnedSchemaIsImmutable(args) {
53020
53107
  const bindingKey = `${args.memberId}\0${genericEnvironmentKey(args.genericEnv)}`;
53021
53108
  if (args.visitedBindings.has(bindingKey)) return;
53022
53109
  args.visitedBindings.add(bindingKey);
53023
- if (resolved.storage === 2 /* Save */ || resolved.storage === 3 /* Session */) {
53110
+ if (resolved.storage === 2 /* Save */ || resolved.storage === 3 /* Session */ || resolved.storage === 4 /* Writable */) {
53024
53111
  throw new Error(
53025
53112
  `Read-only member "${args.label}" has a non-Immutable owned default child "${resolved.name}"; ${args.ownerContext}.`
53026
53113
  );
@@ -56911,6 +56998,9 @@ var init_effective_storage = __esm({
56911
56998
  schemaIndex;
56912
56999
  parentIdsByMemberId = /* @__PURE__ */ new Map();
56913
57000
  effectiveByMemberId = /* @__PURE__ */ new Map();
57001
+ writableAncestor = createWritableAncestorLookup(
57002
+ (id2) => this.schemaIndex.member(id2)
57003
+ );
56914
57004
  resolving = /* @__PURE__ */ new Set();
56915
57005
  /** Declared storage of `memberId`, resolved through the extends chain. */
56916
57006
  declaredStorage(memberId) {
@@ -56919,6 +57009,15 @@ var init_effective_storage = __esm({
56919
57009
  this.schemaIndex.membersById
56920
57010
  );
56921
57011
  }
57012
+ /**
57013
+ * Nearest member in `memberId`'s `extendsMemberId` chain, excluding itself,
57014
+ * whose own record declares `Writable` (P93 §1.4), or null.
57015
+ */
57016
+ writableAncestorId(memberId) {
57017
+ const member = this.schemaIndex.member(memberId);
57018
+ if (member === void 0) return null;
57019
+ return this.writableAncestor(member)?.id ?? null;
57020
+ }
56922
57021
  /** Placement-parent member ids of `memberId` (empty for roots). */
56923
57022
  parentMemberIds(memberId) {
56924
57023
  if (this.ctx.rootStorage.has(memberId)) return [];
@@ -56942,21 +57041,21 @@ var init_effective_storage = __esm({
56942
57041
  result.add(rootStorage);
56943
57042
  } else {
56944
57043
  const declared = this.declaredStorage(memberId);
56945
- const member = this.schemaIndex.member(memberId);
56946
- if (isStaticMember(member) === true) {
56947
- if (declared !== 0 /* Inherit */) {
56948
- result.add(declared);
56949
- } else {
56950
- const declaringClassId = this.schemaIndex.firstSchemaPlacement(memberId)?.ownerClass.id;
56951
- const inherited = declaringClassId === void 0 ? null : resolveAllowedStorage(declaringClassId, this.ctx.classes);
56952
- result.add(inherited ?? 3 /* Session */);
56953
- }
56954
- } else if (declared !== 0 /* Inherit */) {
57044
+ if (isEffectiveMemberStorage(declared)) {
56955
57045
  result.add(declared);
57046
+ } else if (isStaticMember(this.schemaIndex.member(memberId))) {
57047
+ const declaringClassId = this.schemaIndex.firstSchemaPlacement(memberId)?.ownerClass.id;
57048
+ const constraint = declaringClassId === void 0 ? null : resolveAllowedStorage(declaringClassId, this.ctx.classes);
57049
+ result.add(
57050
+ childEffectiveStorage(
57051
+ declared,
57052
+ constraintDefaultStorage(constraint)
57053
+ )
57054
+ );
56956
57055
  } else {
56957
57056
  for (const parentId of this.parentMemberIds(memberId)) {
56958
57057
  for (const storage of this.effectiveStorages(parentId)) {
56959
- result.add(storage);
57058
+ result.add(childEffectiveStorage(declared, storage));
56960
57059
  }
56961
57060
  }
56962
57061
  }
@@ -56971,26 +57070,44 @@ var init_effective_storage = __esm({
56971
57070
  * Effective storage for one concrete placement chain, ordered root-first
56972
57071
  * (the project root at index 0, the target member last). Used when the
56973
57072
  * caller knows the exact path (NeoScript member walks, value trees).
57073
+ * Anchors at the root, the nearest concrete declaration, or a single-storage
57074
+ * static head, then folds the members below through `childEffectiveStorage`.
56974
57075
  */
56975
57076
  effectiveStorageForPath(memberIdsRootToLeaf) {
56976
- for (let i = memberIdsRootToLeaf.length - 1; i >= 0; i--) {
56977
- const memberId = memberIdsRootToLeaf[i];
56978
- const rootStorage = this.ctx.rootStorage.get(memberId);
56979
- if (rootStorage !== void 0 && i === 0) return rootStorage;
57077
+ let anchorIndex = memberIdsRootToLeaf.length - 1;
57078
+ let storage;
57079
+ for (; anchorIndex >= 0; anchorIndex--) {
57080
+ const memberId = memberIdsRootToLeaf[anchorIndex];
57081
+ if (anchorIndex === 0) storage = this.ctx.rootStorage.get(memberId);
57082
+ if (storage !== void 0) break;
56980
57083
  const declared = this.declaredStorage(memberId);
56981
- if (declared !== 0 /* Inherit */) return declared;
57084
+ if (isEffectiveMemberStorage(declared)) {
57085
+ storage = declared;
57086
+ break;
57087
+ }
57088
+ }
57089
+ if (storage === void 0) {
57090
+ anchorIndex = 0;
57091
+ storage = this.staticRootStorage(memberIdsRootToLeaf[0]);
56982
57092
  }
56983
- const rootId = memberIdsRootToLeaf[0];
56984
- const anchored = rootId === void 0 ? void 0 : this.ctx.rootStorage.get(rootId);
56985
- if (anchored !== void 0) return anchored;
57093
+ for (let i = anchorIndex + 1; i < memberIdsRootToLeaf.length; i++) {
57094
+ storage = childEffectiveStorage(
57095
+ this.declaredStorage(memberIdsRootToLeaf[i]),
57096
+ storage
57097
+ );
57098
+ }
57099
+ return storage;
57100
+ }
57101
+ /** A static chain head's storage when it resolves to exactly one. */
57102
+ staticRootStorage(rootId) {
56986
57103
  if (rootId !== void 0 && isStaticMember(this.schemaIndex.member(rootId))) {
56987
57104
  const staticStorages = this.effectiveStorages(rootId);
56988
57105
  if (staticStorages.size === 1) {
56989
- return staticStorages.values().next().value;
57106
+ for (const storage of staticStorages) return storage;
56990
57107
  }
56991
57108
  }
56992
57109
  throw new Error(
56993
- `Storage path did not resolve: no member in the chain declares a storage class and the chain head "${rootId ?? "(empty)"}" is not a stamped project root.`
57110
+ `Storage path did not resolve: no member in the chain declares an Immutable, Save, or Session storage class and the chain head "${rootId ?? "(empty)"}" is not a stamped project root.`
56994
57111
  );
56995
57112
  }
56996
57113
  /** Class's merged (inheritance-resolved) schema. */
@@ -64211,6 +64328,14 @@ function buildRootStorageMap(project) {
64211
64328
  }
64212
64329
  return map;
64213
64330
  }
64331
+ function writtenRootStorage(project, memberIds) {
64332
+ const map = /* @__PURE__ */ new Map();
64333
+ for (const root of PROJECT_ROOTS) {
64334
+ const memberId = project[root.projectField];
64335
+ if (memberIds.has(memberId)) map.set(memberId, root.storage);
64336
+ }
64337
+ return map;
64338
+ }
64214
64339
  function isProjectRootMember(project, memberId) {
64215
64340
  return PROJECT_ROOTS.some((root) => project[root.projectField] === memberId);
64216
64341
  }
@@ -64802,7 +64927,9 @@ function symbolWritability(memberId, context, staticOwnerClassId) {
64802
64927
  if (storage === 0 /* Inherit */ && staticOwnerClassId === null) {
64803
64928
  return {};
64804
64929
  }
64805
- const effectiveStorage = storage === 0 /* Inherit */ ? resolveAllowedStorage(staticOwnerClassId, context.vm.classes) ?? 3 /* Session */ : storage;
64930
+ const effectiveStorage = storage === 0 /* Inherit */ ? constraintDefaultStorage(
64931
+ resolveAllowedStorage(staticOwnerClassId, context.vm.classes)
64932
+ ) : storage;
64806
64933
  switch (effectiveStorage) {
64807
64934
  case 1 /* Immutable */:
64808
64935
  return { writability: "immutable" };
@@ -64810,6 +64937,8 @@ function symbolWritability(memberId, context, staticOwnerClassId) {
64810
64937
  return { writability: "save" };
64811
64938
  case 3 /* Session */:
64812
64939
  return { writability: "session" };
64940
+ case 4 /* Writable */:
64941
+ return { writability: "runtime" };
64813
64942
  }
64814
64943
  }
64815
64944
  function uniqueEffectiveWritability(memberId, context) {
@@ -94947,6 +95076,121 @@ var init_member_access_modifier_validation = __esm({
94947
95076
  }
94948
95077
  });
94949
95078
 
95079
+ // ../src/database/project-version-write-changes.ts
95080
+ function applyProjectVersionWriteChanges(document, changes) {
95081
+ return projectVersionDocumentWithChanges(document, changes, false);
95082
+ }
95083
+ function projectVersionDocumentWithChanges(document, changes, reuseUnchangedBuckets) {
95084
+ const projectRows = (rows, kind) => {
95085
+ const relevant = changes.filter((change) => change.recordKind === kind);
95086
+ if (reuseUnchangedBuckets && relevant.length === 0) return rows;
95087
+ return applyArrayChanges(rows, relevant);
95088
+ };
95089
+ const hasGranularDialogueRecords = document.dialogueRecords !== void 0 || changes.some((change) => change.recordKind === "dialogue");
95090
+ const hasGranularDialogueNodes = document.dialogueNodes !== void 0 || changes.some((change) => change.recordKind === "dialogue-node");
95091
+ const dialogueRecords = hasGranularDialogueRecords ? projectRows(document.dialogueRecords ?? [], "dialogue") : void 0;
95092
+ const dialogueNodes = hasGranularDialogueNodes ? projectRows(document.dialogueNodes ?? [], "dialogue-node") : void 0;
95093
+ const projected = {
95094
+ localizedTexts: projectRows(
95095
+ document.localizedTexts ?? [],
95096
+ "localized-text"
95097
+ ),
95098
+ localizationConfig: applySingletonChange(
95099
+ document.localizationConfig,
95100
+ changes,
95101
+ "localization-config"
95102
+ ),
95103
+ project: applySingletonChange(document.project, changes, "project"),
95104
+ members: projectRows(document.members, "member"),
95105
+ classes: projectRows(document.classes, "class"),
95106
+ // P43 §6. Declared constructors participate in the post-transaction
95107
+ // document so the whole-graph validator and every body compile see the
95108
+ // same constructor surface the transaction is about to persist.
95109
+ constructors: projectRows(document.constructors ?? [], "constructor"),
95110
+ // P67 §2.1. Same reason as constructors: without these buckets the
95111
+ // whole-graph validator, the variant ownership roots, and every body
95112
+ // compile would grade the transaction against the pre-transaction
95113
+ // variant set — so a newly pushed variant's closures would compile
95114
+ // against a document that does not contain it.
95115
+ variants: projectRows(document.variants ?? [], "variant"),
95116
+ variantFolders: projectRows(
95117
+ document.variantFolders ?? [],
95118
+ "variant-folder"
95119
+ ),
95120
+ values: projectRows(document.values ?? [], "value"),
95121
+ // Project-file staging finalizes pending uploads before schema
95122
+ // preparation. Defaults and authored instance graphs may reference a file
95123
+ // created by the same source transaction, so every construction pass must
95124
+ // see that finalized row in its post-write document.
95125
+ projectFiles: projectRows(document.projectFiles ?? [], "project-file"),
95126
+ // Import-setting templates are part of the same file graph. A new file
95127
+ // may select a new template in one source transaction, and default-value
95128
+ // materialization validates that relationship immediately against this
95129
+ // post-write document.
95130
+ textureTemplates: projectRows(
95131
+ document.textureTemplates ?? [],
95132
+ "unity-texture-template"
95133
+ ),
95134
+ audioClipTemplates: projectRows(
95135
+ document.audioClipTemplates ?? [],
95136
+ "unity-audio-clip-template"
95137
+ ),
95138
+ internalRecordRelations: projectRows(
95139
+ document.internalRecordRelations ?? [],
95140
+ "internal-record-relation"
95141
+ ),
95142
+ enums: projectRows(document.enums, "enum"),
95143
+ interfaces: projectRows(document.interfaces, "interface"),
95144
+ dialogues: dialogueRecords === void 0 || dialogueNodes === void 0 || dialogueRecords === document.dialogueRecords && dialogueNodes === document.dialogueNodes ? document.dialogues : materializeDialogues(
95145
+ dialogueRecords,
95146
+ dialogueNodes
95147
+ ),
95148
+ dialogueGroups: projectRows(
95149
+ document.dialogueGroups ?? [],
95150
+ "dialogue-group"
95151
+ ),
95152
+ migrations: projectRows(document.migrations ?? [], "migration")
95153
+ };
95154
+ return {
95155
+ ...document,
95156
+ ...projected,
95157
+ ...dialogueRecords === void 0 ? {} : { dialogueRecords },
95158
+ ...dialogueNodes === void 0 ? {} : { dialogueNodes }
95159
+ // The document spread above carries every bucket the caller's view declares
95160
+ // beyond the projected ones, so the result is the same view; TypeScript
95161
+ // cannot prove that of an object literal built over a type parameter.
95162
+ };
95163
+ }
95164
+ function applySingletonChange(current, changes, recordKind) {
95165
+ const change = changes.find(
95166
+ (candidate) => candidate.recordKind === recordKind
95167
+ );
95168
+ if (change === void 0) return current;
95169
+ if (change.operation === "delete" || change.nextData === void 0) {
95170
+ throw new Error(`Schema commit cannot delete the ${recordKind} record.`);
95171
+ }
95172
+ return change.nextData;
95173
+ }
95174
+ function applyArrayChanges(current, changes) {
95175
+ const byId = new Map(current.map((record6) => [record6.id, record6]));
95176
+ for (const change of changes) {
95177
+ if (change.operation === "delete") {
95178
+ byId.delete(change.recordId);
95179
+ continue;
95180
+ }
95181
+ if (change.nextData !== void 0) {
95182
+ byId.set(change.recordId, change.nextData);
95183
+ }
95184
+ }
95185
+ return [...byId.values()];
95186
+ }
95187
+ var init_project_version_write_changes = __esm({
95188
+ "../src/database/project-version-write-changes.ts"() {
95189
+ "use strict";
95190
+ init_projectDocumentDialogueMaterialization();
95191
+ }
95192
+ });
95193
+
94950
95194
  // ../src/database/member-localization-normalization.ts
94951
95195
  function planMemberLocalizationNormalization(input, now = 0) {
94952
95196
  const before = expandPackedDocumentValues(input);
@@ -95237,7 +95481,7 @@ var init_member_localization_normalization = __esm({
95237
95481
  init_constructor_argument_ownership();
95238
95482
  init_variant_value_graph();
95239
95483
  init_packed_value_write_fold();
95240
- init_project_version_schema_commit();
95484
+ init_project_version_write_changes();
95241
95485
  init_packed_value_encoding();
95242
95486
  init_inheritance();
95243
95487
  init_members();
@@ -100461,16 +100705,38 @@ function prepareServerOwnedValueInitializerBodies(args) {
100461
100705
  }
100462
100706
  }
100463
100707
  function collectDelegateSweepRows(args) {
100464
- if (args.targetIds.size === 0) return [];
100708
+ const closureRows = [];
100709
+ const inlineRows = [];
100710
+ if (args.targetIds.size === 0) return { closureRows, inlineRows };
100465
100711
  const preparedValueIds = indexChangesByRecordId(args.prepared, "value");
100466
- const sweepRows = [];
100467
100712
  for (const value of args.postDocument.values) {
100468
100713
  if (!args.targetIds.has(value.id)) continue;
100469
100714
  if (preparedValueIds.has(value.id)) continue;
100470
100715
  const row = delegateValueRow(value);
100471
- if (row !== null) sweepRows.push(row);
100716
+ if (row !== null) {
100717
+ closureRows.push(row);
100718
+ continue;
100719
+ }
100720
+ const inlineRow = inlineDelegateSweepRow(value);
100721
+ if (inlineRow !== null) inlineRows.push(inlineRow);
100472
100722
  }
100473
- return sweepRows;
100723
+ return { closureRows, inlineRows };
100724
+ }
100725
+ function inlineDelegateSweepRow(value) {
100726
+ if (!isMemberValue(value) || !isLiteralValueContent(value)) return null;
100727
+ const constructorArgs = asRecord4(
100728
+ value.constructorArgs
100729
+ );
100730
+ if (constructorArgs === null) return null;
100731
+ let sourceArgs = null;
100732
+ for (const [key, argument2] of Object.entries(constructorArgs)) {
100733
+ const closure = asRecord4(argument2);
100734
+ if (typeof closure?.code !== "string") continue;
100735
+ if (!("action" in closure)) continue;
100736
+ sourceArgs ??= { ...constructorArgs };
100737
+ sourceArgs[key] = { code: closure.code };
100738
+ }
100739
+ return sourceArgs === null ? null : { ...value, constructorArgs: sourceArgs };
100474
100740
  }
100475
100741
  function delegateValueRow(value) {
100476
100742
  if (!isMemberValue(value) || !isLiteralValueContent(value)) return null;
@@ -100617,8 +100883,8 @@ function prepareServerOwnedDelegateValueBodies(args) {
100617
100883
  const withInlineArgs = inlineDelegateArgumentRow(change.nextData);
100618
100884
  if (withInlineArgs !== null) inlineArgumentRows.set(index, withInlineArgs);
100619
100885
  }
100620
- const sweepRows = collectDelegateSweepRows(args);
100621
- if (explicitRows.size === 0 && sweepRows.length === 0 && inlineArgumentRows.size === 0) {
100886
+ const { closureRows: sweepRows, inlineRows: inlineSweepRows } = collectDelegateSweepRows(args);
100887
+ if (explicitRows.size === 0 && sweepRows.length === 0 && inlineArgumentRows.size === 0 && inlineSweepRows.length === 0) {
100622
100888
  return;
100623
100889
  }
100624
100890
  const committedDocument = applyProjectVersionWriteChanges(
@@ -100627,7 +100893,7 @@ function prepareServerOwnedDelegateValueBodies(args) {
100627
100893
  );
100628
100894
  const targetIds = valueIdsFromRows(
100629
100895
  [...explicitRows.values(), ...inlineArgumentRows.values()],
100630
- sweepRows
100896
+ [...sweepRows, ...inlineSweepRows]
100631
100897
  );
100632
100898
  const rootOwnerByValueId = /* @__PURE__ */ new Map();
100633
100899
  const delegateScope = withVariantOwnershipRoots(
@@ -100702,7 +100968,7 @@ function prepareServerOwnedDelegateValueBodies(args) {
100702
100968
  };
100703
100969
  }
100704
100970
  }
100705
- if (sweepRows.length === 0) return;
100971
+ if (sweepRows.length === 0 && inlineSweepRows.length === 0) return;
100706
100972
  const currentById = new Map(
100707
100973
  args.document.values.map((value) => [value.id, value])
100708
100974
  );
@@ -100712,40 +100978,65 @@ function prepareServerOwnedDelegateValueBodies(args) {
100712
100978
  prepared: args.prepared,
100713
100979
  contentHashHeads: args.contentHashHeads
100714
100980
  });
100981
+ const refresh = (id2, compiled) => {
100982
+ const current = currentById.get(id2);
100983
+ if (current === void 0 || canonicallyEqual2(current, compiled)) return;
100984
+ const contentHash = baseHashFor(id2);
100985
+ if (contentHash === void 0) {
100986
+ throw new Error(
100987
+ `Cannot safely refresh the compiled NeoDelegate closure for value "${id2}": its current content hash is unavailable.`
100988
+ );
100989
+ }
100990
+ args.prepared.push({
100991
+ recordKind: "value",
100992
+ recordId: id2,
100993
+ operation: "update",
100994
+ nextData: compiled,
100995
+ intent: createProjectVersionIntent("value.update", {
100996
+ source: "server-neoscript-recompile"
100997
+ }),
100998
+ ...contentHash === null ? {} : { expectedBaseContentHash: contentHash }
100999
+ });
101000
+ };
100715
101001
  for (const row of sweepRows) {
100716
101002
  const compiled = { ...row, value: { ...row.value } };
100717
101003
  try {
100718
101004
  compileOne(delegateScope.document, compiled);
100719
101005
  } catch (postWriteError) {
100720
- const current2 = delegateValueRow(
101006
+ const current = delegateValueRow(
100721
101007
  structuredClone(currentById.get(row.id))
100722
101008
  );
100723
- if (current2 === null) throw postWriteError;
101009
+ if (current === null) throw postWriteError;
100724
101010
  try {
100725
- compileOne(args.document, current2);
101011
+ compileOne(args.document, current);
100726
101012
  } catch (preWriteError) {
100727
101013
  if (sameCompileFailure(preWriteError, postWriteError)) continue;
100728
101014
  }
100729
101015
  throw postWriteError;
100730
101016
  }
100731
- const current = currentById.get(row.id);
100732
- if (current === void 0 || canonicallyEqual2(current, compiled)) continue;
100733
- const contentHash = baseHashFor(row.id);
100734
- if (contentHash === void 0) {
100735
- throw new Error(
100736
- `Cannot safely refresh the compiled NeoDelegate closure for value "${row.id}": its current content hash is unavailable.`
100737
- );
101017
+ refresh(row.id, compiled);
101018
+ }
101019
+ const compileInlineOne = (document, row) => compileInlineDelegateConstructorArguments({
101020
+ document,
101021
+ row,
101022
+ ownerMember: ownerByValueId.get(row.id),
101023
+ rootOwner: rootOwnerByValueId.get(row.id) ?? null
101024
+ });
101025
+ for (const row of inlineSweepRows) {
101026
+ let compiledArgs;
101027
+ try {
101028
+ compiledArgs = compileInlineOne(delegateScope.document, row);
101029
+ } catch (postWriteError) {
101030
+ const current = inlineDelegateSweepRow(currentById.get(row.id));
101031
+ if (current === null) throw postWriteError;
101032
+ try {
101033
+ compileInlineOne(args.document, current);
101034
+ } catch (preWriteError) {
101035
+ if (sameCompileFailure(preWriteError, postWriteError)) continue;
101036
+ }
101037
+ throw postWriteError;
100738
101038
  }
100739
- args.prepared.push({
100740
- recordKind: "value",
100741
- recordId: row.id,
100742
- operation: "update",
100743
- nextData: compiled,
100744
- intent: createProjectVersionIntent("value.update", {
100745
- source: "server-neoscript-recompile"
100746
- }),
100747
- ...contentHash === null ? {} : { expectedBaseContentHash: contentHash }
100748
- });
101039
+ refresh(row.id, { ...row, constructorArgs: compiledArgs });
100749
101040
  }
100750
101041
  }
100751
101042
  function prepareServerOwnedConstructorBodies(args) {
@@ -101000,113 +101291,6 @@ function assertUniqueChanges(changes) {
101000
101291
  }
101001
101292
  }
101002
101293
  }
101003
- function applyProjectVersionWriteChanges(document, changes) {
101004
- return projectVersionDocumentWithChanges(document, changes, false);
101005
- }
101006
- function projectVersionDocumentWithChanges(document, changes, reuseUnchangedBuckets) {
101007
- const projectRows = (rows, kind) => {
101008
- const relevant = changes.filter((change) => change.recordKind === kind);
101009
- if (reuseUnchangedBuckets && relevant.length === 0) return rows;
101010
- return applyArrayChanges(rows, relevant);
101011
- };
101012
- const hasGranularDialogueRecords = document.dialogueRecords !== void 0 || changes.some((change) => change.recordKind === "dialogue");
101013
- const hasGranularDialogueNodes = document.dialogueNodes !== void 0 || changes.some((change) => change.recordKind === "dialogue-node");
101014
- const dialogueRecords = hasGranularDialogueRecords ? projectRows(document.dialogueRecords ?? [], "dialogue") : void 0;
101015
- const dialogueNodes = hasGranularDialogueNodes ? projectRows(document.dialogueNodes ?? [], "dialogue-node") : void 0;
101016
- const projected = {
101017
- localizedTexts: projectRows(
101018
- document.localizedTexts ?? [],
101019
- "localized-text"
101020
- ),
101021
- localizationConfig: applySingletonChange(
101022
- document.localizationConfig,
101023
- changes,
101024
- "localization-config"
101025
- ),
101026
- project: applySingletonChange(document.project, changes, "project"),
101027
- members: projectRows(document.members, "member"),
101028
- classes: projectRows(document.classes, "class"),
101029
- // P43 §6. Declared constructors participate in the post-transaction
101030
- // document so the whole-graph validator and every body compile see the
101031
- // same constructor surface the transaction is about to persist.
101032
- constructors: projectRows(document.constructors ?? [], "constructor"),
101033
- // P67 §2.1. Same reason as constructors: without these buckets the
101034
- // whole-graph validator, the variant ownership roots, and every body
101035
- // compile would grade the transaction against the pre-transaction
101036
- // variant set — so a newly pushed variant's closures would compile
101037
- // against a document that does not contain it.
101038
- variants: projectRows(document.variants ?? [], "variant"),
101039
- variantFolders: projectRows(
101040
- document.variantFolders ?? [],
101041
- "variant-folder"
101042
- ),
101043
- values: projectRows(document.values ?? [], "value"),
101044
- // Project-file staging finalizes pending uploads before schema
101045
- // preparation. Defaults and authored instance graphs may reference a file
101046
- // created by the same source transaction, so every construction pass must
101047
- // see that finalized row in its post-write document.
101048
- projectFiles: projectRows(document.projectFiles ?? [], "project-file"),
101049
- // Import-setting templates are part of the same file graph. A new file
101050
- // may select a new template in one source transaction, and default-value
101051
- // materialization validates that relationship immediately against this
101052
- // post-write document.
101053
- textureTemplates: projectRows(
101054
- document.textureTemplates ?? [],
101055
- "unity-texture-template"
101056
- ),
101057
- audioClipTemplates: projectRows(
101058
- document.audioClipTemplates ?? [],
101059
- "unity-audio-clip-template"
101060
- ),
101061
- internalRecordRelations: projectRows(
101062
- document.internalRecordRelations ?? [],
101063
- "internal-record-relation"
101064
- ),
101065
- enums: projectRows(document.enums, "enum"),
101066
- interfaces: projectRows(document.interfaces, "interface"),
101067
- dialogues: dialogueRecords === void 0 || dialogueNodes === void 0 || dialogueRecords === document.dialogueRecords && dialogueNodes === document.dialogueNodes ? document.dialogues : materializeDialogues(
101068
- dialogueRecords,
101069
- dialogueNodes
101070
- ),
101071
- dialogueGroups: projectRows(
101072
- document.dialogueGroups ?? [],
101073
- "dialogue-group"
101074
- ),
101075
- migrations: projectRows(document.migrations ?? [], "migration")
101076
- };
101077
- return {
101078
- ...document,
101079
- ...projected,
101080
- ...dialogueRecords === void 0 ? {} : { dialogueRecords },
101081
- ...dialogueNodes === void 0 ? {} : { dialogueNodes }
101082
- // The document spread above carries every bucket the caller's view declares
101083
- // beyond the projected ones, so the result is the same view; TypeScript
101084
- // cannot prove that of an object literal built over a type parameter.
101085
- };
101086
- }
101087
- function applySingletonChange(current, changes, recordKind) {
101088
- const change = changes.find(
101089
- (candidate) => candidate.recordKind === recordKind
101090
- );
101091
- if (change === void 0) return current;
101092
- if (change.operation === "delete" || change.nextData === void 0) {
101093
- throw new Error(`Schema commit cannot delete the ${recordKind} record.`);
101094
- }
101095
- return change.nextData;
101096
- }
101097
- function applyArrayChanges(current, changes) {
101098
- const byId = new Map(current.map((record6) => [record6.id, record6]));
101099
- for (const change of changes) {
101100
- if (change.operation === "delete") {
101101
- byId.delete(change.recordId);
101102
- continue;
101103
- }
101104
- if (change.nextData !== void 0) {
101105
- byId.set(change.recordId, change.nextData);
101106
- }
101107
- }
101108
- return [...byId.values()];
101109
- }
101110
101294
  function asRecord4(value) {
101111
101295
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
101112
101296
  return null;
@@ -101301,7 +101485,6 @@ var init_project_version_schema_commit = __esm({
101301
101485
  init_evaluateInitializer();
101302
101486
  init_virtual_instance_graph();
101303
101487
  init_virtual_instance_values();
101304
- init_projectDocumentDialogueMaterialization();
101305
101488
  init_valueHeadPlacementGraph();
101306
101489
  init_canonical_json();
101307
101490
  init_compile_ns_property();
@@ -101326,6 +101509,7 @@ var init_project_version_schema_commit = __esm({
101326
101509
  init_project_version_relation_sweep_trigger();
101327
101510
  init_project_version_static_value_writes();
101328
101511
  init_project_version_transaction_planning();
101512
+ init_project_version_write_changes();
101329
101513
  init_value_row_owner_members();
101330
101514
  init_variant_value_graph();
101331
101515
  SERVER_COMPILATION_PROJECT_CACHE_LIMIT = 4;
@@ -101375,10 +101559,6 @@ var init_unity_static_members = __esm({
101375
101559
  });
101376
101560
 
101377
101561
  // ../src/models/animation/animation-clips.ts
101378
- function isWorldAnimationStructuralMember(memberId, members) {
101379
- const membersById = new Map(members.map((member) => [member.id, member]));
101380
- return isWorldAnimationStructuralMemberFromIndex(memberId, membersById);
101381
- }
101382
101562
  function isWorldAnimationStructuralMemberFromIndex(memberId, membersById) {
101383
101563
  const visited = /* @__PURE__ */ new Set();
101384
101564
  let currentId = memberId;
@@ -101389,6 +101569,35 @@ function isWorldAnimationStructuralMemberFromIndex(memberId, membersById) {
101389
101569
  }
101390
101570
  return false;
101391
101571
  }
101572
+ function worldOverridePathIsStructurallyEligible(memberIdsRootToLeaf, membersById) {
101573
+ for (const memberId of memberIdsRootToLeaf) {
101574
+ if (isWorldAnimationStructuralMemberFromIndex(memberId, membersById)) {
101575
+ return false;
101576
+ }
101577
+ const member = membersById.get(memberId);
101578
+ if (member === void 0) return false;
101579
+ const resolved = resolveMemberFromIndex(member, membersById);
101580
+ if (isStaticMember(resolved) || isReadOnlyMember(resolved)) return false;
101581
+ if (resolved.kind === 13 /* Function */ || resolved.kind === 23 /* NSFunction */ || resolved.kind === 10 /* NSProperty */ || resolved.kind === 6 /* List */ || resolved.kind === 5 /* Dictionary */) {
101582
+ return false;
101583
+ }
101584
+ }
101585
+ return true;
101586
+ }
101587
+ function animationTrackStorage(memberIdsRootToLeaf, membersById, childrenPrefix = []) {
101588
+ if (!worldOverridePathIsStructurallyEligible(memberIdsRootToLeaf, membersById)) {
101589
+ return null;
101590
+ }
101591
+ const path = [...childrenPrefix, ...memberIdsRootToLeaf];
101592
+ for (let index = path.length - 1; index >= 0; index -= 1) {
101593
+ const memberId = path[index];
101594
+ if (memberId === void 0) continue;
101595
+ const storage = declaredMemberStorageFromIndex(memberId, membersById);
101596
+ if (storage === 0 /* Inherit */) continue;
101597
+ return storage === 1 /* Immutable */ ? null : storage;
101598
+ }
101599
+ return null;
101600
+ }
101392
101601
  function assertAnimationClipDocumentValid(document, scope) {
101393
101602
  const context = new AnimationValidationContext(document);
101394
101603
  context.validateSegments(scope);
@@ -101416,7 +101625,6 @@ var init_animation_clips = __esm({
101416
101625
  init_member_storage();
101417
101626
  init_neoscript_types();
101418
101627
  init_neoscript_guards();
101419
- init_project_root_members();
101420
101628
  init_members();
101421
101629
  WORLD_ANIMATION_STRUCTURAL_MEMBER_ROOT_IDS = /* @__PURE__ */ new Set([
101422
101630
  WORLD_GRID_CHILDREN_MEMBER_ID,
@@ -101442,17 +101650,11 @@ var init_animation_clips = __esm({
101442
101650
  document.members.map((value) => [value.id, value])
101443
101651
  );
101444
101652
  this.valueById = new Map(document.values.map((value) => [value.id, value]));
101445
- this.storage = new EffectiveStorageResolver({
101446
- classes: document.classes,
101447
- members: document.members,
101448
- rootStorage: buildRootStorageMap(document.project)
101449
- });
101450
101653
  }
101451
101654
  document;
101452
101655
  classById;
101453
101656
  memberById;
101454
101657
  valueById;
101455
- storage;
101456
101658
  classHasWorldKind(classId, worldKind) {
101457
101659
  const visited = /* @__PURE__ */ new Set();
101458
101660
  let current = this.classById.get(classId);
@@ -101528,6 +101730,7 @@ var init_animation_clips = __esm({
101528
101730
  definitionNode: targetDefinition,
101529
101731
  memberPath: [],
101530
101732
  storagePath: [],
101733
+ childrenPrefix: [],
101531
101734
  visited: /* @__PURE__ */ new Set()
101532
101735
  });
101533
101736
  }
@@ -101612,7 +101815,8 @@ var init_animation_clips = __esm({
101612
101815
  overrideNode: overrides,
101613
101816
  definitionNode: child,
101614
101817
  memberPath: [`child:${childId}`],
101615
- storagePath: [...args.childStoragePath],
101818
+ storagePath: [],
101819
+ childrenPrefix: args.childStoragePath,
101616
101820
  visited: /* @__PURE__ */ new Set()
101617
101821
  });
101618
101822
  }
@@ -101955,12 +102159,13 @@ var init_animation_clips = __esm({
101955
102159
  `${args.label} targets "${boundChildName}.${targetEntry.schemaKey}" of kind ${MemberKind[targetMember.kind]}, but its segment plays ${MemberKind[valueBindingMember.kind]} values.`
101956
102160
  );
101957
102161
  }
101958
- if (!this.overrideLeafEligible(targetMember, [
101959
- ...args.childStoragePath,
101960
- targetMember.id
101961
- ])) {
102162
+ if (!this.overrideLeafEligible(
102163
+ targetMember,
102164
+ [targetMember.id],
102165
+ args.childStoragePath
102166
+ )) {
101962
102167
  throw new Error(
101963
- `${args.label} targets "${boundChildName}.${targetEntry.schemaKey}", which is not a writable Save/Session override leaf.`
102168
+ `${args.label} targets "${boundChildName}.${targetEntry.schemaKey}", which is not a writable Save, Session, or Writable override leaf.`
101964
102169
  );
101965
102170
  }
101966
102171
  }
@@ -102130,7 +102335,8 @@ var init_animation_clips = __esm({
102130
102335
  partial: overrideChild.value,
102131
102336
  path,
102132
102337
  schemaKey,
102133
- storagePath: args.storagePath
102338
+ storagePath: args.storagePath,
102339
+ childrenPrefix: args.childrenPrefix
102134
102340
  });
102135
102341
  continue;
102136
102342
  }
@@ -102161,9 +102367,13 @@ var init_animation_clips = __esm({
102161
102367
  });
102162
102368
  continue;
102163
102369
  }
102164
- if (!this.overrideLeafEligible(member, [...args.storagePath, member.id])) {
102370
+ if (!this.overrideLeafEligible(
102371
+ member,
102372
+ [...args.storagePath, member.id],
102373
+ args.childrenPrefix
102374
+ )) {
102165
102375
  throw new Error(
102166
- `Animation clip "${args.clipName}" frame ${args.frameIndex} member "${path.join(".")}" is not a writable Save/Session override leaf.`
102376
+ `Animation clip "${args.clipName}" frame ${args.frameIndex} member "${path.join(".")}" is not a writable Save, Session, or Writable override leaf.`
102167
102377
  );
102168
102378
  }
102169
102379
  }
@@ -102198,12 +102408,13 @@ var init_animation_clips = __esm({
102198
102408
  );
102199
102409
  }
102200
102410
  const kindLabel = MemberKind[kind];
102201
- if (!this.overrideLeafEligible(args.member, [
102202
- ...args.storagePath,
102203
- args.member.id
102204
- ])) {
102411
+ if (!this.overrideLeafEligible(
102412
+ args.member,
102413
+ [...args.storagePath, args.member.id],
102414
+ args.childrenPrefix
102415
+ )) {
102205
102416
  throw new Error(
102206
- `${prefix} ${kindLabel} member "${memberPath3}" cannot take a "${STRUCTURED_LEAF_PARTIAL_KEY}" field override; member "${args.member.name}" is not a writable Save/Session override leaf.`
102417
+ `${prefix} ${kindLabel} member "${memberPath3}" cannot take a "${STRUCTURED_LEAF_PARTIAL_KEY}" field override; member "${args.member.name}" is not a writable Save, Session, or Writable override leaf.`
102207
102418
  );
102208
102419
  }
102209
102420
  const violation = structuredLeafPartialViolation(
@@ -102244,24 +102455,9 @@ var init_animation_clips = __esm({
102244
102455
  throw new Error(`${descent}; its value is not a ${kindLabel} record.`);
102245
102456
  }
102246
102457
  }
102247
- overrideLeafEligible(member, storagePath) {
102248
- if (isReadOnlyMember(member) === true || isStaticMember(member))
102249
- return false;
102250
- if (isWorldAnimationStructuralMember(member.id, this.document.members)) {
102251
- return false;
102252
- }
102253
- if (member.kind === 13 /* Function */ || member.kind === 23 /* NSFunction */ || member.kind === 10 /* NSProperty */ || member.kind === 7 /* Class */ || member.kind === 6 /* List */ || member.kind === 5 /* Dictionary */) {
102254
- return false;
102255
- }
102256
- try {
102257
- const storage = this.storage.effectiveStorageForPath(storagePath);
102258
- return storage === 2 /* Save */ || storage === 3 /* Session */;
102259
- } catch {
102260
- const storages = this.storage.effectiveStorages(member.id);
102261
- return storages.size > 0 && [...storages].every(
102262
- (storage) => storage === 2 /* Save */ || storage === 3 /* Session */
102263
- );
102264
- }
102458
+ overrideLeafEligible(member, storagePath, childrenPrefix) {
102459
+ if (member.kind === 7 /* Class */) return false;
102460
+ return animationTrackStorage(storagePath, this.memberById, childrenPrefix) !== null;
102265
102461
  }
102266
102462
  authoredChildren(targetClassId, definitionNode) {
102267
102463
  const childEntry = mergeStoredInstanceSchema(
@@ -102285,9 +102481,8 @@ var init_animation_clips = __esm({
102285
102481
  (value) => typeof value === "string"
102286
102482
  ) : []
102287
102483
  ),
102288
- // Child values inherit storage from their concrete placement under the
102289
- // owner's resolved Children member. This mirrors the authoring panel's
102290
- // storage path instead of treating a shared child-class leaf as a root.
102484
+ // P93 §5: a child track's declaration walk continues through the
102485
+ // owner's resolved Children member, as the authoring pane's does.
102291
102486
  storagePath: [childEntry.memberId]
102292
102487
  };
102293
102488
  }
@@ -103250,7 +103445,7 @@ function readClass(value) {
103250
103445
  CLASS_VISIBILITY_KINDS,
103251
103446
  `${label} uiVisibility`
103252
103447
  );
103253
- if (value.allowedStorage != null && !isEffectiveMemberStorage(value.allowedStorage)) {
103448
+ if (value.allowedStorage != null && !isClassAllowedStorage(value.allowedStorage)) {
103254
103449
  throw new Error(`${label} allowedStorage is invalid.`);
103255
103450
  }
103256
103451
  if (value.genericParams !== void 0 && value.genericParams !== null) {
@@ -103632,6 +103827,14 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
103632
103827
  const classes = view.classes.map(readClass);
103633
103828
  const interfaces = view.interfaces.map(readInterface);
103634
103829
  const membersById = new Map(members.map((member) => [member.id, member]));
103830
+ for (const [rootId, stamped] of view.rootStorage ?? []) {
103831
+ const root = membersById.get(rootId);
103832
+ if (root === void 0 || root.storage === stamped) continue;
103833
+ const declared = root.storage === void 0 ? "none" : MemberStorageKind[root.storage];
103834
+ throw new Error(
103835
+ `Project root member "${root.name}" (${rootId}) must declare ${MemberStorageKind[stamped]} storage but declares ${declared}.`
103836
+ );
103837
+ }
103635
103838
  const classesById = new Map(
103636
103839
  classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
103637
103840
  );
@@ -103751,6 +103954,11 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
103751
103954
  `Static member "${member.name}" (${member.id}) must be a plain non-override member.`
103752
103955
  );
103753
103956
  }
103957
+ if (member.storage === 4 /* Writable */) {
103958
+ throw new Error(
103959
+ `Static member "${member.name}" (${member.id}) cannot declare Writable storage; a static member has no placement parent to resolve it against.`
103960
+ );
103961
+ }
103754
103962
  if (member.kind === 21) {
103755
103963
  throw new Error(
103756
103964
  `Generic placeholder member "${member.name}" (${member.id}) cannot be static.`
@@ -104055,6 +104263,19 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
104055
104263
  if (member.mutability === void 0) delete resolved.mutability;
104056
104264
  return resolved;
104057
104265
  };
104266
+ const writableAncestor2 = createWritableAncestorLookup(
104267
+ (id2) => membersById.get(id2)
104268
+ );
104269
+ for (const member of members) {
104270
+ if (member.storage !== 1 /* Immutable */ && member.storage !== 0 /* Inherit */) {
104271
+ continue;
104272
+ }
104273
+ const ancestor = writableAncestor2(member);
104274
+ if (ancestor === null) continue;
104275
+ throw new Error(
104276
+ `Member "${member.name}" (${member.id}) declares ${MemberStorageKind[member.storage]} storage but overrides Writable member "${ancestor.name}" (${ancestor.id}); an override of a Writable member may declare Writable, Save, or Session, or omit storage.`
104277
+ );
104278
+ }
104058
104279
  const rootAndEntryMemberIds = /* @__PURE__ */ new Set();
104059
104280
  for (const owner of members) {
104060
104281
  if (typeof owner.entryMemberId === "string") {
@@ -104196,7 +104417,7 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
104196
104417
  }
104197
104418
  }
104198
104419
  if (rawOwned.id === member.id && isAbstractMember(member)) return;
104199
- if (owned.storage === 2 /* Save */ || owned.storage === 3 /* Session */) {
104420
+ if (owned.storage === 2 /* Save */ || owned.storage === 3 /* Session */ || owned.storage === 4 /* Writable */) {
104200
104421
  throw new Error(
104201
104422
  `Read-only member "${member.name}" has a non-Immutable owned default child "${owned.name}"; ${ownerLabel}.`
104202
104423
  );
@@ -104727,7 +104948,7 @@ function assertProjectInterfaceDocumentValid(view, options = {}) {
104727
104948
  return resolved.setterCode === void 0 ? "NSProperty has no setter implementation." : null;
104728
104949
  }
104729
104950
  const storage = resolved.storage;
104730
- if (storage === 2 /* Save */ || storage === 3 /* Session */) {
104951
+ if (storage === 2 /* Save */ || storage === 3 /* Session */ || storage === 4 /* Writable */) {
104731
104952
  return null;
104732
104953
  }
104733
104954
  if (storage === 1 /* Immutable */) {
@@ -106765,6 +106986,14 @@ function assertStagedProjectInterfaceValid(projected, changes) {
106765
106986
  variants: projected.variants ?? [],
106766
106987
  variantFolders: projected.variantFolders ?? [],
106767
106988
  interfaces: projected.interfaces,
106989
+ rootStorage: writtenRootStorage(
106990
+ projected.project,
106991
+ new Set(
106992
+ changes.flatMap(
106993
+ (change) => change.recordKind === "member" ? [change.recordId] : []
106994
+ )
106995
+ )
106996
+ ),
106768
106997
  values: projected.values
106769
106998
  },
106770
106999
  {
@@ -107360,9 +107589,10 @@ var init_project_version_whole_graph_validation = __esm({
107360
107589
  init_core();
107361
107590
  init_animation_clips();
107362
107591
  init_projectInterfaceValidation();
107592
+ init_project_root_members();
107363
107593
  init_project_schema_identifier_validation();
107364
107594
  init_packed_value_write_fold();
107365
- init_project_version_schema_commit();
107595
+ init_project_version_write_changes();
107366
107596
  init_project_version_system_protection_writes();
107367
107597
  init_reserved_record_id_validation();
107368
107598
  init_members();
@@ -109487,6 +109717,7 @@ function memberCommon(context, ownerClass, declaration, id2, owner, base) {
109487
109717
  const storage = annotation(declaration.annotations, "storage");
109488
109718
  const distribution = annotation(declaration.annotations, "distribution");
109489
109719
  const inheritedId = declaration.modifiers.includes("override") || declaration.modifiers.includes("sealed") ? inheritedMemberId(context, ownerClass, declaration.name) : null;
109720
+ assertWritableStorageRules(context, ownerClass, declaration, inheritedId);
109490
109721
  return {
109491
109722
  id: id2,
109492
109723
  source: sourceIdentity2(declaration, "member", declaration.name),
@@ -111523,7 +111754,74 @@ function numberArgument(value, name) {
111523
111754
  }
111524
111755
  function storageValue(value, name) {
111525
111756
  const enumValue2 = contextualEnumArgument(value, name)?.toLowerCase();
111526
- return enumValue2 === "immutable" || enumValue2 === "save" || enumValue2 === "session" ? enumValue2 : null;
111757
+ return enumValue2 === "immutable" || enumValue2 === "save" || enumValue2 === "session" || enumValue2 === "writable" ? enumValue2 : null;
111758
+ }
111759
+ function assertWritableStorageRules(context, ownerClass, declaration, inheritedId) {
111760
+ const site = `${ownerClass.name}.${declaration.name}`;
111761
+ const storage = annotation(declaration.annotations, "storage");
111762
+ const declared = storage ? contextualEnumArgument(storage, "allowed")?.toLowerCase() : void 0;
111763
+ if (declared === "writable" && declaration.modifiers.includes("static")) {
111764
+ throw new Error(
111765
+ `Static member ${site} cannot declare Writable storage: a static member has no parent to resolve it against. Declare Save or Session.`
111766
+ );
111767
+ }
111768
+ if (inheritedId === null) return;
111769
+ const ancestor = writableAncestor(context, inheritedId);
111770
+ if (ancestor === null) return;
111771
+ if (declared === "immutable") {
111772
+ throw new Error(
111773
+ `${site} cannot declare Immutable storage because it overrides ${ancestor}, which declares Writable. Declare Writable, Save, or Session, or omit @storage.`
111774
+ );
111775
+ }
111776
+ if (declared === "inherit") {
111777
+ throw new Error(
111778
+ `${site} cannot declare Inherit storage because it overrides ${ancestor}, which declares Writable. Declare Writable, Save, or Session, or omit @storage.`
111779
+ );
111780
+ }
111781
+ }
111782
+ function writableAncestor(context, memberId) {
111783
+ const memo = context.writableAncestors;
111784
+ const chain = /* @__PURE__ */ new Set();
111785
+ let currentId = memberId;
111786
+ let found = null;
111787
+ while (currentId !== null) {
111788
+ const known = memo.get(currentId);
111789
+ if (known !== void 0) {
111790
+ found = known;
111791
+ break;
111792
+ }
111793
+ if (chain.has(currentId)) break;
111794
+ chain.add(currentId);
111795
+ const step = storageChainStep(context, currentId);
111796
+ if (step === null) break;
111797
+ if (step.storage === "writable") {
111798
+ found = step.label;
111799
+ break;
111800
+ }
111801
+ currentId = step.overrideOf;
111802
+ }
111803
+ for (const id2 of chain) memo.set(id2, found);
111804
+ return found;
111805
+ }
111806
+ function storageChainStep(context, memberId) {
111807
+ const sourcePlacement = context.sourceMembersById.get(memberId);
111808
+ if (sourcePlacement !== void 0) {
111809
+ const { ownerClass, member } = sourcePlacement;
111810
+ const storage = annotation(member.annotations, "storage");
111811
+ return {
111812
+ label: `${ownerClass.name}.${member.name}`,
111813
+ storage: storage ? storageValue(storage, "allowed") : null,
111814
+ overrideOf: member.modifiers.includes("override") || member.modifiers.includes("sealed") ? inheritedMemberId(context, ownerClass, member.name) : null
111815
+ };
111816
+ }
111817
+ const persisted = context.baseMembers.get(memberId);
111818
+ if (persisted === void 0) return null;
111819
+ const ownerName = persisted.owner.kind === "classMember" ? context.baseClasses.get(persisted.owner.classId)?.name : void 0;
111820
+ return {
111821
+ label: ownerName === void 0 ? persisted.name : `${ownerName}.${persisted.name}`,
111822
+ storage: persisted.storage,
111823
+ overrideOf: persisted.overrideOf
111824
+ };
111527
111825
  }
111528
111826
  function referenceIdArgument(value, name) {
111529
111827
  const raw = argument(value, name);
@@ -112503,6 +112801,7 @@ function lowerProjectSchemaV4(base, analysis, options = {}) {
112503
112801
  enumOptionsByName,
112504
112802
  loweredMembers: /* @__PURE__ */ new Map(),
112505
112803
  entryTypesByEntryId: /* @__PURE__ */ new Map(),
112804
+ writableAncestors: /* @__PURE__ */ new Map(),
112506
112805
  placements: /* @__PURE__ */ new Map(),
112507
112806
  staticValueIdsBySymbol: staticValueIdsBySymbol3,
112508
112807
  settleInitializerReferences: createSchemaInitializerReferenceSettlerV4({
@@ -139241,7 +139540,7 @@ var init_registry2 = __esm({
139241
139540
  "schema-contract/registry.mjs"() {
139242
139541
  "use strict";
139243
139542
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
139244
- cliVersion: "0.55.0",
139543
+ cliVersion: "0.56.1",
139245
139544
  projectFileUploadBatchSize: 32,
139246
139545
  documentRecords: {
139247
139546
  member: {
@@ -139583,8 +139882,8 @@ var init_registry2 = __esm({
139583
139882
  sdkDeclarations: {
139584
139883
  attributes: {
139585
139884
  enums: {
139586
- NeoStorage: ["Inherit", "Immutable", "Save", "Session"],
139587
- NeoAllowedStorage: ["Any", "Immutable", "Save", "Session"],
139885
+ NeoStorage: ["Inherit", "Immutable", "Save", "Session", "Writable"],
139886
+ NeoAllowedStorage: ["Any", "Immutable", "Save", "Session", "Writable"],
139588
139887
  NeoListKind: ["Ordered", "Unordered"],
139589
139888
  NeoDictionaryKeyKind: ["String", "Enum"],
139590
139889
  NeoClassVisibility: ["Visible", "Hidden"],
@@ -147062,7 +147361,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
147062
147361
  async function main() {
147063
147362
  const args = parseArgs(process.argv.slice(2));
147064
147363
  if (args.command === "--version") {
147065
- console.log("0.55.0");
147364
+ console.log("0.56.1");
147066
147365
  return;
147067
147366
  }
147068
147367
  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.55.0",
3
+ "version": "0.56.1",
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.55.0 -->
12
+ <!-- reviewed-through-cli: 0.56.1 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -97,7 +97,7 @@ wrappers.
97
97
  The marker near the top of `SKILL.md` must exactly match the package version:
98
98
 
99
99
  ```html
100
- <!-- reviewed-through-cli: 0.55.0 -->
100
+ <!-- reviewed-through-cli: 0.56.1 -->
101
101
  ```
102
102
 
103
103
  The quoted version above is checked too, so this instruction cannot go stale
@@ -103,6 +103,39 @@ Use context-aware typed annotations:
103
103
  Use numeric literals for numeric limits. Keep annotations separate instead of
104
104
  inventing a generic metadata bag.
105
105
 
106
+ ### Storage
107
+
108
+ A member's `@storage(allowed: ...)` is `.Immutable`, `.Save`, `.Session`, or
109
+ `.Writable`. Omit it to inherit the placement parent's storage. `.Writable` is
110
+ always writable at runtime and resolves from where the value is placed: Save
111
+ under a Save parent, Session under a Session or Immutable parent. Use it for a
112
+ member that NeoScript or an animation clip writes and whose value should persist
113
+ exactly when its owner persists. Animation clips can target `.Writable` members.
114
+
115
+ ```neo
116
+ class AnimationSpriteObject(SpriteInfo sprite) : NeoSpriteObject {
117
+ @id("69905c25-8c05-4d18-90fd-069f1526a38a")
118
+ @storage(allowed: .Writable)
119
+ public override Vector3 Size = new(1, 1, 0);
120
+ }
121
+
122
+ abstract class BodyLayerSprite(CharacterCosmeticConfig config, SpriteInfo sprite, int nearOrder, int farOrder) : AnimationSpriteObject(sprite: sprite) {
123
+ @id("e81dcef4-9ea8-4bbc-a83b-565b7cf3feff")
124
+ @storage(allowed: .Session)
125
+ public override Vector3 Size = new(2, 2, 0);
126
+ }
127
+ ```
128
+
129
+ An override below a `.Writable` member, directly or through other overrides,
130
+ may declare `.Writable`, `.Save`, or `.Session`, or omit `@storage`. It may not
131
+ declare `.Immutable` or `.Inherit`, and it may not be `readonly`. Static
132
+ members, `readonly` members, and project roots cannot declare `.Writable`.
133
+ Lowering reports each violation at the member's position.
134
+
135
+ On a class, `@storage(allowed: .Writable)` means every placement must resolve
136
+ to Save or Session. A subclass may narrow it to `.Save` or `.Session`. An
137
+ undeclared static member of such a class is Session.
138
+
106
139
  `@index(member: Field, kind: .Unique)` and
107
140
  `@column(member: Field, width: ..., visibility: ..., pin: ..., overflow: ...)`
108
141
  are repeatable List annotations. `member:` is a bare field name on the entry