@neocompose/cli 0.68.2 → 0.68.3

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.68.3] - 2026-10-05
4
+
5
+ - `neo push --dry-run` preserves valid authored UUID-v4 IDs on new nested member and dictionary values, including values named by local declarations. Previously, verification dropped the ID and failed with an internal "has no pending id assignment" error. Status and push report malformed new member-value IDs at the annotation, name the affected `Class.Member`, and suggest removing `@id` or providing an unused UUID v4 ([#1189](https://github.com/ryanbliss/neo-compose/issues/1189)).
6
+
7
+ - A stored nested class value can keep its UUID-v4 when moved between compatible empty fields on existing parents. Its old owning declaration must be removed in the same push. Unsafe ownership changes are refused with an explanation.
8
+
9
+ - Trusted verification resolves complete pending IDs inside initializer code, so `Reference<T>(key:)` can target a row created in the same push without confusing its ID with the containing value's ID.
10
+
11
+ - An override of a generic class's function passes the push-time animation check, including an override of an override, as in `class ClampedIntWatcher : MinIntWatcher` where `MinIntWatcher : ValueWatcher<int>`. Before, status warned and test and push stopped with "Prospective animation validation received an invalid member record … (\"Apply\")" in any project with animation classes.
12
+
3
13
  ## [0.68.2] - 2026-10-05
4
14
 
5
15
  - Local initializer IDs may appear above the variable declaration. Formatting and pull move inline IDs there while preserving the stored row identity. Nested IDs remain on their expressions.
package/dist/neo.mjs CHANGED
@@ -31690,7 +31690,7 @@ var init_registry = __esm({
31690
31690
  "../packages/manifest/schema-contract/registry.mjs"() {
31691
31691
  "use strict";
31692
31692
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
31693
- cliVersion: "0.68.2",
31693
+ cliVersion: "0.68.3",
31694
31694
  projectFileUploadBatchSize: 32,
31695
31695
  documentRecords: {
31696
31696
  member: {
@@ -116847,6 +116847,59 @@ var init_client_commit_preparation = __esm({
116847
116847
  }
116848
116848
  });
116849
116849
 
116850
+ // src/project-source/identity-rewriter.ts
116851
+ function createIdentityRewriter(assignments, onReplace) {
116852
+ if (assignments.size === 0) return (value) => value;
116853
+ const root = { children: /* @__PURE__ */ new Map() };
116854
+ for (const [source, replacement] of assignments) {
116855
+ let prefix = root;
116856
+ for (let index = 0; index < source.length; index += 1) {
116857
+ const character = source[index];
116858
+ let child2 = prefix.children.get(character);
116859
+ if (child2 === void 0) {
116860
+ child2 = { children: /* @__PURE__ */ new Map() };
116861
+ prefix.children.set(character, child2);
116862
+ }
116863
+ prefix = child2;
116864
+ }
116865
+ prefix.replacement = replacement;
116866
+ }
116867
+ return (value) => {
116868
+ const exact = assignments.get(value);
116869
+ if (exact !== void 0) {
116870
+ onReplace?.(value);
116871
+ return exact;
116872
+ }
116873
+ const parts = [];
116874
+ let copiedThrough = 0;
116875
+ for (let start = 0; start < value.length; start += 1) {
116876
+ let prefix = root.children.get(value[start]);
116877
+ let replacement;
116878
+ let end = start;
116879
+ for (let index = start; prefix !== void 0; index += 1) {
116880
+ if (prefix.replacement !== void 0) {
116881
+ replacement = prefix.replacement;
116882
+ end = index + 1;
116883
+ }
116884
+ prefix = prefix.children.get(value[index + 1]);
116885
+ }
116886
+ if (replacement === void 0) continue;
116887
+ onReplace?.(value.slice(start, end));
116888
+ parts.push(value.slice(copiedThrough, start), replacement);
116889
+ copiedThrough = end;
116890
+ start = end - 1;
116891
+ }
116892
+ if (copiedThrough === 0) return value;
116893
+ parts.push(value.slice(copiedThrough));
116894
+ return parts.join("");
116895
+ };
116896
+ }
116897
+ var init_identity_rewriter = __esm({
116898
+ "src/project-source/identity-rewriter.ts"() {
116899
+ "use strict";
116900
+ }
116901
+ });
116902
+
116850
116903
  // src/project-source/schema-source-validation.ts
116851
116904
  function sourceDiagnosticMapper(analysis) {
116852
116905
  const locations = /* @__PURE__ */ new Map();
@@ -124875,7 +124928,8 @@ function computeWorkspaceStatus(workspace, options) {
124875
124928
  claim.message,
124876
124929
  position.file,
124877
124930
  position.line,
124878
- position.column
124931
+ position.column,
124932
+ claim.code
124879
124933
  )
124880
124934
  );
124881
124935
  }
@@ -125305,7 +125359,7 @@ function prospectiveAnimationDocumentV4(records2) {
125305
125359
  let project;
125306
125360
  const constructors = [];
125307
125361
  const classes = [];
125308
- const members = [];
125362
+ const memberData = [];
125309
125363
  const values = [];
125310
125364
  for (const record6 of candidates) {
125311
125365
  if (record6.recordKind === "project") {
@@ -125325,16 +125379,7 @@ function prospectiveAnimationDocumentV4(records2) {
125325
125379
  }
125326
125380
  classes.push(record6.data);
125327
125381
  } else if (record6.recordKind === "member") {
125328
- const member = hydrateProspectiveMemberOverride(
125329
- record6.data,
125330
- memberRecords
125331
- );
125332
- if (!isMember(member)) {
125333
- throw new Error(
125334
- `Prospective animation validation received an invalid member record ${recordLabel(record6.data)}.`
125335
- );
125336
- }
125337
- members.push(member);
125382
+ memberData.push(record6.data);
125338
125383
  } else if (record6.recordKind === "value") {
125339
125384
  if (!isMemberValue(record6.data)) {
125340
125385
  throw new Error(
@@ -125345,6 +125390,22 @@ function prospectiveAnimationDocumentV4(records2) {
125345
125390
  }
125346
125391
  }
125347
125392
  if (project === void 0) return null;
125393
+ const members = [];
125394
+ const inheritedCallables = [];
125395
+ for (const data of memberData) {
125396
+ const member = hydrateProspectiveMemberOverride(data, memberRecords);
125397
+ if (typeof data.extendsMemberId === "string" && member.returnTypeInfo !== void 0) {
125398
+ inheritedCallables.push(member);
125399
+ } else {
125400
+ members.push(validProspectiveMember(member));
125401
+ }
125402
+ }
125403
+ const closedCallables = inheritedCallables.map(
125404
+ (member) => validProspectiveMember(
125405
+ closeInheritedCallableSignature(member, classes, members)
125406
+ )
125407
+ );
125408
+ members.push(...closedCallables);
125348
125409
  const classesById = new Map(
125349
125410
  classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
125350
125411
  );
@@ -125768,6 +125829,36 @@ function hydrateProspectiveMemberOverride(data, members) {
125768
125829
  }
125769
125830
  return hydrated;
125770
125831
  }
125832
+ function validProspectiveMember(member) {
125833
+ if (!isMember(member)) {
125834
+ throw new Error(
125835
+ `Prospective animation validation received an invalid member record ${recordLabel(member)}.`
125836
+ );
125837
+ }
125838
+ return member;
125839
+ }
125840
+ function closeInheritedCallableSignature(member, classes, members) {
125841
+ if (typeof member.id !== "string") return member;
125842
+ const env = resolveDeclaringClassGenericEnv(member.id, classes);
125843
+ if (env.size === 0) return member;
125844
+ const close = (typeInfo) => substituteInheritedCallableTypeInfo(typeInfo, env, classes, members);
125845
+ const closed = { ...member };
125846
+ if (isNSFunctionReturnTypeInfo(member.returnTypeInfo)) {
125847
+ closed.returnTypeInfo = close(member.returnTypeInfo);
125848
+ }
125849
+ if (Array.isArray(member.argumentTypes)) {
125850
+ closed.argumentTypes = member.argumentTypes.map((argument2) => {
125851
+ if (!isNSFunctionArgumentTypeInfo(argument2)) return argument2;
125852
+ const { name, defaultValue } = argument2;
125853
+ return {
125854
+ ...close(argument2),
125855
+ name,
125856
+ ...defaultValue === void 0 ? {} : { defaultValue }
125857
+ };
125858
+ });
125859
+ }
125860
+ return closed;
125861
+ }
125771
125862
  function resolveProspectiveMemberField(start, field, members) {
125772
125863
  let current = start;
125773
125864
  const visited = /* @__PURE__ */ new Set();
@@ -126149,6 +126240,8 @@ var init_workspace_status_core = __esm({
126149
126240
  init_project_documents();
126150
126241
  init_animation_clips();
126151
126242
  init_classes();
126243
+ init_generics();
126244
+ init_neoscript_guards();
126152
126245
  init_members();
126153
126246
  init_value_row_owner_members();
126154
126247
  init_constructors2();
@@ -126255,7 +126348,16 @@ ${blockingErrors.map((error) => error.message).join("\n")}` : `Trusted source lo
126255
126348
  );
126256
126349
  const usedAssignments = /* @__PURE__ */ new Set();
126257
126350
  const positionless = positionIndependentAssignments(assignments);
126258
- const rewrite = (value) => rewritePending(value, assignments, usedAssignments, positionless);
126351
+ const rewriteEmbedded = createIdentityRewriter(assignments, (id2) => {
126352
+ usedAssignments.add(id2);
126353
+ });
126354
+ const rewrite = (value) => rewritePending(
126355
+ value,
126356
+ assignments,
126357
+ usedAssignments,
126358
+ positionless,
126359
+ rewriteEmbedded
126360
+ );
126259
126361
  const expectedChanges = status.changes.map((change) => {
126260
126362
  const rewrittenData = change.nextData === void 0 ? void 0 : rewrite(change.nextData);
126261
126363
  return {
@@ -126495,7 +126597,7 @@ function relocatedAssignment(pendingId2, assignments, byPosition, used) {
126495
126597
  const relocatedVirtual = pendingVirtualValueId(relocatedRoot, sourceIdentity3);
126496
126598
  return assignments.has(relocatedVirtual) ? relocatedVirtual : null;
126497
126599
  }
126498
- function rewritePending(value, assignments, used, positionless) {
126600
+ function rewritePending(value, assignments, used, positionless, rewriteEmbedded) {
126499
126601
  if (typeof value === "string") {
126500
126602
  const exact = assignments.get(value);
126501
126603
  if (exact !== void 0) {
@@ -126518,28 +126620,23 @@ function rewritePending(value, assignments, used, positionless) {
126518
126620
  );
126519
126621
  }
126520
126622
  if (!value.includes("__pending__:")) return value;
126521
- let rewritten = value;
126522
- for (const [pendingId2, assignedId] of assignments) {
126523
- if (!rewritten.includes(pendingId2)) continue;
126524
- rewritten = rewritten.replaceAll(pendingId2, assignedId);
126525
- used.add(pendingId2);
126526
- }
126527
- return rewritten;
126623
+ return rewriteEmbedded(value);
126528
126624
  }
126529
126625
  if (Array.isArray(value)) {
126530
126626
  return value.map(
126531
- (entry) => rewritePending(entry, assignments, used, positionless)
126627
+ (entry) => rewritePending(entry, assignments, used, positionless, rewriteEmbedded)
126532
126628
  );
126533
126629
  }
126534
126630
  if (isObjectRecord2(value)) {
126535
126631
  const result = {};
126536
126632
  for (const [key, entry] of Object.entries(value)) {
126537
- const rewrittenKey = key.startsWith("__pending__:") ? rewritePending(key, assignments, used, positionless) : key;
126633
+ const rewrittenKey = key.startsWith("__pending__:") ? rewritePending(key, assignments, used, positionless, rewriteEmbedded) : key;
126538
126634
  result[rewrittenKey] = rewritePending(
126539
126635
  entry,
126540
126636
  assignments,
126541
126637
  used,
126542
- positionless
126638
+ positionless,
126639
+ rewriteEmbedded
126543
126640
  );
126544
126641
  }
126545
126642
  return result;
@@ -126775,6 +126872,7 @@ var ProjectSourceCommitVerificationError, ABSENT_DIFFERENCE_VALUE;
126775
126872
  var init_trusted_commit_verification = __esm({
126776
126873
  "src/project-source/trusted-commit-verification.ts"() {
126777
126874
  "use strict";
126875
+ init_identity_rewriter();
126778
126876
  init_schema_source_validation();
126779
126877
  init_projectRecordBuckets();
126780
126878
  init_workspace_status_core();
@@ -128342,6 +128440,8 @@ function createValueLowerRegistryV4(options = {}) {
128342
128440
  initAuthoredRowIds: /* @__PURE__ */ new Map(),
128343
128441
  loweringFailures: [],
128344
128442
  storedRowClaims: /* @__PURE__ */ new Map(),
128443
+ movedValueSites: /* @__PURE__ */ new Map(),
128444
+ storedMoveRoots: /* @__PURE__ */ new Map(),
128345
128445
  placedStoredRowIds: /* @__PURE__ */ new Set(),
128346
128446
  pendingValueIdentitySites: /* @__PURE__ */ new Map(),
128347
128447
  initializerSites: /* @__PURE__ */ new Map(),
@@ -128468,6 +128568,7 @@ function buildValueLowerContext(state, manifest, options = {}) {
128468
128568
  localizationConversionValueIds: registry.localizationConversionValueIds,
128469
128569
  loweredMemberIdByValueId: /* @__PURE__ */ new Map(),
128470
128570
  storedClassSchemaMemberIds: /* @__PURE__ */ new Map(),
128571
+ ordinaryMoveFields: /* @__PURE__ */ new Map(),
128471
128572
  pendingBindingMembersByClassId: registry.pendingBindingMembersByClassId,
128472
128573
  declaredConstructors: declaredConstructorIndex,
128473
128574
  initAuthoredRowIds: seedInitAuthoredRowIds(
@@ -128481,6 +128582,8 @@ function buildValueLowerContext(state, manifest, options = {}) {
128481
128582
  referenceObligations: registry.referenceObligations,
128482
128583
  loweringFailures: registry.loweringFailures,
128483
128584
  storedRowClaims: registry.storedRowClaims,
128585
+ movedValueSites: registry.movedValueSites,
128586
+ storedMoveRoots: registry.storedMoveRoots,
128484
128587
  placedStoredRowIds: registry.placedStoredRowIds,
128485
128588
  pendingValueIdentitySites: registry.pendingValueIdentitySites,
128486
128589
  initializerSites: registry.initializerSites,
@@ -129131,6 +129234,21 @@ function drainValueReferenceObligationsV4(state, manifest, options) {
129131
129234
  })
129132
129235
  });
129133
129236
  const failures = [];
129237
+ const movedRoots = /* @__PURE__ */ new Map();
129238
+ for (const [valueId, move] of options.registry.movedValueSites) {
129239
+ const placements = context.valuePlacements.placementsByValueId.get(valueId) ?? [];
129240
+ let message;
129241
+ if (placements.length !== 1) {
129242
+ message = `Value @id(${JSON.stringify(valueId)}) must have exactly one owning slot after a move; found ${placements.length}. Remove its declaration from the old parent before keeping the same ID on the new owner.`;
129243
+ } else if (singleSlotBindingRoot(context, valueId, movedRoots) !== move.rootValueId) {
129244
+ message = `Value @id(${JSON.stringify(valueId)}) cannot move: moves require one acyclic path under the same root or static binding, through non-overridden class fields with inherited storage and automatic distribution.`;
129245
+ } else continue;
129246
+ failures.push({
129247
+ code: "invalid-value-move",
129248
+ message,
129249
+ ...composeReferenceSitePosition(move.site, options.sourceTextByUri)
129250
+ });
129251
+ }
129134
129252
  for (const recorded of options.registry.loweringFailures) {
129135
129253
  failures.push({
129136
129254
  message: recorded.message,
@@ -129730,27 +129848,17 @@ function lowerRowBackedDefault(context, member, backed, baseData3, binding) {
129730
129848
  const childValueMember = recursivePartialMember(member, childMember);
129731
129849
  const childSlice = assignmentSlices.get(assignment.name);
129732
129850
  const existingId = isObjectRecord2(baseBody) ? baseBody[assignment.name] : void 0;
129733
- const annotatedId = authoredRowId(context, assignment.value, binding);
129734
- const storedId = typeof existingId === "string" ? existingId : annotatedId;
129735
- if (storedId !== null && storedId !== void 0) {
129736
- if (typeof existingId === "string" || context.state[`value:${storedId}`] !== void 0) {
129737
- body[assignment.name] = lowerValueRow(
129738
- context,
129739
- childValueMember,
129740
- assignment.value,
129741
- storedId,
129742
- binding,
129743
- environment,
129744
- childSlice
129745
- );
129746
- continue;
129747
- }
129748
- if (!isPendingId(storedId) && !isUuidV4Id(storedId)) {
129749
- context.loweringFailures.push({
129750
- message: `Default value ${binding.label}.${assignment.name} is annotated @id("${storedId}"), which names no existing row, and a new row's durable identity must be a UUID v4. Fix the id if it meant an existing row, or drop the @id to mint a fresh identity.`,
129751
- site: referenceSite(binding, assignment.value)
129752
- });
129753
- }
129851
+ if (typeof existingId === "string") {
129852
+ body[assignment.name] = lowerValueRow(
129853
+ context,
129854
+ childValueMember,
129855
+ assignment.value,
129856
+ existingId,
129857
+ binding,
129858
+ environment,
129859
+ childSlice
129860
+ );
129861
+ continue;
129754
129862
  }
129755
129863
  const placedId = sourceValueSymbol(context, assignment.value);
129756
129864
  if (placedId !== null) {
@@ -129782,6 +129890,7 @@ function lowerRowBackedDefault(context, member, backed, baseData3, binding) {
129782
129890
  }
129783
129891
  const entryMember = requiredMember(context, member.entryMemberId);
129784
129892
  const existingIds = new Set(backed.body);
129893
+ const placedIds = /* @__PURE__ */ new Set();
129785
129894
  const entrySlices = topLevelEntrySlices(binding.initializer, "[", "]");
129786
129895
  const ids = expression.elements.map((element, index) => {
129787
129896
  const symbolId = sourceValueSymbol(context, element);
@@ -129812,6 +129921,21 @@ function lowerRowBackedDefault(context, member, backed, baseData3, binding) {
129812
129921
  entrySlices[index]
129813
129922
  );
129814
129923
  }
129924
+ if (!existingIds.has(itemId)) {
129925
+ context.storedRowClaims.set(itemId, {
129926
+ code: "invalid-value-move",
129927
+ message: `Value @id(${JSON.stringify(itemId)}) cannot move into another default list. Remove @id to create a new value.`,
129928
+ site: referenceSite(binding, element)
129929
+ });
129930
+ return itemId;
129931
+ }
129932
+ recordRepeatedRow(
129933
+ context,
129934
+ itemId,
129935
+ `Default list ${binding.label}`,
129936
+ referenceSite(binding, element),
129937
+ placedIds
129938
+ );
129815
129939
  return lowerValueRow(
129816
129940
  context,
129817
129941
  entryMember,
@@ -129832,6 +129956,8 @@ function lowerRowBackedDefault(context, member, backed, baseData3, binding) {
129832
129956
  const keyEnum = dictionaryKeyEnum(context, member);
129833
129957
  const baseBody = backed.body;
129834
129958
  const body = {};
129959
+ const existingIds = new Set(Object.values(baseBody));
129960
+ const placedIds = /* @__PURE__ */ new Set();
129835
129961
  const entrySlices = topLevelEntrySlices(binding.initializer, "{", "}");
129836
129962
  let entryIndex = 0;
129837
129963
  for (const entry of expression.entries) {
@@ -129843,6 +129969,23 @@ function lowerRowBackedDefault(context, member, backed, baseData3, binding) {
129843
129969
  `Default dictionary ${binding.label}[${JSON.stringify(key)}] has no existing owned value row. Creating new default rows from source is not implemented by this slice.`
129844
129970
  );
129845
129971
  }
129972
+ if (context.state[`value:${entryId}`] !== void 0 && !existingIds.has(entryId)) {
129973
+ context.storedRowClaims.set(entryId, {
129974
+ code: "invalid-value-move",
129975
+ message: `Value @id(${JSON.stringify(entryId)}) cannot move into another default dictionary. Remove @id to create a new value.`,
129976
+ site: referenceSite(binding, entry.value)
129977
+ });
129978
+ body[key] = entryId;
129979
+ entryIndex += 1;
129980
+ continue;
129981
+ }
129982
+ recordRepeatedRow(
129983
+ context,
129984
+ entryId,
129985
+ `Default dictionary ${binding.label}`,
129986
+ referenceSite(binding, entry.value),
129987
+ placedIds
129988
+ );
129846
129989
  body[key] = lowerValueRow(
129847
129990
  context,
129848
129991
  entryMember,
@@ -130909,8 +131052,64 @@ function structuralMemberByParameterName(context, effectiveClass) {
130909
131052
  function lowerSeedChild(context, member, expression, source, path, rows, localizedTexts, containerId, environment, authoredSlice) {
130910
131053
  const symbolId = sourceValueSymbol(context, expression);
130911
131054
  if (symbolId !== null) return symbolId;
130912
- const annotatedId = authoredRowId(context, expression, source);
131055
+ const local = context.localValues.resolve(expression, source);
131056
+ const annotatedId = annotatedValue(local.expression).id;
131057
+ if (annotatedId !== null && !isPendingId(annotatedId) && context.state[`value:${annotatedId}`] === void 0 && member.owner.kind === "classMember" && !isUuidV4Id(annotatedId)) {
131058
+ const owner = context.classes.get(member.owner.classId)?.name;
131059
+ const name = owner === void 0 ? member.name : `${owner}.${member.name}`;
131060
+ context.loweringFailures.push({
131061
+ code: "invalid-new-value-id",
131062
+ message: `Value ${name} has @id(${JSON.stringify(annotatedId)}), which names no existing row. A new nested value requires a UUID v4. Remove @id to let Neo assign the value's identity, or provide a valid, unused UUID v4.`,
131063
+ site: referenceSiteAt(
131064
+ local.source,
131065
+ local.expression.kind === "annotated" ? local.expression.annotations.find((entry) => entry.name === "id")?.pos ?? local.expression.pos : local.expression.pos
131066
+ )
131067
+ });
131068
+ }
130913
131069
  const valueId = annotatedId ?? context.structuralStoredIds.get(path) ?? pendingNestedValueId(source, path);
131070
+ if (!context.structuralConstruction && annotatedId !== null && context.state[`value:${annotatedId}`] !== void 0) {
131071
+ const failure = storedClassMoveFailure(
131072
+ context,
131073
+ member,
131074
+ annotatedId,
131075
+ path,
131076
+ containerId
131077
+ );
131078
+ if (failure !== null) {
131079
+ context.storedRowClaims.set(annotatedId, {
131080
+ code: "invalid-value-move",
131081
+ message: failure,
131082
+ site: referenceSite(local.source, local.expression)
131083
+ });
131084
+ return annotatedId;
131085
+ }
131086
+ const rootValueId = singleSlotBindingRoot(
131087
+ context,
131088
+ annotatedId,
131089
+ context.storedMoveRoots
131090
+ );
131091
+ if (rootValueId === null) {
131092
+ context.storedRowClaims.set(annotatedId, {
131093
+ code: "invalid-value-move",
131094
+ message: `Value @id(${JSON.stringify(annotatedId)}) cannot move: moves require an existing path to a root or static binding through non-overridden class fields with inherited storage and automatic distribution.`,
131095
+ site: referenceSite(local.source, local.expression)
131096
+ });
131097
+ return annotatedId;
131098
+ }
131099
+ context.movedValueSites.set(annotatedId, {
131100
+ site: referenceSite(local.source, local.expression),
131101
+ rootValueId
131102
+ });
131103
+ return lowerValueRow(
131104
+ context,
131105
+ member,
131106
+ expression,
131107
+ annotatedId,
131108
+ source,
131109
+ environment,
131110
+ authoredSlice
131111
+ );
131112
+ }
130914
131113
  lowerSeedRow(
130915
131114
  context,
130916
131115
  member,
@@ -130926,6 +131125,99 @@ function lowerSeedChild(context, member, expression, source, path, rows, localiz
130926
131125
  );
130927
131126
  return valueId;
130928
131127
  }
131128
+ function singleSlotBindingRoot(context, start, roots) {
131129
+ const placements = context.valuePlacements;
131130
+ const path = /* @__PURE__ */ new Set();
131131
+ let cursor = start;
131132
+ let root = null;
131133
+ while (!path.has(cursor)) {
131134
+ const cached = roots.get(cursor);
131135
+ if (cached !== void 0) {
131136
+ root = cached;
131137
+ break;
131138
+ }
131139
+ if (placements.memberIdsByBoundValueId.has(cursor)) {
131140
+ root = cursor;
131141
+ break;
131142
+ }
131143
+ path.add(cursor);
131144
+ const parents = placements.placementsByValueId.get(cursor) ?? [];
131145
+ const parent = parents[0];
131146
+ if (parents.length !== 1 || parent === void 0) break;
131147
+ if (parent.containerClassId === null) break;
131148
+ const key = `${parent.containerClassId}:${parent.schemaKey}`;
131149
+ let ordinary = context.ordinaryMoveFields.get(key);
131150
+ if (ordinary === void 0) {
131151
+ const member = classMemberByName(
131152
+ context,
131153
+ parent.containerClassId,
131154
+ parent.schemaKey
131155
+ );
131156
+ ordinary = member.overrideOf === null && member.storage === null && member.storageKey === null && member.distribution === null;
131157
+ context.ordinaryMoveFields.set(key, ordinary);
131158
+ }
131159
+ if (!ordinary) break;
131160
+ cursor = parent.containerValueId;
131161
+ }
131162
+ for (const id2 of path) roots.set(id2, root);
131163
+ return root;
131164
+ }
131165
+ function storedClassMoveFailure(context, member, valueId, path, containerId) {
131166
+ const refuse = (reason) => `Value @id(${JSON.stringify(valueId)}) cannot move: ${reason}. Remove @id to create a new value.`;
131167
+ if (!isUuidV4Id(valueId))
131168
+ return refuse("its identity is derived from its original placement");
131169
+ if (containerId !== void 0)
131170
+ return refuse("unordered-list membership is immutable");
131171
+ if (member.owner.kind !== "classMember")
131172
+ return refuse("the new owner is not a class member");
131173
+ if (member.kind !== "class")
131174
+ return refuse("only nested class values support moves");
131175
+ const base = valueBase(context, valueId);
131176
+ if (typeof base.containerId === "string")
131177
+ return refuse("unordered-list membership is immutable");
131178
+ if ((context.valuePlacements.memberIdsByBoundValueId.get(valueId)?.length ?? 0) > 0)
131179
+ return refuse("a root or static binding owns it");
131180
+ const placement = context.readStoredValuePlacements().placementByChildValueId.get(valueId);
131181
+ if (placement === void 0)
131182
+ return refuse("its old owning class slot could not be resolved");
131183
+ if (placement.source !== "body")
131184
+ return refuse("its old owner supplies it through a constructor");
131185
+ const parent = valueBase(context, placement.parentValueId);
131186
+ const targetParentId = path.slice(0, -(member.name.length + 1));
131187
+ if (context.state[`value:${targetParentId}`] === void 0)
131188
+ return refuse("the new parent is not an existing stored class value");
131189
+ const targetParent = valueBase(context, targetParentId);
131190
+ for (const row of [base, parent, targetParent]) {
131191
+ if (typeof row.sourceValueId === "string")
131192
+ return refuse("a projected value depends on its original owner");
131193
+ if (typeof row.mapKey === "string")
131194
+ return refuse("partitioned values cannot change ownership");
131195
+ if (isObjectRecord2(row.genericBindings) && Object.keys(row.genericBindings).length > 0)
131196
+ return refuse("generic value ownership cannot be changed");
131197
+ }
131198
+ if (typeof targetParent.instanceConstructorId === "string" || isObjectRecord2(targetParent.constructorArgs) && Object.keys(targetParent.constructorArgs).length > 0)
131199
+ return refuse("the new owner has constructor creation data");
131200
+ if (typeof parent.instanceConstructorId === "string" || isObjectRecord2(parent.constructorArgs) && Object.keys(parent.constructorArgs).length > 0)
131201
+ return refuse("its old owner has constructor creation data");
131202
+ const oldMember = placement.memberId === null ? void 0 : context.members.get(placement.memberId);
131203
+ if (oldMember?.kind !== "class")
131204
+ return refuse(
131205
+ "its old owner is not a class member containing a class value"
131206
+ );
131207
+ if (oldMember.storageKey !== member.storageKey)
131208
+ return refuse("the new member declares a different storage key");
131209
+ if (oldMember.distribution !== member.distribution)
131210
+ return refuse("the new member declares a different distribution");
131211
+ if (oldMember.storage !== member.storage)
131212
+ return refuse("the new member uses different storage");
131213
+ if (oldMember.classId !== member.classId)
131214
+ return refuse("the new member declares a different class type");
131215
+ if (Object.keys(oldMember.classArguments).length > 0)
131216
+ return refuse("generic member ownership cannot be changed");
131217
+ if (Object.keys(member.classArguments).length > 0)
131218
+ return refuse("the new member declares generic arguments");
131219
+ return null;
131220
+ }
130929
131221
  function unorderedRowSubtreePath(valueId, path, containerId) {
130930
131222
  if (containerId === void 0 || containerId === null) return path;
130931
131223
  return valueId;
@@ -131174,7 +131466,8 @@ function lowerValueBody(context, member, expression, base, source, environment,
131174
131466
  expression,
131175
131467
  base,
131176
131468
  source,
131177
- environment
131469
+ environment,
131470
+ authoredSlice
131178
131471
  );
131179
131472
  default:
131180
131473
  throw new Error(
@@ -132133,9 +132426,9 @@ function lowerListValue(context, member, expression, base, source, inheritedEnvi
132133
132426
  value: member.listKind === "unordered" ? [] : ids
132134
132427
  };
132135
132428
  }
132136
- function recordRepeatedRow(context, rowId, collection, site) {
132137
- if (!context.placedStoredRowIds.has(rowId)) {
132138
- context.placedStoredRowIds.add(rowId);
132429
+ function recordRepeatedRow(context, rowId, collection, site, placedIds = context.placedStoredRowIds) {
132430
+ if (!placedIds.has(rowId)) {
132431
+ placedIds.add(rowId);
132139
132432
  return;
132140
132433
  }
132141
132434
  context.loweringFailures.push({
@@ -132177,18 +132470,20 @@ function existingListItemIds(context, member, base) {
132177
132470
  Array.isArray(base.value) ? base.value.filter((entry) => typeof entry === "string") : []
132178
132471
  );
132179
132472
  }
132180
- function lowerDictionaryValue(context, member, expression, base, source, inheritedEnvironment) {
132473
+ function lowerDictionaryValue(context, member, expression, base, source, inheritedEnvironment, authoredSlice) {
132181
132474
  if (expression.kind === "litNull")
132182
132475
  return { ...valueFileFields(base), value: null };
132183
132476
  if (expression.kind !== "litDict") {
132184
132477
  throw new Error("Dictionary values require {...}.");
132185
132478
  }
132186
132479
  const baseBody = isObjectRecord2(base.value) ? base.value : {};
132480
+ const existingIds = new Set(Object.values(baseBody));
132187
132481
  const keyEnum = dictionaryKeyEnum(context, member);
132188
132482
  const entryMember = requiredMember(context, member.entryMemberId);
132189
132483
  const environment = valueGenericEnvironment(base, inheritedEnvironment);
132190
132484
  const body = {};
132191
- for (const entry of expression.entries) {
132485
+ const entrySlices = authoredSlice === void 0 ? [] : topLevelEntrySlices(authoredSlice, "{", "}");
132486
+ for (const [entryIndex, entry] of expression.entries.entries()) {
132192
132487
  const key = lowerDictionaryKey(entry.key, keyEnum, member.name);
132193
132488
  const existingId = baseBody[key];
132194
132489
  const symbolId = sourceValueSymbol(context, entry.value);
@@ -132208,6 +132503,15 @@ function lowerDictionaryValue(context, member, expression, base, source, inherit
132208
132503
  `Dictionary ${member.name}[${JSON.stringify(key)}] has no existing row. New dictionary entries are not implemented by this slice.`
132209
132504
  );
132210
132505
  }
132506
+ if (context.state[`value:${valueId}`] !== void 0 && !existingIds.has(valueId)) {
132507
+ context.storedRowClaims.set(valueId, {
132508
+ code: "invalid-value-move",
132509
+ message: `Value @id(${JSON.stringify(valueId)}) cannot move into another dictionary. Remove @id to create a new value.`,
132510
+ site: referenceSite(source, entry.value)
132511
+ });
132512
+ body[key] = valueId;
132513
+ continue;
132514
+ }
132211
132515
  recordRepeatedRow(
132212
132516
  context,
132213
132517
  valueId,
@@ -132220,7 +132524,8 @@ function lowerDictionaryValue(context, member, expression, base, source, inherit
132220
132524
  entry.value,
132221
132525
  valueId,
132222
132526
  source,
132223
- environment
132527
+ environment,
132528
+ dictionaryValueSlice(entrySlices[entryIndex])
132224
132529
  );
132225
132530
  }
132226
132531
  return { ...valueFileFields(base), value: body };
@@ -133724,6 +134029,15 @@ function emitValue(context, member, valueId, options) {
133724
134029
  );
133725
134030
  return prefixFor(expression);
133726
134031
  }
134032
+ function needsProspectiveValueIdentity(context, valueId, parentId, key) {
134033
+ if (pendingVirtualValueIdentity(valueId) !== null) return true;
134034
+ if (context.storedValueBase === void 0 || isPendingId(valueId))
134035
+ return false;
134036
+ if (typeof parentId !== "string")
134037
+ return !context.storedValueBase.has(valueId);
134038
+ const storedBody = context.storedValueBase.get(parentId)?.value;
134039
+ return !isObjectRecord2(storedBody) || storedBody[key] !== valueId;
134040
+ }
133727
134041
  function emitValueBody(context, member, value, visited, targetTyped, environment) {
133728
134042
  const kind = numberField(member, "kind");
133729
134043
  const body = value.value;
@@ -133993,9 +134307,13 @@ function classValueBody(context, member, value, visited, targetTyped, outerEnvir
133993
134307
  `${key} = ${emitValue(context, emittedChildMember, childId, {
133994
134308
  environment,
133995
134309
  definitionSite: !context.symbolsByValueId.has(childId),
133996
- // Existing slots recover identity from storage. A newly pinned virtual
133997
- // slot must keep its derived pending identity through repeat lowering.
133998
- writeIdentity: pendingVirtualValueIdentity(childId) !== null,
134310
+ // New and moved identities must survive repeat lowering against the old placement.
134311
+ writeIdentity: needsProspectiveValueIdentity(
134312
+ context,
134313
+ childId,
134314
+ value.id,
134315
+ key
134316
+ ),
133999
134317
  visited
134000
134318
  })}`
134001
134319
  );
@@ -135273,9 +135591,14 @@ ${entries.flatMap(
135273
135591
  {
135274
135592
  environment,
135275
135593
  definitionSite: !context.symbolsByValueId.has(id2),
135276
- // Dictionary keys, like class schema keys, recover entry
135277
- // identity from the owning row. Only list entries need an id.
135278
- writeIdentity: false,
135594
+ // Existing keys recover identity from their owning row;
135595
+ // new authored identities must survive repeat lowering.
135596
+ writeIdentity: needsProspectiveValueIdentity(
135597
+ context,
135598
+ id2,
135599
+ value.id,
135600
+ key
135601
+ ),
135279
135602
  visited
135280
135603
  }
135281
135604
  )}`,
@@ -137495,55 +137818,6 @@ var init_status_output = __esm({
137495
137818
  }
137496
137819
  });
137497
137820
 
137498
- // src/project-source/identity-rewriter.ts
137499
- function createIdentityRewriter(assignments) {
137500
- if (assignments.size === 0) return (value) => value;
137501
- const root = { children: /* @__PURE__ */ new Map() };
137502
- for (const [source, replacement] of assignments) {
137503
- let prefix = root;
137504
- for (let index = 0; index < source.length; index += 1) {
137505
- const character = source[index];
137506
- let child2 = prefix.children.get(character);
137507
- if (child2 === void 0) {
137508
- child2 = { children: /* @__PURE__ */ new Map() };
137509
- prefix.children.set(character, child2);
137510
- }
137511
- prefix = child2;
137512
- }
137513
- prefix.replacement = replacement;
137514
- }
137515
- return (value) => {
137516
- const exact = assignments.get(value);
137517
- if (exact !== void 0) return exact;
137518
- const parts = [];
137519
- let copiedThrough = 0;
137520
- for (let start = 0; start < value.length; start += 1) {
137521
- let prefix = root.children.get(value[start]);
137522
- let replacement;
137523
- let end = start;
137524
- for (let index = start; prefix !== void 0; index += 1) {
137525
- if (prefix.replacement !== void 0) {
137526
- replacement = prefix.replacement;
137527
- end = index + 1;
137528
- }
137529
- prefix = prefix.children.get(value[index + 1]);
137530
- }
137531
- if (replacement === void 0) continue;
137532
- parts.push(value.slice(copiedThrough, start), replacement);
137533
- copiedThrough = end;
137534
- start = end - 1;
137535
- }
137536
- if (copiedThrough === 0) return value;
137537
- parts.push(value.slice(copiedThrough));
137538
- return parts.join("");
137539
- };
137540
- }
137541
- var init_identity_rewriter = __esm({
137542
- "src/project-source/identity-rewriter.ts"() {
137543
- "use strict";
137544
- }
137545
- });
137546
-
137547
137821
  // src/project-source/source-layout.ts
137548
137822
  import { readFileSync as readFileSync9 } from "node:fs";
137549
137823
  import { relative as relative4, sep as sep4 } from "node:path";
@@ -164838,7 +165112,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
164838
165112
  async function main() {
164839
165113
  const args = parseArgs(process.argv.slice(2));
164840
165114
  if (args.command === "--version") {
164841
- console.log("0.68.2");
165115
+ console.log("0.68.3");
164842
165116
  return;
164843
165117
  }
164844
165118
  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.68.2",
3
+ "version": "0.68.3",
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.68.2 -->
12
+ <!-- reviewed-through-cli: 0.68.3 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -63,8 +63,9 @@ opaque JSON or guessed syntax.
63
63
  values, files, or other records. `@system` is reserved for platform-owned
64
64
  system records; authoring it can make the record uneditable.
65
65
  - Preserve existing `@id` annotations through rename, reorder, and file moves.
66
- A new row cannot reuse a stored row's `@id`, including inside a new parent:
67
- author the new row without it.
66
+ A copy must get a new ID. A stored nested class value may keep its UUID-v4
67
+ when moved between compatible empty fields on existing parents, provided
68
+ its old parent no longer declares it. See the identity reference for limits.
68
69
  - Omit `@id` on new ordinary declarations and nested rows unless a same-push
69
70
  structural reference genuinely requires an explicit UUID. A successful push
70
71
  assigns the durable IDs and rewrites source.
@@ -110,7 +110,7 @@ wrappers.
110
110
  The marker near the top of `SKILL.md` must exactly match the package version:
111
111
 
112
112
  ```html
113
- <!-- reviewed-through-cli: 0.68.2 -->
113
+ <!-- reviewed-through-cli: 0.68.3 -->
114
114
  ```
115
115
 
116
116
  The quoted version above is checked too, so this instruction cannot go stale
@@ -57,9 +57,19 @@ keys as quoted IDs when their option has been deleted.
57
57
 
58
58
  ## Identity rules
59
59
 
60
- Preserve every existing ID through rename, reorder, and file moves. A new row
61
- cannot reuse a stored row's `@id`, including inside a new parent, and source
62
- cannot place one stored row in two collection positions. For a new ordinary record, omit
60
+ Preserve every existing ID through rename, reorder, and file moves. A copy
61
+ must get a new ID. A stored nested class value may keep its UUID-v4 when moved
62
+ to an empty, compatible class field on another existing parent. Remove its
63
+ old owning declaration in the same push. Both fields must declare the same
64
+ non-generic class and storage rules. The move must leave exactly one owning
65
+ path to the same root or static binding through ordinary class fields.
66
+ Both ownership paths must use inherited storage and automatic distribution;
67
+ member overrides are not supported for moves. Moving
68
+ projected values, constructor-owned slots, partitioned values, generic
69
+ instances, or unordered-list entries is refused. Moves into newly constructed
70
+ parents are also refused; omit the ID to create a new value there.
71
+
72
+ For a new ordinary record, omit
63
73
  `@id` and let a successful real push assign and insert it. Dry-run and failed pushes
64
74
  leave tracked source unchanged.
65
75
 
@@ -91,6 +101,11 @@ form, an author may assign a previously unseen UUID-v4 with `@id` to the new
91
101
  row and reference it in the same edit. Reusing an existing ID means update, not
92
102
  create. The server validates authored new IDs for UUID-v4 shape and uniqueness.
93
103
  Prefer generated IDs unless the structural reference needs an ID immediately.
104
+ Dry-run preserves a valid authored UUID-v4 ID on a new nested member or dictionary
105
+ value, including one declared as a local initializer. An unknown malformed member
106
+ value ID is an error at its annotation; remove `@id` to let push assign the ID,
107
+ or supply an unused UUID v4. This does not change the derived identity of a
108
+ root/static binding or an existing projected value.
94
109
 
95
110
  Do not invent IDs for structural rows whose identities derive from an owner
96
111
  role. Do not add a separate root/static initializer ID. A constructor argument