@neocompose/cli 0.46.1 → 0.46.2

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.46.2] - 2026-09-04
4
+
5
+ ### Fixed
6
+
7
+ - Pulling variants with non-class value rows no longer produces false local
8
+ `classId: null` updates. Unchanged status, pull, and test preparation avoid
9
+ the animation validation those false edits triggered.
10
+ - Normal pull checks the remote head before local reconstruction, avoiding
11
+ compilation when there are no remote changes.
12
+ - Terminal spinners keep animating during synchronous compilation and show
13
+ workspace loading and preparation phases for status, diff, pull, and test.
14
+ `NO_COLOR` disables color without disabling animation; piped and JSON
15
+ status/test output remain free of terminal controls.
16
+ - Prospective animation validation reuses one class index for each validation
17
+ document instead of rebuilding it for every animation root and initializer.
18
+ - Stored construction replay validates and compiles constructor records once per
19
+ emission batch, and reuses each value's constructor-body schema-key index.
20
+
3
21
  ## [0.46.1] - 2026-09-04
4
22
 
5
23
  ### Changed
package/dist/neo.mjs CHANGED
@@ -539,6 +539,64 @@ var init_token_store = __esm({
539
539
  }
540
540
  });
541
541
 
542
+ // src/spinner-renderer.ts
543
+ import { writeSync } from "node:fs";
544
+ import { Worker } from "node:worker_threads";
545
+ function startSpinnerRenderer(label, frames, colored) {
546
+ const state = new Int32Array(new SharedArrayBuffer(4));
547
+ const open = colored ? "\x1B[36m" : "";
548
+ const close = colored ? "\x1B[39m" : "";
549
+ writeSync(1, `\r\x1B[2K${open}${frames[0]}${close} ${label}`);
550
+ const worker = new Worker(RENDERER, {
551
+ eval: true,
552
+ execArgv: [],
553
+ workerData: { state: state.buffer, frames, label, open, close }
554
+ });
555
+ const stop = () => {
556
+ for (; ; ) {
557
+ const previous = Atomics.compareExchange(state, 0, 0, 2);
558
+ if (previous === 2) return;
559
+ if (previous === 0) break;
560
+ Atomics.wait(state, 0, 1);
561
+ }
562
+ void worker.terminate();
563
+ writeSync(1, "\r\x1B[2K");
564
+ };
565
+ worker.on("error", stop);
566
+ worker.unref();
567
+ return {
568
+ update(next) {
569
+ if (Atomics.load(state, 0) !== 2) worker.postMessage(next);
570
+ },
571
+ stop
572
+ };
573
+ }
574
+ var RENDERER;
575
+ var init_spinner_renderer = __esm({
576
+ "src/spinner-renderer.ts"() {
577
+ "use strict";
578
+ RENDERER = `
579
+ const { workerData, parentPort } = require("node:worker_threads");
580
+ const { writeSync } = require("node:fs");
581
+ const state = new Int32Array(workerData.state);
582
+ let label = workerData.label;
583
+ let frame = 1;
584
+ function render() {
585
+ if (Atomics.compareExchange(state, 0, 0, 1) !== 0) return;
586
+ try {
587
+ const glyph = workerData.frames[frame++ % workerData.frames.length];
588
+ writeSync(1, "\\r\\u001b[2K" + workerData.open + glyph + workerData.close + " " + label);
589
+ } finally {
590
+ Atomics.store(state, 0, 0);
591
+ Atomics.notify(state, 0);
592
+ }
593
+ }
594
+ parentPort.on("message", (next) => { label = next; render(); });
595
+ setInterval(render, 80);
596
+ `;
597
+ }
598
+ });
599
+
542
600
  // src/ui.ts
543
601
  function paint(open, close) {
544
602
  if (!colorEnabled) return (text) => text;
@@ -564,42 +622,45 @@ function paintChangeKind(kind) {
564
622
  return painter === void 0 ? kind : painter(kind);
565
623
  }
566
624
  function spinner(text) {
567
- if (!colorEnabled) {
625
+ if (process.stdout.isTTY !== true || process.env.TERM === "dumb") {
568
626
  console.log(text);
627
+ let stopped2 = false;
569
628
  return {
570
629
  update: () => void 0,
571
- succeed: (done) => console.log(done),
572
- fail: (done) => console.error(done),
573
- stop: () => void 0
630
+ succeed(done) {
631
+ if (!stopped2) console.log(done);
632
+ stopped2 = true;
633
+ },
634
+ fail(done) {
635
+ if (!stopped2) console.error(done);
636
+ stopped2 = true;
637
+ },
638
+ stop() {
639
+ stopped2 = true;
640
+ }
574
641
  };
575
642
  }
576
- let frame = 0;
577
- let label = text;
578
- const render = () => {
579
- process.stdout.write(
580
- `\r\x1B[2K${color.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length])} ${label}`
581
- );
582
- frame += 1;
583
- };
584
- render();
585
- const timer = setInterval(render, 80);
643
+ const renderer = startSpinnerRenderer(text, SPINNER_FRAMES, colorEnabled);
644
+ let stopped = false;
586
645
  const clear = () => {
587
- clearInterval(timer);
588
- process.stdout.write("\r\x1B[2K");
646
+ if (stopped) return false;
647
+ stopped = true;
648
+ renderer.stop();
649
+ return true;
589
650
  };
590
651
  return {
591
652
  update(next) {
592
- label = next;
653
+ if (!stopped) renderer.update(next);
593
654
  },
594
655
  succeed(done) {
595
- clear();
596
- console.log(`${sym.ok} ${done}`);
656
+ if (clear()) console.log(`${sym.ok} ${done}`);
597
657
  },
598
658
  fail(done) {
599
- clear();
600
- console.error(`${sym.fail} ${done}`);
659
+ if (clear()) console.error(`${sym.fail} ${done}`);
601
660
  },
602
- stop: clear
661
+ stop() {
662
+ clear();
663
+ }
603
664
  };
604
665
  }
605
666
  async function promptSelect(args) {
@@ -643,6 +704,7 @@ var colorEnabled, color, sym, CHANGE_KIND_COLOR, SPINNER_FRAMES;
643
704
  var init_ui = __esm({
644
705
  "src/ui.ts"() {
645
706
  "use strict";
707
+ init_spinner_renderer();
646
708
  colorEnabled = process.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
647
709
  color = {
648
710
  bold: paint(1, 22),
@@ -46679,12 +46741,24 @@ function projectSchemaIndexFor(source, revision = EMPTY_REVISION) {
46679
46741
  byMembers = /* @__PURE__ */ new WeakMap();
46680
46742
  projectSchemaIndexes.set(source.classes, byMembers);
46681
46743
  }
46682
- const cached = byMembers.get(source.members);
46744
+ let byEnums = byMembers.get(source.members);
46745
+ if (byEnums === void 0) {
46746
+ byEnums = /* @__PURE__ */ new WeakMap();
46747
+ byMembers.set(source.members, byEnums);
46748
+ }
46749
+ const enumKey = source.enums ?? ABSENT_COLLECTION;
46750
+ let byConstructors = byEnums.get(enumKey);
46751
+ if (byConstructors === void 0) {
46752
+ byConstructors = /* @__PURE__ */ new WeakMap();
46753
+ byEnums.set(enumKey, byConstructors);
46754
+ }
46755
+ const constructorKey = source.constructors ?? ABSENT_COLLECTION;
46756
+ const cached = byConstructors.get(constructorKey);
46683
46757
  if (cached !== void 0 && isCurrent(cached, source, revision)) {
46684
46758
  return cached.index;
46685
46759
  }
46686
46760
  const index = new ProjectSchemaIndex(source);
46687
- byMembers.set(source.members, {
46761
+ byConstructors.set(constructorKey, {
46688
46762
  classCount: source.classes.length,
46689
46763
  memberCount: source.members.length,
46690
46764
  enums: source.enums,
@@ -46697,7 +46771,7 @@ function projectSchemaIndexFor(source, revision = EMPTY_REVISION) {
46697
46771
  });
46698
46772
  return index;
46699
46773
  }
46700
- var CircularInheritanceError, ProjectSchemaIndex, EMPTY_STRING_SET, EMPTY_REVISION, projectSchemaIndexEpoch, projectSchemaIndexes;
46774
+ var CircularInheritanceError, ProjectSchemaIndex, EMPTY_STRING_SET, EMPTY_REVISION, projectSchemaIndexEpoch, projectSchemaIndexes, ABSENT_COLLECTION;
46701
46775
  var init_project_schema_index = __esm({
46702
46776
  "../src/models/project/project-schema-index.ts"() {
46703
46777
  "use strict";
@@ -46859,6 +46933,7 @@ var init_project_schema_index = __esm({
46859
46933
  EMPTY_REVISION = [];
46860
46934
  projectSchemaIndexEpoch = 0;
46861
46935
  projectSchemaIndexes = /* @__PURE__ */ new WeakMap();
46936
+ ABSENT_COLLECTION = {};
46862
46937
  }
46863
46938
  });
46864
46939
 
@@ -70862,9 +70937,9 @@ function evaluatorOwnershipDistances(rowId, indexes) {
70862
70937
  if (cached !== void 0) return cached;
70863
70938
  const distances = /* @__PURE__ */ new Map([[rowId, 0]]);
70864
70939
  const pending = [rowId];
70865
- while (pending.length > 0) {
70866
- const currentId = pending.shift();
70867
- if (currentId === void 0) break;
70940
+ for (let cursor = 0; cursor < pending.length; cursor += 1) {
70941
+ const currentId = pending[cursor];
70942
+ if (currentId === void 0) continue;
70868
70943
  const currentDistance = distances.get(currentId);
70869
70944
  if (currentDistance === void 0) continue;
70870
70945
  for (const parent of evaluatorParentLinks(indexes, currentId)) {
@@ -71482,6 +71557,19 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
71482
71557
  }
71483
71558
  const escapedRoots = /* @__PURE__ */ new Set();
71484
71559
  const scannedRows = /* @__PURE__ */ new Set();
71560
+ let rowsByContainerId;
71561
+ const containerEntryIds = (containerId) => {
71562
+ if (rowsByContainerId === void 0) {
71563
+ rowsByContainerId = /* @__PURE__ */ new Map();
71564
+ for (const candidate of evaluatorValues(ctx)) {
71565
+ if (typeof candidate.containerId !== "string") continue;
71566
+ const entries = rowsByContainerId.get(candidate.containerId) ?? [];
71567
+ entries.push(candidate.id);
71568
+ rowsByContainerId.set(candidate.containerId, entries);
71569
+ }
71570
+ }
71571
+ return rowsByContainerId.get(containerId) ?? [];
71572
+ };
71485
71573
  const markConstructorGroupEscaped = (rootId) => {
71486
71574
  if (escapedRoots.has(rootId)) return;
71487
71575
  escapedRoots.add(rootId);
@@ -71531,13 +71619,11 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
71531
71619
  ctx.onUnattributableValueRead?.(
71532
71620
  `constructor-argument-list:${argumentId}`
71533
71621
  );
71534
- for (const candidate of evaluatorValues(ctx)) {
71535
- if (candidate.containerId === argumentId) {
71536
- scanCreationDataRow({
71537
- id: candidate.id,
71538
- typeInfo: typeInfo.entryTypeInfo
71539
- });
71540
- }
71622
+ for (const childId of containerEntryIds(argumentId)) {
71623
+ scanCreationDataRow({
71624
+ id: childId,
71625
+ typeInfo: typeInfo.entryTypeInfo
71626
+ });
71541
71627
  }
71542
71628
  return;
71543
71629
  }
@@ -71557,9 +71643,11 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
71557
71643
  }
71558
71644
  const rawMember = hintedMember ?? activeStaticBindingRoot(valueId, ctx)?.member ?? memberForValueRow(row, ctx);
71559
71645
  let member = rawMember;
71646
+ let resolvedMemberForSubstitution;
71560
71647
  if (rawMember !== null) {
71561
71648
  try {
71562
- member = resolveMember2(rawMember, ctx.vm.members);
71649
+ member = cachedResolvedMember(rawMember, ctx);
71650
+ resolvedMemberForSubstitution = member;
71563
71651
  } catch {
71564
71652
  member = rawMember;
71565
71653
  }
@@ -71569,7 +71657,8 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
71569
71657
  member = substituteMember(
71570
71658
  member,
71571
71659
  envFromStamp(row.genericBindings),
71572
- ctx.vm.members
71660
+ ctx.vm.members,
71661
+ resolvedMemberForSubstitution
71573
71662
  );
71574
71663
  } catch {
71575
71664
  }
@@ -71589,10 +71678,8 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
71589
71678
  const entryMember = evalMemberById(ctx.vm, member.entryMemberId);
71590
71679
  if (listKindOf(member) === 1 /* Unordered */) {
71591
71680
  ctx.onUnattributableValueRead?.(`constructed-unordered-list:${row.id}`);
71592
- for (const candidate of evaluatorValues(ctx)) {
71593
- if (candidate.containerId === row.id) {
71594
- scanOwnedRow(candidate.id, entryMember);
71595
- }
71681
+ for (const childId of containerEntryIds(row.id)) {
71682
+ scanOwnedRow(childId, entryMember);
71596
71683
  }
71597
71684
  return;
71598
71685
  }
@@ -104746,10 +104833,12 @@ function compareWorkspacePaths(left, right) {
104746
104833
  }
104747
104834
  function computeWorkspaceStatus(workspace, options) {
104748
104835
  let phaseStarted = performance.now();
104749
- const reportPhase = (phase) => {
104836
+ options.onPhase?.("Reading project schema\u2026");
104837
+ const reportPhase = (phase, nextLabel) => {
104750
104838
  const finished = performance.now();
104751
104839
  options.reportPhase?.(phase, finished - phaseStarted);
104752
104840
  phaseStarted = finished;
104841
+ if (nextLabel !== void 0) options.onPhase?.(nextLabel);
104753
104842
  };
104754
104843
  const conflictedFiles = [];
104755
104844
  const parseErrors = [];
@@ -104912,7 +105001,7 @@ function computeWorkspaceStatus(workspace, options) {
104912
105001
  analysis,
104913
105002
  compilationSources
104914
105003
  );
104915
- reportPhase("analysis-schema-defaults");
105004
+ reportPhase("analysis-schema-defaults", "Reading stored values\u2026");
104916
105005
  } catch (error) {
104917
105006
  parseErrors.push(
104918
105007
  error instanceof SchemaSourceError ? error : new SchemaSourceError(
@@ -104987,7 +105076,7 @@ function computeWorkspaceStatus(workspace, options) {
104987
105076
  manifest,
104988
105077
  { registry: valueLowerRegistry }
104989
105078
  );
104990
- reportPhase("documents-static-root");
105079
+ reportPhase("documents-static-root", "Reading variants and dialogues\u2026");
104991
105080
  authoredValueSeeds = new Map([...authoredValueSeeds, ...rootValues.seeds]);
104992
105081
  const rootPathResolutionState = overlayProspectiveSourceRecords(
104993
105082
  loweringRecords,
@@ -105145,7 +105234,7 @@ function computeWorkspaceStatus(workspace, options) {
105145
105234
  prospectiveState,
105146
105235
  projectAnalysisV4
105147
105236
  );
105148
- reportPhase("supplemental-dialogue");
105237
+ reportPhase("supplemental-dialogue", "Comparing records\u2026");
105149
105238
  records2.push(
105150
105239
  ...staticValues.records,
105151
105240
  ...memberDefaults.records,
@@ -105268,7 +105357,7 @@ function computeWorkspaceStatus(workspace, options) {
105268
105357
  )
105269
105358
  );
105270
105359
  }
105271
- reportPhase("record-diff-references");
105360
+ reportPhase("record-diff-references", "Checking packed values\u2026");
105272
105361
  if (referenceFailures.length > 0) {
105273
105362
  return {
105274
105363
  changes: [],
@@ -105349,7 +105438,7 @@ function computeWorkspaceStatus(workspace, options) {
105349
105438
  reconstructed: reconstructed3,
105350
105439
  changes
105351
105440
  });
105352
- reportPhase("packed-value-fold");
105441
+ reportPhase("packed-value-fold", "Checking initializers\u2026");
105353
105442
  for (const change of changes) {
105354
105443
  if (change.recordKind !== "value" || change.kind !== "update") continue;
105355
105444
  const refusal = valueDematerializationRefusal({
@@ -105393,7 +105482,7 @@ function computeWorkspaceStatus(workspace, options) {
105393
105482
  )
105394
105483
  );
105395
105484
  }
105396
- reportPhase("initializer-reconciliation");
105485
+ reportPhase("initializer-reconciliation", "Checking project files\u2026");
105397
105486
  for (const fileId of options.trustedPendingProjectFiles?.keys() ?? []) {
105398
105487
  const key = `project-file:${fileId}`;
105399
105488
  const base = workspace.state.records[key];
@@ -105430,7 +105519,7 @@ function computeWorkspaceStatus(workspace, options) {
105430
105519
  binaryChanges = binaryFiles.filter(
105431
105520
  (binary) => binary.action !== "unchanged" && binary.action !== "converged"
105432
105521
  );
105433
- reportPhase("binary-inspection");
105522
+ reportPhase("binary-inspection", "Validating animation changes\u2026");
105434
105523
  const invalidatedFileIds = /* @__PURE__ */ new Set();
105435
105524
  for (const binary of binaryFiles) {
105436
105525
  if (binary.action === "create" || binary.action === "upload") {
@@ -105479,7 +105568,7 @@ function computeWorkspaceStatus(workspace, options) {
105479
105568
  );
105480
105569
  }
105481
105570
  }
105482
- reportPhase("animation-clip-validation");
105571
+ reportPhase("animation-clip-validation", "Preparing new values\u2026");
105483
105572
  for (const binary of binaryChanges) {
105484
105573
  if (binary.action !== "missing-local") continue;
105485
105574
  parseErrors.push(
@@ -105616,7 +105705,7 @@ function replayAnimationDeclarationInitializersV4(records2, document, fallbackDo
105616
105705
  if (owner === void 0) continue;
105617
105706
  const declaredClassId = typeof row.classId === "string" ? row.classId : Reflect.get(owner, "classId");
105618
105707
  if (typeof declaredClassId !== "string") continue;
105619
- if (!classBelongsToAnimationFamily(document.classes, declaredClassId)) {
105708
+ if (!classBelongsToAnimationFamily(document.classesById, declaredClassId)) {
105620
105709
  continue;
105621
105710
  }
105622
105711
  const fallback = fallbackValues.get(row.id);
@@ -105695,10 +105784,7 @@ function animationRecordsFromState(records2) {
105695
105784
  return isObjectRecord2(data) ? [{ recordKind: record4.recordKind, data }] : [];
105696
105785
  });
105697
105786
  }
105698
- function classBelongsToAnimationFamily(classes, classId) {
105699
- const classesById = new Map(
105700
- classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
105701
- );
105787
+ function classBelongsToAnimationFamily(classesById, classId) {
105702
105788
  const visited = /* @__PURE__ */ new Set();
105703
105789
  let currentId = classId;
105704
105790
  while (currentId !== null && !visited.has(currentId)) {
@@ -105764,6 +105850,9 @@ function prospectiveAnimationDocumentV4(records2) {
105764
105850
  }
105765
105851
  }
105766
105852
  if (project === void 0) return null;
105853
+ const classesById = new Map(
105854
+ classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
105855
+ );
105767
105856
  const classIdByName = new Map(classes.map((value) => [value.name, value.id]));
105768
105857
  const validationValues = expandPackedValueRows(values).map((value) => {
105769
105858
  if (typeof value.classId === "string") return value;
@@ -105788,10 +105877,12 @@ function prospectiveAnimationDocumentV4(records2) {
105788
105877
  return {
105789
105878
  project,
105790
105879
  classes,
105880
+ classesById,
105791
105881
  members,
105792
105882
  values: expandProspectiveAnimationInstances({
105793
105883
  project,
105794
105884
  classes,
105885
+ classesById,
105795
105886
  constructors,
105796
105887
  members,
105797
105888
  values: validationValues
@@ -105800,7 +105891,7 @@ function prospectiveAnimationDocumentV4(records2) {
105800
105891
  }
105801
105892
  function expandProspectiveAnimationInstances(document) {
105802
105893
  const animationRoots = document.values.filter(
105803
- (row) => isLiteralValueContent(row) && isVirtualInstanceRootShape(row) && typeof row.classId === "string" && classBelongsToAnimationFamily(document.classes, row.classId)
105894
+ (row) => isLiteralValueContent(row) && isVirtualInstanceRootShape(row) && typeof row.classId === "string" && classBelongsToAnimationFamily(document.classesById, row.classId)
105804
105895
  );
105805
105896
  if (animationRoots.length === 0) return [...document.values];
105806
105897
  const lookups = cliEvaluatorLookups({
@@ -107194,8 +107285,7 @@ var init_commit_preparation_scope_planner = __esm({
107194
107285
  "source-only-rows",
107195
107286
  "whole-graph-validation-members",
107196
107287
  "world-value-ranges"
107197
- ],
107198
- mutatesDocument: []
107288
+ ]
107199
107289
  },
107200
107290
  "authored-value-seeds": {
107201
107291
  order: 2,
@@ -107217,10 +107307,7 @@ var init_commit_preparation_scope_planner = __esm({
107217
107307
  "every-variant-root",
107218
107308
  "localizable-reference-candidates",
107219
107309
  "world-value-ranges"
107220
- ],
107221
- // `materializeStaticSeedBindingMembers` pushes the binding member it mints
107222
- // into the document the rest of this pass reads, not only into `prepared`.
107223
- mutatesDocument: ["members"]
107310
+ ]
107224
107311
  },
107225
107312
  "variant-child-override-binding-members": {
107226
107313
  order: 3,
@@ -107236,10 +107323,7 @@ var init_commit_preparation_scope_planner = __esm({
107236
107323
  "every-variant-root",
107237
107324
  "stamp-edge-candidates",
107238
107325
  "world-value-ranges"
107239
- ],
107240
- // The minted binding member is pushed straight into the document the
107241
- // later passes read, not only into `prepared`.
107242
- mutatesDocument: ["members"]
107326
+ ]
107243
107327
  },
107244
107328
  "prepared-instance-initializers": {
107245
107329
  order: 4,
@@ -107252,8 +107336,7 @@ var init_commit_preparation_scope_planner = __esm({
107252
107336
  "full-content-dimension",
107253
107337
  "member-default-value-closure",
107254
107338
  "world-value-ranges"
107255
- ],
107256
- mutatesDocument: []
107339
+ ]
107257
107340
  },
107258
107341
  "delegate-value-bodies": {
107259
107342
  order: 5,
@@ -107269,8 +107352,7 @@ var init_commit_preparation_scope_planner = __esm({
107269
107352
  "every-variant-root",
107270
107353
  "source-only-rows",
107271
107354
  "world-value-ranges"
107272
- ],
107273
- mutatesDocument: []
107355
+ ]
107274
107356
  },
107275
107357
  "variant-constructor-args": {
107276
107358
  order: 6,
@@ -107283,8 +107365,7 @@ var init_commit_preparation_scope_planner = __esm({
107283
107365
  "touched-variant-schema-key-children",
107284
107366
  "member-default-value-closure",
107285
107367
  "world-value-ranges"
107286
- ],
107287
- mutatesDocument: []
107368
+ ]
107288
107369
  }
107289
107370
  };
107290
107371
  }
@@ -108997,6 +109078,12 @@ var init_server_preparation_preflight = __esm({
108997
109078
  function isPulledInitializerCompilationDocumentV4(document) {
108998
109079
  return typeof Reflect.get(document, "project") === "object" && Reflect.get(document, "project") !== null && Array.isArray(Reflect.get(document, "projectFiles")) && Array.isArray(Reflect.get(document, "members")) && Array.isArray(Reflect.get(document, "classes")) && Array.isArray(Reflect.get(document, "enums")) && Array.isArray(Reflect.get(document, "interfaces")) && Array.isArray(Reflect.get(document, "constructors")) && Array.isArray(Reflect.get(document, "values"));
108999
109080
  }
109081
+ function createPulledConstructionReplayContext(args) {
109082
+ return {
109083
+ document: args.document,
109084
+ constructorsValidated: false
109085
+ };
109086
+ }
109000
109087
  function pulledInitializerCompilerStateV4(document, compilationProject) {
109001
109088
  const existing = pulledInitializerCompilerStates.get(document);
109002
109089
  if (existing !== void 0) {
@@ -109072,6 +109159,14 @@ function describePulledProjectBodyCompileError(error) {
109072
109159
  }
109073
109160
  function replayStoredConstructionV4(args) {
109074
109161
  const document = args.document ?? readPulledProjectDocumentV4(args.records);
109162
+ if (args.replayContext !== void 0 && args.replayContext.document !== document) {
109163
+ throw new Error(
109164
+ "Construction replay context belongs to a different document."
109165
+ );
109166
+ }
109167
+ if (args.compileDocumentBodies === true && args.replayContext !== void 0) {
109168
+ args.replayContext.constructorsValidated = false;
109169
+ }
109075
109170
  const compilationProject = args.compilationProject ?? (args.compileDocumentBodies === true ? createNeoScriptCompilationProject({
109076
109171
  project: document.project,
109077
109172
  projectFiles: document.projectFiles,
@@ -109156,8 +109251,12 @@ function replayStoredConstructionV4(args) {
109156
109251
  ...candidate,
109157
109252
  init: withoutInitializerConstructionFields(candidate.init)
109158
109253
  } : candidate;
109159
- compilePulledUncompiledConstructorsV4(document, compilationProject);
109160
- assertPulledConstructorsCompiled(document);
109254
+ compilePulledUncompiledConstructorsV4(
109255
+ document,
109256
+ compilationProject,
109257
+ args.replayContext
109258
+ );
109259
+ assertPulledConstructorsCompiled(document, args.replayContext);
109161
109260
  const materialized = materializeInitializerValue2({
109162
109261
  document: {
109163
109262
  ...document,
@@ -109185,11 +109284,15 @@ function replayStoredConstructionV4(args) {
109185
109284
  ])
109186
109285
  );
109187
109286
  }
109188
- function compilePulledUncompiledConstructorsV4(document, compilationProject) {
109287
+ function compilePulledUncompiledConstructorsV4(document, compilationProject, replayContext) {
109288
+ if (replayContext?.constructorsValidated === true) return;
109189
109289
  const uncompiled = (document.constructors ?? []).filter(
109190
109290
  (constructor2) => !isNeoClassConstructor(constructor2)
109191
109291
  );
109192
- if (uncompiled.length === 0) return;
109292
+ if (uncompiled.length === 0) {
109293
+ if (replayContext !== void 0) replayContext.constructorsValidated = true;
109294
+ return;
109295
+ }
109193
109296
  const project = compilationProject ?? createNeoScriptCompilationProject({
109194
109297
  project: document.project,
109195
109298
  projectFiles: document.projectFiles,
@@ -109212,6 +109315,7 @@ function compilePulledUncompiledConstructorsV4(document, compilationProject) {
109212
109315
  for (const constructor2 of uncompiled) {
109213
109316
  compileConstructorRecord({ ...compileArgs, constructor: constructor2 });
109214
109317
  }
109318
+ assertPulledConstructorsCompiled(document, replayContext);
109215
109319
  }
109216
109320
  function valueInitializerCompilationSites(document) {
109217
109321
  const cached = pulledValueInitializerCompilationSites.get(document);
@@ -109319,7 +109423,13 @@ function compilePulledProjectDocumentForEvaluationV4(document, compilationProjec
109319
109423
  }
109320
109424
  }
109321
109425
  }
109322
- function assertPulledConstructorsCompiled(document) {
109426
+ function assertPulledConstructorsCompiled(document, replayContext) {
109427
+ if (replayContext !== void 0 && replayContext.document !== document) {
109428
+ throw new Error(
109429
+ "Constructor validation context belongs to a different document."
109430
+ );
109431
+ }
109432
+ if (replayContext?.constructorsValidated === true) return;
109323
109433
  const uncompiled = (document.constructors ?? []).find(
109324
109434
  (constructor2) => !isNeoClassConstructor(constructor2)
109325
109435
  );
@@ -109328,6 +109438,7 @@ function assertPulledConstructorsCompiled(document) {
109328
109438
  `Constructor "${uncompiled.id}" was read from authored source but was not compiled before evaluation.`
109329
109439
  );
109330
109440
  }
109441
+ if (replayContext !== void 0) replayContext.constructorsValidated = true;
109331
109442
  }
109332
109443
  function compilePulledMemberInitializerBodyV4(document, memberId, compilationProject) {
109333
109444
  const compileArgs = {
@@ -109526,11 +109637,16 @@ function buildValueEmitContext(records2, manifest) {
109526
109637
  const readDocument = () => document ??= readPulledProjectDocumentV4(records2);
109527
109638
  let compilationProject;
109528
109639
  const readCompilationProject = () => compilationProject ??= createNeoScriptCompilationProject(readDocument());
109640
+ let replayContext;
109641
+ const readReplayContext = () => replayContext ??= createPulledConstructionReplayContext({
109642
+ document: readDocument()
109643
+ });
109529
109644
  let materializedGraph;
109530
109645
  return {
109531
109646
  records: records2,
109532
109647
  readDocument,
109533
109648
  readCompilationProject,
109649
+ readReplayContext,
109534
109650
  readMaterializedGraph: () => {
109535
109651
  if (materializedGraph !== void 0) return materializedGraph;
109536
109652
  materializedGraph = new MaterializedValueGraphContext(
@@ -109572,6 +109688,7 @@ function buildValueEmitContext(records2, manifest) {
109572
109688
  fileSymbols: projectFileSymbols(records2),
109573
109689
  localizedTextIds: /* @__PURE__ */ new Set(),
109574
109690
  constructionReplays: /* @__PURE__ */ new Map(),
109691
+ constructorBodySchemaKeys: /* @__PURE__ */ new WeakMap(),
109575
109692
  materializedConstructors: /* @__PURE__ */ new Map(),
109576
109693
  materializedConstructorOverrideKeys: /* @__PURE__ */ new Map()
109577
109694
  };
@@ -110475,6 +110592,9 @@ function variantReconstructedFields(context, row) {
110475
110592
  }
110476
110593
  function mergedOverStoredRow(base, lowered) {
110477
110594
  const merged = { ...valueFileFields(base), ...lowered };
110595
+ if (lowered.classId === null && base.classId === void 0) {
110596
+ delete merged.classId;
110597
+ }
110478
110598
  const storedArgs = isObjectRecord2(base.constructorArgs) ? base.constructorArgs : null;
110479
110599
  const loweredArgs = isObjectRecord2(merged.constructorArgs) ? merged.constructorArgs : null;
110480
110600
  if (storedArgs !== null && loweredArgs !== null) {
@@ -115149,7 +115269,8 @@ function storedConstructionReplayRows(context, valueId, code, member) {
115149
115269
  valueId,
115150
115270
  code,
115151
115271
  member,
115152
- compilationProject: context.readCompilationProject()
115272
+ compilationProject: context.readCompilationProject(),
115273
+ replayContext: context.readReplayContext()
115153
115274
  });
115154
115275
  context.constructionReplays.set(valueId, replay);
115155
115276
  return replay;
@@ -115291,7 +115412,11 @@ function storedConstructorCallSource(context, schemaClass2, value, className, en
115291
115412
  // re-declared that external graph under the construction, and the
115292
115413
  // emitter then refused the whole pull with a containment cycle on the
115293
115414
  // re-declared row's own children.
115294
- constructorArgumentBodySchemaKey(value, constructorArgs[key]) !== void 0
115415
+ constructorArgumentBodySchemaKey(
115416
+ context,
115417
+ value,
115418
+ constructorArgs[key]
115419
+ ) !== void 0
115295
115420
  )}`
115296
115421
  ];
115297
115422
  });
@@ -115303,7 +115428,11 @@ function storedConstructorCallSource(context, schemaClass2, value, className, en
115303
115428
  function settledMemberForConstructorArgument(context, schemaClass2, value, argument2, argumentName) {
115304
115429
  if (typeof argument2 !== "string") return void 0;
115305
115430
  const schema = isObjectRecord2(schemaClass2.schema) ? schemaClass2.schema : {};
115306
- const bodySchemaKey = constructorArgumentBodySchemaKey(value, argument2);
115431
+ const bodySchemaKey = constructorArgumentBodySchemaKey(
115432
+ context,
115433
+ value,
115434
+ argument2
115435
+ );
115307
115436
  if (bodySchemaKey !== void 0) {
115308
115437
  return memberForSchemaKey(context, schema, bodySchemaKey);
115309
115438
  }
@@ -115320,11 +115449,22 @@ function settledMemberForConstructorArgument(context, schemaClass2, value, argum
115320
115449
  const namedSchemaKey = schemaKeyForParameterName(schema, argumentName);
115321
115450
  return namedSchemaKey === void 0 ? void 0 : memberForSchemaKey(context, schema, namedSchemaKey);
115322
115451
  }
115323
- function constructorArgumentBodySchemaKey(value, argument2) {
115452
+ function constructorArgumentBodySchemaKey(context, value, argument2) {
115324
115453
  if (typeof argument2 !== "string") return void 0;
115325
115454
  const body = value.value;
115326
115455
  if (!isObjectRecord2(body)) return void 0;
115327
- return Object.entries(body).find(([, rowId]) => rowId === argument2)?.[0];
115456
+ let keysByArgument = context.constructorBodySchemaKeys.get(value);
115457
+ if (keysByArgument === void 0) {
115458
+ const created = /* @__PURE__ */ new Map();
115459
+ for (const [schemaKey, rowId] of Object.entries(body)) {
115460
+ if (typeof rowId === "string" && !created.has(rowId)) {
115461
+ created.set(rowId, schemaKey);
115462
+ }
115463
+ }
115464
+ keysByArgument = created;
115465
+ context.constructorBodySchemaKeys.set(value, keysByArgument);
115466
+ }
115467
+ return keysByArgument.get(argument2);
115328
115468
  }
115329
115469
  function memberForSchemaKey(context, schema, schemaKey) {
115330
115470
  const memberId = schema[schemaKey];
@@ -117790,6 +117930,7 @@ function listProjectTestFilesV1(root) {
117790
117930
  return listNeoWorkspaceFilesV1(root).specs;
117791
117931
  }
117792
117932
  function computeWorkspaceStatus2(workspace, options = {}) {
117933
+ options.onPhase?.("Reading project sources\u2026");
117793
117934
  const virtualSourceFiles = options.virtualSourceFiles ?? listProjectSourceFilesV4(workspace.root).map((path) => ({
117794
117935
  path: relative3(workspace.root, path).split(sep3).join("/"),
117795
117936
  content: readFileSync7(path, "utf8")
@@ -119408,12 +119549,11 @@ import {
119408
119549
  readFileSync as readFileSync9
119409
119550
  } from "node:fs";
119410
119551
  import { dirname as dirname8 } from "node:path";
119411
- async function runPull(workspace, options) {
119552
+ async function runPull(workspace, options, progress = spinner("Checking local workspace\u2026")) {
119412
119553
  if (options.reset) {
119413
- await runResetPull(workspace);
119554
+ await runResetPull(workspace, progress);
119414
119555
  return;
119415
119556
  }
119416
- const progress = spinner("Checking local workspace\u2026");
119417
119557
  try {
119418
119558
  await runNormalPull(workspace, options, progress);
119419
119559
  } catch (error) {
@@ -119428,32 +119568,6 @@ async function runNormalPull(workspace, options, progress) {
119428
119568
  if (acceptSourceEquivalentConflictBases(workspace, mainLocale) > 0) {
119429
119569
  writeWorkspaceState(workspace.root, workspace.state);
119430
119570
  }
119431
- let localByKey = /* @__PURE__ */ new Map();
119432
- let localStatus = null;
119433
- if (hasBaseline && !destructive) {
119434
- const status = computeWorkspaceStatus2(workspace);
119435
- localStatus = status;
119436
- if (status.conflictedFiles.length > 0) {
119437
- throw new Error(
119438
- `Resolve conflict markers before pulling: ${status.conflictedFiles.join(", ")}`
119439
- );
119440
- }
119441
- const blockingErrors = status.parseErrors.filter(
119442
- isBlockingSchemaSourceError
119443
- );
119444
- if (blockingErrors.length > 0) {
119445
- throw new Error(
119446
- `Fix parse errors before pulling:
119447
- ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
119448
- );
119449
- }
119450
- localByKey = new Map(
119451
- [...status.reconstructed.entries()].map(([key, record4]) => [
119452
- key,
119453
- record4.fullData
119454
- ])
119455
- );
119456
- }
119457
119571
  let changedRecordKeys = null;
119458
119572
  let nextCursor = null;
119459
119573
  let observedHeadTransactionHash;
@@ -119508,6 +119622,35 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
119508
119622
  document = await fetchProjectDocument(workspace);
119509
119623
  nextCursor = document.revision === void 0 ? null : cursorFromRevision(document.revision);
119510
119624
  }
119625
+ let localByKey = /* @__PURE__ */ new Map();
119626
+ let localStatus = null;
119627
+ if (hasBaseline && !destructive) {
119628
+ const status = computeWorkspaceStatus2(workspace, {
119629
+ onPhase: (label) => progress.update(label)
119630
+ });
119631
+ localStatus = status;
119632
+ if (status.conflictedFiles.length > 0) {
119633
+ throw new Error(
119634
+ `Resolve conflict markers before pulling: ${status.conflictedFiles.join(", ")}`
119635
+ );
119636
+ }
119637
+ const blockingErrors = status.parseErrors.filter(
119638
+ isBlockingSchemaSourceError
119639
+ );
119640
+ if (blockingErrors.length > 0) {
119641
+ throw new Error(
119642
+ `Fix parse errors before pulling:
119643
+ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
119644
+ );
119645
+ }
119646
+ localByKey = new Map(
119647
+ [...status.reconstructed.entries()].map(([key, record4]) => [
119648
+ key,
119649
+ record4.fullData
119650
+ ])
119651
+ );
119652
+ }
119653
+ progress.update("Merging remote changes\u2026");
119511
119654
  const plans = /* @__PURE__ */ new Map();
119512
119655
  let mergedCount = 0;
119513
119656
  let conflictCount = 0;
@@ -119639,8 +119782,8 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
119639
119782
  progress
119640
119783
  });
119641
119784
  }
119642
- async function runResetPull(workspace) {
119643
- const progress = spinner("Pulling project snapshot\u2026");
119785
+ async function runResetPull(workspace, progress) {
119786
+ progress.update("Pulling project snapshot\u2026");
119644
119787
  try {
119645
119788
  const document = await fetchProjectDocument(workspace);
119646
119789
  progress.update("Downloading project files\u2026");
@@ -125108,6 +125251,7 @@ async function downloadProjectVersionTransactionResult(args) {
125108
125251
  async function preparePushStatus(workspace, options, onPhase = () => void 0) {
125109
125252
  onPhase("Analyzing working copy\u2026");
125110
125253
  const status = computeWorkspaceStatus2(workspace, {
125254
+ onPhase,
125111
125255
  forceRecompile: options.forceRecompile
125112
125256
  });
125113
125257
  if (status.conflictedFiles.length > 0) {
@@ -127497,7 +127641,7 @@ var init_registry2 = __esm({
127497
127641
  "schema-contract/registry.mjs"() {
127498
127642
  "use strict";
127499
127643
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
127500
- cliVersion: "0.46.1",
127644
+ cliVersion: "0.46.2",
127501
127645
  projectFileUploadBatchSize: 32,
127502
127646
  documentRecords: {
127503
127647
  member: {
@@ -134316,6 +134460,17 @@ var init_resolve = __esm({
134316
134460
 
134317
134461
  // src/main.ts
134318
134462
  var main_exports = {};
134463
+ function computeCommandWorkspaceStatus(args) {
134464
+ const progress = !boolFlag(args, "json") && process.stdout.isTTY === true ? spinner("Reading local workspace\u2026") : null;
134465
+ try {
134466
+ const workspace = loadWorkspaceForCommand(args);
134467
+ return computeWorkspaceStatus2(workspace, {
134468
+ onPhase: (label) => progress?.update(label)
134469
+ });
134470
+ } finally {
134471
+ progress?.stop();
134472
+ }
134473
+ }
134319
134474
  function profileFlag(args) {
134320
134475
  const value = stringFlag(args, "profile");
134321
134476
  if (value === null) return null;
@@ -134510,7 +134665,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
134510
134665
  async function main() {
134511
134666
  const args = parseArgs(process.argv.slice(2));
134512
134667
  if (args.command === "--version") {
134513
- console.log("0.46.1");
134668
+ console.log("0.46.2");
134514
134669
  return;
134515
134670
  }
134516
134671
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
@@ -134585,13 +134740,25 @@ async function main() {
134585
134740
  args,
134586
134741
  /* @__PURE__ */ new Set(["api", "force", "reset", "regenerate-source-names"])
134587
134742
  );
134588
- const workspace = loadWorkspaceForCommand(args);
134589
- const { runPull: runPull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
134590
- await runPull2(workspace, {
134591
- force: boolFlag(args, "force"),
134592
- reset: boolFlag(args, "reset"),
134593
- regenerateSourceNames: boolFlag(args, "regenerate-source-names")
134594
- });
134743
+ const progress = spinner("Reading local workspace\u2026");
134744
+ try {
134745
+ const workspace = loadWorkspaceForCommand(args);
134746
+ const { runPull: runPull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
134747
+ await runPull2(
134748
+ workspace,
134749
+ {
134750
+ force: boolFlag(args, "force"),
134751
+ reset: boolFlag(args, "reset"),
134752
+ regenerateSourceNames: boolFlag(args, "regenerate-source-names")
134753
+ },
134754
+ progress
134755
+ );
134756
+ } catch (error) {
134757
+ progress.fail("Pull failed.");
134758
+ throw error;
134759
+ } finally {
134760
+ progress.stop();
134761
+ }
134595
134762
  return;
134596
134763
  }
134597
134764
  case "doctor": {
@@ -134799,7 +134966,6 @@ async function main() {
134799
134966
  return;
134800
134967
  }
134801
134968
  case "test": {
134802
- const workspace = loadWorkspaceForCommand(args);
134803
134969
  const reporterValue = stringFlag(args, "reporter") ?? "default";
134804
134970
  if (reporterValue !== "default" && reporterValue !== "json") {
134805
134971
  throw new NeoCliUsageError('--reporter must be "default" or "json".');
@@ -134821,8 +134987,9 @@ async function main() {
134821
134987
  "--testTimeout must be a positive number of milliseconds."
134822
134988
  );
134823
134989
  }
134824
- const progress = reporterValue === "default" && process.stdout.isTTY === true ? spinner("Preparing test project\u2026") : null;
134990
+ const progress = reporterValue === "default" && process.stdout.isTTY === true ? spinner("Reading local workspace\u2026") : null;
134825
134991
  try {
134992
+ const workspace = loadWorkspaceForCommand(args);
134826
134993
  const { runTest: runTest2 } = await Promise.resolve().then(() => (init_test(), test_exports));
134827
134994
  await runTest2(
134828
134995
  workspace,
@@ -134839,6 +135006,8 @@ async function main() {
134839
135006
  } catch (error) {
134840
135007
  progress?.fail("Test run failed.");
134841
135008
  throw error;
135009
+ } finally {
135010
+ progress?.stop();
134842
135011
  }
134843
135012
  return;
134844
135013
  }
@@ -134924,8 +135093,7 @@ async function main() {
134924
135093
  return;
134925
135094
  }
134926
135095
  case "status": {
134927
- const workspace = loadWorkspaceForCommand(args);
134928
- const status = computeWorkspaceStatus2(workspace);
135096
+ const status = computeCommandWorkspaceStatus(args);
134929
135097
  if (boolFlag(args, "json")) {
134930
135098
  console.log(
134931
135099
  JSON.stringify(
@@ -134969,8 +135137,7 @@ async function main() {
134969
135137
  return;
134970
135138
  }
134971
135139
  case "diff": {
134972
- const workspace = loadWorkspaceForCommand(args);
134973
- const status = computeWorkspaceStatus2(workspace);
135140
+ const status = computeCommandWorkspaceStatus(args);
134974
135141
  if (boolFlag(args, "json")) {
134975
135142
  console.log(
134976
135143
  JSON.stringify(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.46.1",
3
+ "version": "0.46.2",
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.46.1 -->
12
+ <!-- reviewed-through-cli: 0.46.2 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -88,7 +88,7 @@ wrappers.
88
88
  The marker near the top of `SKILL.md` must exactly match the package version:
89
89
 
90
90
  ```html
91
- <!-- reviewed-through-cli: 0.46.1 -->
91
+ <!-- reviewed-through-cli: 0.46.2 -->
92
92
  ```
93
93
 
94
94
  The quoted version above is checked too, so this instruction cannot go stale
@@ -31,7 +31,9 @@ and managed binaries. Reset retains declaration file grouping already recorded
31
31
  by the workspace, while declarations with no prior placement use the canonical
32
32
  per-declaration layout. Use `--force` only to discard local edits for the server
33
33
  version. Normal pulls persist a transaction cursor and fetch only changed
34
- records; an undo/reset metadata change invalidates the cursor and causes a safe
34
+ records. If the remote head is unchanged, pull preserves the local source
35
+ without compiling it; use `neo status` to inspect local edits and diagnostics.
36
+ An undo/reset metadata change invalidates the cursor and causes a safe
35
37
  full-snapshot fallback.
36
38
 
37
39
  Treat `neo push --dry-run` as a full local commit rehearsal: source emission,
@@ -52,6 +54,11 @@ bodies affected by changed declarations. Use `--force-recompile` with either a
52
54
  dry run or a real push only when diagnosing stale derived IR: it retains the
53
55
  exhaustive project-wide body sweep and is intentionally slower.
54
56
 
57
+ Interactive status, diff, pull, and test commands report workspace loading and
58
+ preparation phases. Spinners continue animating during synchronous compilation;
59
+ `NO_COLOR` removes color without stopping the animation. JSON reporters and
60
+ piped status/test output contain no terminal animation.
61
+
55
62
  Interactive pushes report the active preparation phase before workspace
56
63
  discovery, lowering, compilation, source hashing, and validation begin.
57
64
  A normal push prepares that transport once before confirmation and reuses it