@neocompose/cli 0.36.3 → 0.36.5

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.36.5] - 2026-08-20
4
+
5
+ ### Fixed
6
+
7
+ - Recover malformed persisted static members during pull without weakening
8
+ authored schema validation: orphaned rows are queued for deletion and
9
+ structurally owned rows are queued for an `isStatic` correction on the next
10
+ push. Diagnostics now identify the member id, name, and inferred owner kind.
11
+
12
+ ## [0.36.4] - 2026-08-19
13
+
14
+ ### Fixed
15
+
16
+ - Keep computed static/root binding value identities derived from their member
17
+ ids, so post-push source rewriting never inserts an `@id` inside the
18
+ initializer expression.
19
+ - Validate assigned-id source rewriting during `neo push --dry-run`, and keep
20
+ materialized scalar bindings stable so a successful push is followed by a
21
+ clean `neo status` without changing the authored initializer expression.
22
+
3
23
  ## [0.36.3] - 2026-08-19
4
24
 
5
25
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -39618,9 +39618,11 @@ function assertStaticMemberInvariants(manifest) {
39618
39618
  const path = `$.members[${memberIndex}]`;
39619
39619
  const owner = requireRecord(member.owner, `${path}.owner`);
39620
39620
  if (owner.kind !== "classMember") {
39621
+ const memberName = typeof member.name === "string" ? member.name : "(unnamed)";
39622
+ const ownerKind = typeof owner.kind === "string" ? owner.kind : "unknown";
39621
39623
  invalid(
39622
39624
  `${path}.owner`,
39623
- "a static member must have exactly one direct Class owner."
39625
+ `static member ${JSON.stringify(memberName)} (${memberId}) has owner kind ${JSON.stringify(ownerKind)}; expected exactly one direct Class owner.`
39624
39626
  );
39625
39627
  }
39626
39628
  const ownerClassId = nonEmptyString(owner.classId, `${path}.owner.classId`);
@@ -41593,6 +41595,18 @@ function documentsToProjectSchemaManifest(records2, optionsOrContext = {}) {
41593
41595
  records2,
41594
41596
  options.sourceContext ?? DEFAULT_DOCUMENT_TO_MANIFEST_CONTEXT
41595
41597
  );
41598
+ let projectionContext = context;
41599
+ let recoveredMemberRecords;
41600
+ if (options.onInvalidStaticMemberOwnership !== void 0) {
41601
+ const normalizedMembers = new Map(context.normalizedMembers);
41602
+ recoveredMemberRecords = recoverInvalidStaticMemberOwnership(
41603
+ context.recordsByKind.get("member") ?? [],
41604
+ context,
41605
+ normalizedMembers,
41606
+ options.onInvalidStaticMemberOwnership
41607
+ );
41608
+ projectionContext = { ...context, normalizedMembers };
41609
+ }
41596
41610
  const collections = {
41597
41611
  classes: [],
41598
41612
  constructors: [],
@@ -41608,7 +41622,8 @@ function documentsToProjectSchemaManifest(records2, optionsOrContext = {}) {
41608
41622
  internalRecordRelations: []
41609
41623
  };
41610
41624
  for (const adapter of SCHEMA_RECORD_ADAPTERS) {
41611
- const adapterRecords = context.recordsByKind.get(adapter.recordKind) ?? [];
41625
+ const rawAdapterRecords = context.recordsByKind.get(adapter.recordKind) ?? [];
41626
+ const adapterRecords = adapter.recordKind === "member" && recoveredMemberRecords !== void 0 ? recoveredMemberRecords : rawAdapterRecords;
41612
41627
  if (adapter.cardinality === "one") {
41613
41628
  if (adapterRecords.length !== 1) {
41614
41629
  throw new Error(
@@ -41617,12 +41632,12 @@ function documentsToProjectSchemaManifest(records2, optionsOrContext = {}) {
41617
41632
  }
41618
41633
  collections[adapter.manifestCollection] = adapter.fromDocument(
41619
41634
  adapterRecords[0],
41620
- context
41635
+ projectionContext
41621
41636
  );
41622
41637
  continue;
41623
41638
  }
41624
41639
  collections[adapter.manifestCollection] = adapterRecords.map(
41625
- (record3) => adapter.fromDocument(record3, context)
41640
+ (record3) => adapter.fromDocument(record3, projectionContext)
41626
41641
  );
41627
41642
  }
41628
41643
  return parseProjectSchemaManifest({
@@ -41632,6 +41647,50 @@ function documentsToProjectSchemaManifest(records2, optionsOrContext = {}) {
41632
41647
  ...collections
41633
41648
  });
41634
41649
  }
41650
+ function recoverInvalidStaticMemberOwnership(records2, context, normalizedMembers, onRecovery) {
41651
+ const recovered = [];
41652
+ for (const record3 of records2) {
41653
+ const member = context.normalizedMembers.get(record3.recordId);
41654
+ if (member?.isStatic !== true) {
41655
+ recovered.push(record3);
41656
+ continue;
41657
+ }
41658
+ const owner = context.memberOwners.get(record3.recordId) ?? {
41659
+ kind: "loose"
41660
+ };
41661
+ if (owner.kind === "classMember") {
41662
+ recovered.push(record3);
41663
+ continue;
41664
+ }
41665
+ const ownerKind = typeof owner.kind === "string" ? owner.kind : "unknown";
41666
+ const memberName = typeof member.name === "string" ? member.name : record3.recordId;
41667
+ if (ownerKind === "loose") {
41668
+ normalizedMembers.delete(record3.recordId);
41669
+ onRecovery({
41670
+ memberId: record3.recordId,
41671
+ memberName,
41672
+ ownerKind,
41673
+ action: "delete"
41674
+ });
41675
+ continue;
41676
+ }
41677
+ onRecovery({
41678
+ memberId: record3.recordId,
41679
+ memberName,
41680
+ ownerKind,
41681
+ action: "demote"
41682
+ });
41683
+ normalizedMembers.set(record3.recordId, {
41684
+ ...member,
41685
+ isStatic: false
41686
+ });
41687
+ recovered.push({
41688
+ ...record3,
41689
+ data: { ...record3.data, isStatic: false }
41690
+ });
41691
+ }
41692
+ return recovered;
41693
+ }
41635
41694
  function projectSchemaManifestToDocuments(manifestInput, context) {
41636
41695
  const manifest = parseProjectSchemaManifest(manifestInput);
41637
41696
  const emissionContext = {
@@ -80398,6 +80457,19 @@ var init_root_source = __esm({
80398
80457
  }
80399
80458
  });
80400
80459
 
80460
+ // src/project-source/static-member-ownership-recovery.ts
80461
+ function staticMemberOwnershipRecoveryMessage(recovery) {
80462
+ const identity2 = `${JSON.stringify(recovery.memberName)} (${recovery.memberId})`;
80463
+ return recovery.action === "delete" ? `Recovered malformed static member ${identity2}: it has no Class declaration, so no source was emitted and the next push will delete the orphaned record.` : `Recovered malformed static member ${identity2}: it is owned as ${recovery.ownerKind}, so source projected it as instance metadata and the next push will correct isStatic.`;
80464
+ }
80465
+ var STATIC_MEMBER_OWNERSHIP_RECOVERY_SOURCE;
80466
+ var init_static_member_ownership_recovery = __esm({
80467
+ "src/project-source/static-member-ownership-recovery.ts"() {
80468
+ "use strict";
80469
+ STATIC_MEMBER_OWNERSHIP_RECOVERY_SOURCE = "<project>";
80470
+ }
80471
+ });
80472
+
80401
80473
  // ../src/models/animation/animation-clips.ts
80402
80474
  function isWorldAnimationStructuralMember(memberId, members) {
80403
80475
  const membersById = new Map(members.map((member) => [member.id, member]));
@@ -95029,7 +95101,13 @@ function computeWorkspaceStatus(workspace, options) {
95029
95101
  };
95030
95102
  }
95031
95103
  const baseDocuments = schemaBaseDocuments(workspace);
95032
- const baseManifest = baseDocuments.length === 0 ? void 0 : documentsToProjectSchemaManifest(baseDocuments);
95104
+ const staticMemberOwnershipRecoveries = [];
95105
+ const baseManifest = baseDocuments.length === 0 ? void 0 : documentsToProjectSchemaManifest(baseDocuments, {
95106
+ onInvalidStaticMemberOwnership: (recovery) => staticMemberOwnershipRecoveries.push(recovery)
95107
+ });
95108
+ const orphanedStaticMemberKeys = new Set(
95109
+ staticMemberOwnershipRecoveries.filter((recovery) => recovery.action === "delete").map((recovery) => recordStateKey("member", recovery.memberId))
95110
+ );
95033
95111
  let manifest;
95034
95112
  let authoredValueSeeds = /* @__PURE__ */ new Map();
95035
95113
  let staticMemberValueIds = /* @__PURE__ */ new Map();
@@ -95455,11 +95533,12 @@ function computeWorkspaceStatus(workspace, options) {
95455
95533
  }
95456
95534
  const deletedValueIds = /* @__PURE__ */ new Set();
95457
95535
  for (const [key, recordState] of Object.entries(workspace.state.records)) {
95458
- if (recordState.file === void 0) continue;
95536
+ const recordFile = recordState.file ?? (orphanedStaticMemberKeys.has(key) ? STATIC_MEMBER_OWNERSHIP_RECOVERY_SOURCE : void 0);
95537
+ if (recordFile === void 0) continue;
95459
95538
  if (reconstructed3.has(key)) continue;
95460
95539
  const fileStillBroken = parseErrors.some(
95461
- (error) => error.file === recordState.file && isBlockingSchemaSourceError(error)
95462
- ) || conflictedFiles.includes(recordState.file);
95540
+ (error) => error.file === recordFile && isBlockingSchemaSourceError(error)
95541
+ ) || conflictedFiles.includes(recordFile);
95463
95542
  if (fileStillBroken) continue;
95464
95543
  if (recordState.recordKind === "value") {
95465
95544
  deletedValueIds.add(recordState.recordId);
@@ -95468,7 +95547,7 @@ function computeWorkspaceStatus(workspace, options) {
95468
95547
  kind: "delete",
95469
95548
  recordKind: recordState.recordKind,
95470
95549
  recordId: recordState.recordId,
95471
- file: recordState.file,
95550
+ file: recordFile,
95472
95551
  baseData: recordState.data,
95473
95552
  baseContentHash: recordState.contentHash,
95474
95553
  casBaseHash: recordState.conflictServerHash !== void 0 ? recordState.conflictServerHash : recordState.contentHash
@@ -96274,6 +96353,7 @@ var init_workspace_status_core = __esm({
96274
96353
  init_value_sources();
96275
96354
  init_lower_variants();
96276
96355
  init_root_source();
96356
+ init_static_member_ownership_recovery();
96277
96357
  init_root_value_paths();
96278
96358
  init_project_documents();
96279
96359
  init_animation_clips();
@@ -99654,7 +99734,7 @@ function lowerSeedRow(context, member, sourceExpression, source, valueId, path,
99654
99734
  `Value ${path} in ${source.label} writes @id(${JSON.stringify(unattachedId)}) on part of a computed expression rather than on the row. Parenthesize the expression so the annotation names the whole row.`
99655
99735
  );
99656
99736
  }
99657
- if (annotated.id === null && isPendingId(valueId) && !context.pendingValueIdentitySites.has(valueId)) {
99737
+ if (annotated.id === null && isPendingId(valueId) && pendingMemberValueMemberId(valueId) === null && !context.pendingValueIdentitySites.has(valueId)) {
99658
99738
  context.pendingValueIdentitySites.set(valueId, {
99659
99739
  id: valueId,
99660
99740
  uri: source.source.uri,
@@ -100258,6 +100338,16 @@ function lowerValueRow(context, member, sourceExpression, expectedValueId, sourc
100258
100338
  }
100259
100339
  indexInitAuthoredRowIds(context, code, source.label);
100260
100340
  const storedConstructorArgs = isObjectRecord2(base.constructorArgs) ? base.constructorArgs : null;
100341
+ if (resolvedMember.kind !== "class" && isLiteralValueContent(base)) {
100342
+ addReconstructed(
100343
+ context,
100344
+ "value",
100345
+ expectedValueId,
100346
+ valueFileFields(base),
100347
+ source.source
100348
+ );
100349
+ return expectedValueId;
100350
+ }
100261
100351
  const materializedClass = resolvedMember.kind === "class" && isObjectRecord2(base.value);
100262
100352
  const effectiveClassId = resolvedMember.kind === "class" ? stringOrNull(base.classId) ?? resolvedMember.classId : null;
100263
100353
  const materializedPlainClass = materializedClass && effectiveClassId !== null && context.classes.get(effectiveClassId)?.requiredConstructorId === void 0;
@@ -104142,7 +104232,10 @@ function emitProjectDocumentFilesV4(records2) {
104142
104232
  data: record3.data
104143
104233
  });
104144
104234
  }
104145
- const manifest = documentsToProjectSchemaManifest(schemaRecords);
104235
+ const staticMemberOwnershipRecoveries = [];
104236
+ const manifest = documentsToProjectSchemaManifest(schemaRecords, {
104237
+ onInvalidStaticMemberOwnership: (recovery) => staticMemberOwnershipRecoveries.push(recovery)
104238
+ });
104146
104239
  const staticValues = emitStaticValueSourcesV4(records2, manifest);
104147
104240
  const memberDefaults = emitMemberDefaultSourcesV4(records2, manifest);
104148
104241
  const variantValues = emitVariantValueSourcesV4(records2, manifest);
@@ -104215,6 +104308,13 @@ function emitProjectDocumentFilesV4(records2) {
104215
104308
  ]) {
104216
104309
  for (const key of file.recordKeys) recordFiles.set(key, file.path);
104217
104310
  }
104311
+ for (const recovery of staticMemberOwnershipRecoveries) {
104312
+ if (recovery.action !== "delete") continue;
104313
+ recordFiles.set(
104314
+ `member:${recovery.memberId}`,
104315
+ STATIC_MEMBER_OWNERSHIP_RECOVERY_SOURCE
104316
+ );
104317
+ }
104218
104318
  const analysis = compileNeoProjectSources(
104219
104319
  files.map((file) => {
104220
104320
  const kind = neoProjectSourceKind(file.path);
@@ -104242,7 +104342,8 @@ ${errors.map(
104242
104342
  recordFiles,
104243
104343
  analysis,
104244
104344
  materializedConstructors,
104245
- formAlternates: source.formAlternates
104345
+ formAlternates: source.formAlternates,
104346
+ staticMemberOwnershipRecoveries
104246
104347
  };
104247
104348
  }
104248
104349
  function relationEndpointExpressions(records2) {
@@ -104295,6 +104396,7 @@ var init_project_documents = __esm({
104295
104396
  init_root_value_paths();
104296
104397
  init_source_diagnostics();
104297
104398
  init_source_format();
104399
+ init_static_member_ownership_recovery();
104298
104400
  PROJECT_SOURCE_ANALYSIS_V4_CACHE_PATH = ".neo/project-source-analysis-v4.json";
104299
104401
  SCHEMA_RECORD_KINDS2 = /* @__PURE__ */ new Set([
104300
104402
  "class",
@@ -105958,7 +106060,8 @@ function resetWorkspaceToProjectSourcesV4(workspace, document, options = {}) {
105958
106060
  return {
105959
106061
  written: emitted.files.length,
105960
106062
  removed,
105961
- fileCount: emitted.files.length
106063
+ fileCount: emitted.files.length,
106064
+ staticMemberOwnershipRecoveries: emitted.staticMemberOwnershipRecoveries
105962
106065
  };
105963
106066
  }
105964
106067
  function preserveManagedSpecs(root) {
@@ -106680,6 +106783,9 @@ async function runResetPull(workspace) {
106680
106783
  const reset = resetWorkspaceToProjectSourcesV4(workspace, document, {
106681
106784
  regenerateSourceNames: true
106682
106785
  });
106786
+ for (const recovery of reset.staticMemberOwnershipRecoveries ?? []) {
106787
+ warn(staticMemberOwnershipRecoveryMessage(recovery));
106788
+ }
106683
106789
  for (const [key, state] of binaries.states) {
106684
106790
  const record3 = workspace.state.records[key];
106685
106791
  if (record3) record3.projectBinary = state;
@@ -106759,6 +106865,9 @@ async function finishFormat4Pull(args) {
106759
106865
  )
106760
106866
  );
106761
106867
  const localResult = emitProjectDocumentFilesV4(localRecords);
106868
+ for (const recovery of localResult.staticMemberOwnershipRecoveries ?? []) {
106869
+ warn(staticMemberOwnershipRecoveryMessage(recovery));
106870
+ }
106762
106871
  const serverResult = conflictCount === 0 ? null : emitProjectDocumentFilesV4(
106763
106872
  buildEmitRecordSet(document, plans, "server")
106764
106873
  );
@@ -107196,6 +107305,7 @@ var init_pull = __esm({
107196
107305
  init_project_file_pull();
107197
107306
  init_dialogue_sources();
107198
107307
  init_static_value_seed_records();
107308
+ init_static_member_ownership_recovery();
107199
107309
  }
107200
107310
  });
107201
107311
 
@@ -112353,6 +112463,13 @@ async function prepareLocalPushArtifactsV4(workspace, status, onPhase = () => vo
112353
112463
  );
112354
112464
  let preparedChanges = null;
112355
112465
  if (options.fullValidation) {
112466
+ onPhase("Verifying the assigned-id source rewrite\u2026");
112467
+ materializeAssignedSchemaIdsInAuthoredSource(
112468
+ workspace,
112469
+ pendingAssignment.assigned,
112470
+ status.pendingValueIdentitySites,
112471
+ true
112472
+ );
112356
112473
  onPhase("Verifying the complete source round trip\u2026");
112357
112474
  verifyProjectSourceCommitAgainstStateV4({
112358
112475
  projectId: workspace.config.projectId,
@@ -113182,17 +113299,7 @@ function rewriteFilesFromState(workspace, records2 = new Map(
113182
113299
  return { uri: file.path, kind, text: file.content };
113183
113300
  })
113184
113301
  );
113185
- const finalErrors = finalAnalysis.diagnostics.filter(
113186
- (diagnostic) => diagnostic.severity === "error" && !isPullRecoveryDiagnosticCode(diagnostic.code)
113187
- );
113188
- if (finalErrors.length > 0) {
113189
- throw new Error(
113190
- `Assigned-id source rewrite produced invalid source:
113191
- ${finalErrors.map(
113192
- (diagnostic) => ` ${diagnostic.uri}:${diagnostic.range.start.line + 1}:${diagnostic.range.start.character + 1} ${diagnostic.message}`
113193
- ).join("\n")}`
113194
- );
113195
- }
113302
+ assertAssignedIdSourceRewriteValid(finalAnalysis);
113196
113303
  writeProjectSourceAnalysisCacheV4(workspace.root, finalAnalysis);
113197
113304
  const emittedPaths = new Set(files.map((file) => file.path));
113198
113305
  for (const recordState of Object.values(workspace.state.records)) {
@@ -113218,7 +113325,20 @@ ${finalErrors.map(
113218
113325
  workspace.state.records[key] = nextState;
113219
113326
  }
113220
113327
  }
113221
- function materializeAssignedSchemaIdsInAuthoredSource(workspace, replacements, pendingValueIdentitySites = /* @__PURE__ */ new Map()) {
113328
+ function assertAssignedIdSourceRewriteValid(analysis) {
113329
+ const finalErrors = analysis.diagnostics.filter(
113330
+ (diagnostic) => diagnostic.severity === "error" && !isPullRecoveryDiagnosticCode(diagnostic.code)
113331
+ );
113332
+ if (finalErrors.length > 0) {
113333
+ throw new Error(
113334
+ `Assigned-id source rewrite produced invalid source:
113335
+ ${finalErrors.map(
113336
+ (diagnostic) => ` ${diagnostic.uri}:${diagnostic.range.start.line + 1}:${diagnostic.range.start.character + 1} ${diagnostic.message}`
113337
+ ).join("\n")}`
113338
+ );
113339
+ }
113340
+ }
113341
+ function materializeAssignedSchemaIdsInAuthoredSource(workspace, replacements, pendingValueIdentitySites = /* @__PURE__ */ new Map(), validateResult = false) {
113222
113342
  const pendingIdsByUri = /* @__PURE__ */ new Map();
113223
113343
  for (const pendingId2 of replacements.keys()) {
113224
113344
  if (!isPendingId(pendingId2)) continue;
@@ -113394,6 +113514,16 @@ function materializeAssignedSchemaIdsInAuthoredSource(workspace, replacements, p
113394
113514
  }
113395
113515
  preserved.set(uri, source);
113396
113516
  }
113517
+ if (validateResult && preserved.size > 0) {
113518
+ assertAssignedIdSourceRewriteValid(
113519
+ compileNeoProjectSources(
113520
+ inputs.map((input) => ({
113521
+ ...input,
113522
+ text: preserved.get(input.uri) ?? input.text
113523
+ }))
113524
+ )
113525
+ );
113526
+ }
113397
113527
  return preserved;
113398
113528
  }
113399
113529
  function sourceOffsetInsideInitializer(source, oneBasedLine, oneBasedColumn) {
@@ -114010,7 +114140,7 @@ var init_registry2 = __esm({
114010
114140
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
114011
114141
  formatVersion: 3,
114012
114142
  contractVersion: "3.14",
114013
- cliVersion: "0.36.3",
114143
+ cliVersion: "0.36.5",
114014
114144
  projectFileUploadBatchSize: 32,
114015
114145
  documentRecords: {
114016
114146
  member: {
@@ -120612,7 +120742,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
120612
120742
  async function main() {
120613
120743
  const args = parseArgs(process.argv.slice(2));
120614
120744
  if (args.command === "--version") {
120615
- console.log("0.36.3");
120745
+ console.log("0.36.5");
120616
120746
  return;
120617
120747
  }
120618
120748
  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.36.3",
3
+ "version": "0.36.5",
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.36.3 -->
12
+ <!-- reviewed-through-cli: 0.36.5 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -83,7 +83,7 @@ wrappers.
83
83
  The marker near the top of `SKILL.md` must exactly match the package version:
84
84
 
85
85
  ```html
86
- <!-- reviewed-through-cli: 0.36.3 -->
86
+ <!-- reviewed-through-cli: 0.36.5 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale