@neocompose/cli 0.22.0 → 0.22.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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.22.2] - 2026-08-04
4
+
5
+ ### Changed
6
+
7
+ - Dependency-selected NeoScript preparation narrows source-only bodies by
8
+ recoverable changed-contract identifiers instead of recompiling every body
9
+ without stored IR. Structurally affected owners and source that cannot be
10
+ lexically recovered retain the conservative fallback.
11
+ - Optimized-versus-exhaustive fuzz parity now covers class, enum, value,
12
+ dialogue-node, and dialogue-group edits in addition to member edits.
13
+ - Normal `neo pull` persists a transaction cursor and downloads only records
14
+ changed since the previous sync. Source-neutral deltas update local state
15
+ without rebuilding the complete canonical project source.
16
+
17
+ ## [0.22.1] - 2026-08-04
18
+
19
+ ### Fixed
20
+
21
+ - The published `neo` executable retains its Node.js shebang, and package
22
+ verification checks the packed bin target and invokes it directly before
23
+ release.
24
+
3
25
  ## [0.22.0] - 2026-08-04
4
26
 
5
27
  ### Added
package/README.md CHANGED
@@ -92,7 +92,10 @@ neo init --project <id> --version <id> --dir neo
92
92
 
93
93
  For an existing format-4 workspace, `neo pull --reset` regenerates canonical
94
94
  source and managed binaries. Normal `neo pull` performs a stable-ID-aware
95
- three-way merge and preserves source placement when possible.
95
+ three-way merge, preserves source placement when possible, and uses a durable
96
+ transaction cursor to fetch only records changed since the previous sync.
97
+ Undo/reset metadata invalidates that cursor and safely falls back to a full
98
+ snapshot.
96
99
 
97
100
  In Unity projects, `neo.json` may use `unityConfigPath` instead of storing
98
101
  `projectId` and `versionId`. The referenced `NeoComposeConfig.asset` then owns
package/dist/neo.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  var __defProp = Object.defineProperty;
2
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
4
  var __esm = (fn, res, err) => function __init() {
@@ -68315,13 +68316,21 @@ function collectNeoScriptRecompileTargets(args) {
68315
68316
  }
68316
68317
  }
68317
68318
  const constructorOwnerById = constructorOwners(args.postDocument);
68319
+ const impactedSourceNames = collectImpactedSourceNames({
68320
+ currentDocument: args.currentDocument,
68321
+ postDocument: args.postDocument,
68322
+ impactedIds,
68323
+ impactedTypeNames,
68324
+ constructedClassIds
68325
+ });
68318
68326
  const memberIds = new Set(explicit.memberIds);
68319
68327
  for (const member of args.postDocument.members) {
68320
68328
  if (recordDependsOnChangedContract(
68321
68329
  member,
68322
68330
  impactedIds,
68323
68331
  impactedTypeNames,
68324
- constructedClassIds
68332
+ constructedClassIds,
68333
+ impactedSourceNames
68325
68334
  )) {
68326
68335
  memberIds.add(member.id);
68327
68336
  }
@@ -68332,7 +68341,8 @@ function collectNeoScriptRecompileTargets(args) {
68332
68341
  explicit.valueIds,
68333
68342
  impactedIds,
68334
68343
  impactedTypeNames,
68335
- constructedClassIds
68344
+ constructedClassIds,
68345
+ impactedSourceNames
68336
68346
  );
68337
68347
  if (impactedIds.size > 0 && postValues.length > 0) {
68338
68348
  const ownerByValueId = resolveOwnerMembersForValues(
@@ -68355,6 +68365,7 @@ function collectNeoScriptRecompileTargets(args) {
68355
68365
  impactedIds,
68356
68366
  impactedTypeNames,
68357
68367
  constructedClassIds,
68368
+ impactedSourceNames,
68358
68369
  (record3) => constructorOwnerById.get(record3.id) ?? null
68359
68370
  ),
68360
68371
  migrationIds: selectedRecordIds(
@@ -68362,21 +68373,24 @@ function collectNeoScriptRecompileTargets(args) {
68362
68373
  explicit.migrationIds,
68363
68374
  impactedIds,
68364
68375
  impactedTypeNames,
68365
- constructedClassIds
68376
+ constructedClassIds,
68377
+ impactedSourceNames
68366
68378
  ),
68367
68379
  dialogueNodeIds: selectedRecordIds(
68368
68380
  args.postDocument.dialogueNodes ?? [],
68369
68381
  explicit.dialogueNodeIds,
68370
68382
  impactedIds,
68371
68383
  impactedTypeNames,
68372
- constructedClassIds
68384
+ constructedClassIds,
68385
+ impactedSourceNames
68373
68386
  ),
68374
68387
  dialogueGroupIds: selectedRecordIds(
68375
68388
  args.postDocument.dialogueGroups ?? [],
68376
68389
  explicit.dialogueGroupIds,
68377
68390
  impactedIds,
68378
68391
  impactedTypeNames,
68379
- constructedClassIds
68392
+ constructedClassIds,
68393
+ impactedSourceNames
68380
68394
  ),
68381
68395
  valueIds
68382
68396
  };
@@ -68745,7 +68759,7 @@ function explicitTargetIds(changes) {
68745
68759
  }
68746
68760
  return result;
68747
68761
  }
68748
- function selectedRecordIds(records2, explicitIds, impactedIds, impactedTypeNames, constructedClassIds, ownerId = () => null) {
68762
+ function selectedRecordIds(records2, explicitIds, impactedIds, impactedTypeNames, constructedClassIds, impactedSourceNames, ownerId = () => null) {
68749
68763
  const selected2 = new Set(explicitIds);
68750
68764
  for (const record3 of records2) {
68751
68765
  const owner = ownerId(record3);
@@ -68753,14 +68767,17 @@ function selectedRecordIds(records2, explicitIds, impactedIds, impactedTypeNames
68753
68767
  record3,
68754
68768
  impactedIds,
68755
68769
  impactedTypeNames,
68756
- constructedClassIds
68770
+ constructedClassIds,
68771
+ impactedSourceNames
68757
68772
  )) {
68758
68773
  selected2.add(record3.id);
68759
68774
  }
68760
68775
  }
68761
68776
  return selected2;
68762
68777
  }
68763
- function recordDependsOnChangedContract(record3, impactedIds, impactedTypeNames, constructedClassIds) {
68778
+ function recordDependsOnChangedContract(record3, impactedIds, impactedTypeNames, constructedClassIds, impactedSourceNames) {
68779
+ const recordId = isObjectRecord3(record3) ? record3.id : void 0;
68780
+ if (typeof recordId === "string" && impactedIds.has(recordId)) return true;
68764
68781
  if (intersects(collectCompilerReferenceIds(record3), impactedIds)) return true;
68765
68782
  if (intersects(collectCompilerReferenceNames(record3), impactedTypeNames)) {
68766
68783
  return true;
@@ -68768,7 +68785,109 @@ function recordDependsOnChangedContract(record3, impactedIds, impactedTypeNames,
68768
68785
  if (intersects(collectConstructedClassIds(record3), constructedClassIds)) {
68769
68786
  return true;
68770
68787
  }
68771
- return (impactedIds.size > 0 || impactedTypeNames.size > 0) && containsAuthoredNeoScriptSource(record3) && !containsCompiledNeoScriptBody(record3);
68788
+ if (impactedIds.size === 0 && impactedTypeNames.size === 0) return false;
68789
+ if (!containsAuthoredNeoScriptSource(record3)) return false;
68790
+ if (containsCompiledNeoScriptBody(record3)) return false;
68791
+ if (impactedSourceNames === null) return true;
68792
+ const sourceIdentifiers = collectAuthoredNeoScriptSourceIdentifiers(record3);
68793
+ if (sourceIdentifiers === null) return true;
68794
+ return intersects(sourceIdentifiers, impactedSourceNames);
68795
+ }
68796
+ function collectImpactedSourceNames(args) {
68797
+ const names = new Set(args.impactedTypeNames);
68798
+ let recoveredContract = args.impactedTypeNames.size > 0;
68799
+ const collectDocument = (document) => {
68800
+ const classById = new Map(
68801
+ document.classes.map((schemaClass2) => [schemaClass2.id, schemaClass2])
68802
+ );
68803
+ const ownerByConstructorId = constructorOwners(document);
68804
+ const impactedValueIds = new Set(
68805
+ (document.values ?? []).filter((value) => args.impactedIds.has(value.id)).map((value) => value.id)
68806
+ );
68807
+ const ownerByValueId = resolveOwnerMembersForValues(
68808
+ document,
68809
+ impactedValueIds
68810
+ );
68811
+ for (const owner of ownerByValueId.values()) {
68812
+ recoveredContract = true;
68813
+ collectDeclaredSourceNames(owner, names);
68814
+ const ownerId = Reflect.get(owner, "id");
68815
+ if (typeof ownerId !== "string") continue;
68816
+ for (const classId of classPlacementsForMember(ownerId, document)) {
68817
+ const schemaClass2 = classById.get(classId);
68818
+ if (schemaClass2 !== void 0) {
68819
+ collectDeclaredSourceNames(schemaClass2, names);
68820
+ }
68821
+ }
68822
+ }
68823
+ const records2 = [
68824
+ ...document.classes,
68825
+ ...document.members,
68826
+ ...document.constructors ?? [],
68827
+ ...document.enums,
68828
+ ...document.interfaces ?? [],
68829
+ ...document.values ?? []
68830
+ ];
68831
+ for (const record3 of records2) {
68832
+ if (!args.impactedIds.has(record3.id)) continue;
68833
+ recoveredContract = true;
68834
+ collectDeclaredSourceNames(record3, names);
68835
+ const ownerClassId = ownerByConstructorId.get(record3.id);
68836
+ if (ownerClassId === void 0) continue;
68837
+ const owner = classById.get(ownerClassId);
68838
+ if (owner !== void 0) collectDeclaredSourceNames(owner, names);
68839
+ }
68840
+ for (const schemaClass2 of document.classes) {
68841
+ if (!args.constructedClassIds.has(schemaClass2.id)) continue;
68842
+ recoveredContract = true;
68843
+ collectDeclaredSourceNames(schemaClass2, names);
68844
+ }
68845
+ };
68846
+ collectDocument(args.currentDocument);
68847
+ collectDocument(args.postDocument);
68848
+ return recoveredContract ? names : null;
68849
+ }
68850
+ function collectDeclaredSourceNames(value, names) {
68851
+ if (Array.isArray(value)) {
68852
+ for (const child of value) collectDeclaredSourceNames(child, names);
68853
+ return;
68854
+ }
68855
+ if (!isObjectRecord3(value)) return;
68856
+ for (const [field, child] of Object.entries(value)) {
68857
+ if (typeof child === "string" && (field === "name" || field === "sourceName" || field === "sourceFunctionName" || field === "declaredTypeName" || field === "typeName")) {
68858
+ names.add(child);
68859
+ continue;
68860
+ }
68861
+ collectDeclaredSourceNames(child, names);
68862
+ }
68863
+ }
68864
+ function collectAuthoredNeoScriptSourceIdentifiers(value) {
68865
+ const identifiers = /* @__PURE__ */ new Set();
68866
+ let complete2 = true;
68867
+ const collect = (child) => {
68868
+ if (Array.isArray(child)) {
68869
+ for (const entry of child) collect(entry);
68870
+ return;
68871
+ }
68872
+ if (!isObjectRecord3(child)) return;
68873
+ for (const [field, nested] of Object.entries(child)) {
68874
+ if ((field === "code" || field === "setterCode") && typeof nested === "string") {
68875
+ const result = lex(nested);
68876
+ if (result.diagnostics.some(
68877
+ (diagnostic) => diagnostic.severity === "error"
68878
+ )) {
68879
+ complete2 = false;
68880
+ }
68881
+ for (const token of result.tokens) {
68882
+ if (token.kind === "identifier") identifiers.add(token.text);
68883
+ }
68884
+ continue;
68885
+ }
68886
+ collect(nested);
68887
+ }
68888
+ };
68889
+ collect(value);
68890
+ return complete2 ? identifiers : null;
68772
68891
  }
68773
68892
  function containsAuthoredNeoScriptSource(value) {
68774
68893
  if (Array.isArray(value)) {
@@ -68867,6 +68986,7 @@ var init_neo_script_recompile_scope = __esm({
68867
68986
  "../src/database/neo-script-recompile-scope.ts"() {
68868
68987
  "use strict";
68869
68988
  init_canonical_json();
68989
+ init_src();
68870
68990
  init_member_kind_enum();
68871
68991
  init_inheritance();
68872
68992
  init_effective_storage();
@@ -87198,20 +87318,172 @@ async function fetchProjectDocument(workspace) {
87198
87318
  });
87199
87319
  }
87200
87320
  }
87321
+ const revision = readProjectDocumentRevisionMarker2(raw);
87201
87322
  return {
87202
87323
  records: records2,
87203
87324
  heads,
87204
- raw
87325
+ raw,
87326
+ ...revision === null ? {} : { revision }
87327
+ };
87328
+ }
87329
+ async function fetchProjectDocumentSyncMarker(workspace) {
87330
+ const convex = await createConvexClient(workspace);
87331
+ return queryProjectDocumentSyncMarker(workspace, convex);
87332
+ }
87333
+ async function queryProjectDocumentSyncMarker(workspace, convex) {
87334
+ const [revisionValue, signal] = await Promise.all([
87335
+ convex.query(api2.projectDocuments.getDocumentRevision, {
87336
+ projectId: workspace.config.projectId,
87337
+ versionId: workspace.config.versionId
87338
+ }),
87339
+ convex.query(api2.projectExportData.schemaSignal, {
87340
+ projectId: workspace.config.projectId,
87341
+ versionId: workspace.config.versionId
87342
+ })
87343
+ ]);
87344
+ if (!isProjectDocumentRevisionMarker(revisionValue)) {
87345
+ throw new Error(
87346
+ "Project document revision query returned an invalid cursor marker."
87347
+ );
87348
+ }
87349
+ return {
87350
+ revision: revisionValue,
87351
+ headTransactionId: signal?.transactionId ?? null,
87352
+ headTransactionHash: signal?.transactionHash ?? null
87353
+ };
87354
+ }
87355
+ async function fetchProjectDocumentDelta(workspace, cursor) {
87356
+ const convex = await createConvexClient(workspace);
87357
+ const marker = await queryProjectDocumentSyncMarker(workspace, convex);
87358
+ if (marker.revision.versionsStamp !== cursor.versionsStamp) {
87359
+ return {
87360
+ fullResync: true,
87361
+ headTransactionHash: marker.headTransactionHash
87362
+ };
87363
+ }
87364
+ if (marker.headTransactionId === marker.revision.latestTransactionId && marker.headTransactionHash === workspace.state.headTransactionHash) {
87365
+ return {
87366
+ fullResync: false,
87367
+ document: projectDocumentFromWorkspaceState(workspace.state, cursor),
87368
+ changedKeys: /* @__PURE__ */ new Set(),
87369
+ cursor,
87370
+ headTransactionHash: marker.headTransactionHash
87371
+ };
87372
+ }
87373
+ const result = await convex.query(api2.projectDocuments.getRecordsSince, {
87374
+ projectId: workspace.config.projectId,
87375
+ versionId: workspace.config.versionId,
87376
+ sinceCreatedAt: cursor.createdAt,
87377
+ seenTransactionIds: [...cursor.transactionIds]
87378
+ });
87379
+ if (result.fullResync) {
87380
+ return {
87381
+ fullResync: true,
87382
+ headTransactionHash: marker.headTransactionHash
87383
+ };
87384
+ }
87385
+ const document = projectDocumentFromWorkspaceState(workspace.state, cursor);
87386
+ const { records: records2, heads } = document;
87387
+ const changedKeys = /* @__PURE__ */ new Set();
87388
+ for (const record3 of result.records) {
87389
+ const key = `${record3.recordKind}:${record3.recordId}`;
87390
+ changedKeys.add(key);
87391
+ if (record3.deleted) {
87392
+ records2.delete(key);
87393
+ heads.set(key, {
87394
+ recordKind: record3.recordKind,
87395
+ recordId: record3.recordId,
87396
+ contentHash: "",
87397
+ deleted: true
87398
+ });
87399
+ continue;
87400
+ }
87401
+ if (record3.contentHash === null) {
87402
+ throw new Error(
87403
+ `Project document delta record "${key}" is live but has no content hash.`
87404
+ );
87405
+ }
87406
+ records2.set(key, {
87407
+ recordKind: record3.recordKind,
87408
+ recordId: record3.recordId,
87409
+ contentHash: record3.contentHash,
87410
+ deleted: false,
87411
+ data: record3.data
87412
+ });
87413
+ heads.set(key, {
87414
+ recordKind: record3.recordKind,
87415
+ recordId: record3.recordId,
87416
+ contentHash: record3.contentHash,
87417
+ deleted: false
87418
+ });
87419
+ }
87420
+ const next = result.cursor ?? cursor;
87421
+ const nextCursor = {
87422
+ createdAt: next.createdAt,
87423
+ transactionIds: [...next.transactionIds],
87424
+ versionsStamp: marker.revision.versionsStamp
87425
+ };
87426
+ return {
87427
+ fullResync: false,
87428
+ document: projectDocumentWithRevision(document, nextCursor),
87429
+ changedKeys,
87430
+ cursor: nextCursor,
87431
+ headTransactionHash: marker.headTransactionHash
87432
+ };
87433
+ }
87434
+ function projectDocumentFromWorkspaceState(state, cursor) {
87435
+ const records2 = serverRecordsFromWorkspaceState(state);
87436
+ const heads = new Map(
87437
+ [...records2].map(([key, record3]) => [
87438
+ key,
87439
+ {
87440
+ recordKind: record3.recordKind,
87441
+ recordId: record3.recordId,
87442
+ contentHash: record3.contentHash,
87443
+ deleted: false
87444
+ }
87445
+ ])
87446
+ );
87447
+ return projectDocumentWithRevision({ records: records2, heads, raw: {} }, cursor);
87448
+ }
87449
+ function projectDocumentWithRevision(document, cursor) {
87450
+ return {
87451
+ ...document,
87452
+ revision: {
87453
+ latestTransactionId: cursor.transactionIds[cursor.transactionIds.length - 1] ?? null,
87454
+ latestTransactionCreatedAt: cursor.createdAt,
87455
+ versionsStamp: cursor.versionsStamp
87456
+ }
87205
87457
  };
87206
87458
  }
87207
- var fetchProjectDocumentRawChunked2, readProjectDocumentContentHashHeads2, SOURCE_RECORD_KINDS;
87459
+ function serverRecordsFromWorkspaceState(state) {
87460
+ const records2 = /* @__PURE__ */ new Map();
87461
+ for (const [key, record3] of Object.entries(state.records)) {
87462
+ if (record3.conflictServerHash === null) continue;
87463
+ const conflictServerHash = record3.conflictServerHash;
87464
+ const hasConflictServer = conflictServerHash !== void 0;
87465
+ records2.set(key, {
87466
+ recordKind: record3.recordKind,
87467
+ recordId: record3.recordId,
87468
+ contentHash: hasConflictServer ? conflictServerHash : record3.contentHash,
87469
+ deleted: false,
87470
+ data: hasConflictServer ? record3.conflictServerData : record3.data
87471
+ });
87472
+ }
87473
+ return records2;
87474
+ }
87475
+ var fetchProjectDocumentRawChunked2, readProjectDocumentContentHashHeads2, readProjectDocumentRevisionMarker2, SOURCE_RECORD_KINDS;
87208
87476
  var init_document = __esm({
87209
87477
  "src/document.ts"() {
87210
87478
  "use strict";
87211
87479
  init_project_document_read();
87212
87480
  init_project2();
87213
87481
  init_convex();
87214
- ({ fetchProjectDocumentRawChunked: fetchProjectDocumentRawChunked2, readProjectDocumentContentHashHeads: readProjectDocumentContentHashHeads2 } = project_document_read_exports);
87482
+ ({
87483
+ fetchProjectDocumentRawChunked: fetchProjectDocumentRawChunked2,
87484
+ readProjectDocumentContentHashHeads: readProjectDocumentContentHashHeads2,
87485
+ readProjectDocumentRevisionMarker: readProjectDocumentRevisionMarker2
87486
+ } = project_document_read_exports);
87215
87487
  SOURCE_RECORD_KINDS = new Set(
87216
87488
  Object.values(ProjectRecordKind)
87217
87489
  );
@@ -87898,6 +88170,7 @@ var init_static_value_seed_records = __esm({
87898
88170
  // src/commands/pull.ts
87899
88171
  var pull_exports = {};
87900
88172
  __export(pull_exports, {
88173
+ deltaRequiresSourceProjectionV4: () => deltaRequiresSourceProjectionV4,
87901
88174
  projectSourcePathsRequiringRewriteV4: () => projectSourcePathsRequiringRewriteV4,
87902
88175
  runPull: () => runPull
87903
88176
  });
@@ -87914,6 +88187,15 @@ async function runPull(workspace, options) {
87914
88187
  await runResetPull(workspace);
87915
88188
  return;
87916
88189
  }
88190
+ const progress = spinner("Checking local workspace\u2026");
88191
+ try {
88192
+ await runNormalPull(workspace, options, progress);
88193
+ } catch (error) {
88194
+ progress.fail("Pull failed.");
88195
+ throw error;
88196
+ }
88197
+ }
88198
+ async function runNormalPull(workspace, options, progress) {
87917
88199
  const destructive = options.force;
87918
88200
  const hasBaseline = Object.keys(workspace.state.records).length > 0;
87919
88201
  let localByKey = /* @__PURE__ */ new Map();
@@ -87942,25 +88224,54 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
87942
88224
  ])
87943
88225
  );
87944
88226
  }
87945
- if (!destructive && !options.regenerateSourceNames && workspace.state.headTransactionHash !== void 0) {
87946
- const convex = await createConvexClient(workspace);
87947
- const signal = await convex.query(api2.projectExportData.schemaSignal, {
87948
- projectId: workspace.config.projectId,
87949
- versionId: workspace.config.versionId
87950
- });
87951
- if (signal !== null && signal.transactionHash === workspace.state.headTransactionHash && localByKey.size > 0) {
87952
- const dirtyRecords = [...localByKey.entries()].some(([key, local]) => {
87953
- const base = workspace.state.records[key];
87954
- return base === void 0 || !authoredRecordsSemanticallyEqual(base.recordKind, local, base.data);
87955
- });
87956
- const dirtyBinaries = (localStatus?.binaryChanges?.length ?? 0) > 0;
87957
- if (!dirtyRecords && !dirtyBinaries) {
87958
- console.log("Already up to date (sync-signal head unchanged).");
88227
+ let changedRecordKeys = null;
88228
+ let nextCursor = null;
88229
+ let observedHeadTransactionHash;
88230
+ let document = null;
88231
+ if (!destructive && hasBaseline) {
88232
+ progress.update("Checking remote changes\u2026");
88233
+ const cursor = workspace.state.documentRevisionCursor;
88234
+ if (cursor !== void 0) {
88235
+ const delta = await fetchProjectDocumentDelta(workspace, cursor);
88236
+ observedHeadTransactionHash = delta.headTransactionHash;
88237
+ if (!delta.fullResync) {
88238
+ document = delta.document;
88239
+ changedRecordKeys = delta.changedKeys;
88240
+ nextCursor = delta.cursor;
88241
+ if (delta.changedKeys.size === 0 && !options.regenerateSourceNames) {
88242
+ const cursorChanged = !cursorsEqual(
88243
+ workspace.state.documentRevisionCursor,
88244
+ delta.cursor
88245
+ );
88246
+ const headChanged = workspace.state.headTransactionHash !== delta.headTransactionHash;
88247
+ if (cursorChanged || headChanged) {
88248
+ workspace.state.documentRevisionCursor = mutableCursor(
88249
+ delta.cursor
88250
+ );
88251
+ workspace.state.headTransactionHash = delta.headTransactionHash;
88252
+ writeWorkspaceState(workspace.root, workspace.state);
88253
+ }
88254
+ progress.succeed("Already up to date.");
88255
+ return;
88256
+ }
88257
+ }
88258
+ } else if (!options.regenerateSourceNames && workspace.state.headTransactionHash !== void 0) {
88259
+ const marker = await fetchProjectDocumentSyncMarker(workspace);
88260
+ observedHeadTransactionHash = marker.headTransactionHash;
88261
+ if (marker.headTransactionId === marker.revision.latestTransactionId && marker.headTransactionHash === workspace.state.headTransactionHash) {
88262
+ const seededCursor = cursorFromRevision(marker.revision);
88263
+ workspace.state.documentRevisionCursor = mutableCursor(seededCursor);
88264
+ writeWorkspaceState(workspace.root, workspace.state);
88265
+ progress.succeed("Already up to date.");
87959
88266
  return;
87960
88267
  }
87961
88268
  }
87962
88269
  }
87963
- const document = await fetchProjectDocument(workspace);
88270
+ if (document === null) {
88271
+ progress.update("Downloading full project snapshot\u2026");
88272
+ document = await fetchProjectDocument(workspace);
88273
+ nextCursor = document.revision === void 0 ? null : cursorFromRevision(document.revision);
88274
+ }
87964
88275
  const plans = /* @__PURE__ */ new Map();
87965
88276
  let mergedCount = 0;
87966
88277
  let conflictCount = 0;
@@ -88080,7 +88391,11 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
88080
88391
  conflictKeys,
88081
88392
  localStatus,
88082
88393
  destructive,
88083
- regenerateSourceNames: destructive || options.regenerateSourceNames
88394
+ regenerateSourceNames: destructive || options.regenerateSourceNames,
88395
+ changedRecordKeys,
88396
+ nextCursor,
88397
+ observedHeadTransactionHash,
88398
+ progress
88084
88399
  });
88085
88400
  }
88086
88401
  async function runResetPull(workspace) {
@@ -88110,10 +88425,17 @@ async function runResetPull(workspace) {
88110
88425
  projectId: workspace.config.projectId,
88111
88426
  versionId: workspace.config.versionId
88112
88427
  });
88113
- workspace.state.headTransactionHash = signal?.transactionHash ?? null;
88428
+ workspace.state.headTransactionHash = document.revision !== void 0 && signal?.transactionId === document.revision.latestTransactionId ? signal.transactionHash : null;
88114
88429
  } catch {
88115
88430
  workspace.state.headTransactionHash = null;
88116
88431
  }
88432
+ if (document.revision !== void 0) {
88433
+ workspace.state.documentRevisionCursor = mutableCursor(
88434
+ cursorFromRevision(document.revision)
88435
+ );
88436
+ } else {
88437
+ delete workspace.state.documentRevisionCursor;
88438
+ }
88117
88439
  writeWorkspaceState(workspace.root, workspace.state);
88118
88440
  progress.succeed(
88119
88441
  `Pulled ${document.records.size} records \u2014 ${reset.written} file(s) updated \u2014 ${reset.removed} removed.`
@@ -88133,8 +88455,30 @@ async function finishFormat4Pull(args) {
88133
88455
  conflictKeys,
88134
88456
  localStatus,
88135
88457
  destructive,
88136
- regenerateSourceNames
88458
+ regenerateSourceNames,
88459
+ changedRecordKeys,
88460
+ nextCursor,
88461
+ observedHeadTransactionHash,
88462
+ progress
88137
88463
  } = args;
88464
+ if (changedRecordKeys !== null && !regenerateSourceNames && conflictCount === 0 && localStatus !== null && !deltaRequiresSourceProjectionV4({
88465
+ workspace,
88466
+ document,
88467
+ plans,
88468
+ localStatus,
88469
+ changedRecordKeys
88470
+ })) {
88471
+ finishRecordOnlyDeltaPull({
88472
+ workspace,
88473
+ document,
88474
+ changedRecordKeys,
88475
+ nextCursor,
88476
+ observedHeadTransactionHash,
88477
+ progress
88478
+ });
88479
+ return;
88480
+ }
88481
+ progress.update("Regenerating affected Neo sources\u2026");
88138
88482
  const plannedLocalRecords = buildEmitRecordSet(document, plans, "emit");
88139
88483
  for (const [key, record3] of staticValueSeedEmitRecords(
88140
88484
  localStatus,
@@ -88149,7 +88493,7 @@ async function finishFormat4Pull(args) {
88149
88493
  )
88150
88494
  );
88151
88495
  const localResult = emitProjectDocumentFilesV4(localRecords);
88152
- const serverResult = emitProjectDocumentFilesV4(
88496
+ const serverResult = conflictCount === 0 ? null : emitProjectDocumentFilesV4(
88153
88497
  buildEmitRecordSet(document, plans, "server")
88154
88498
  );
88155
88499
  const binaries = await pullProjectBinariesV4({
@@ -88163,11 +88507,11 @@ async function finishFormat4Pull(args) {
88163
88507
  localResult.files.map((file) => [file.path, file])
88164
88508
  );
88165
88509
  const serverByPath = new Map(
88166
- serverResult.files.map((file) => [file.path, file])
88510
+ (serverResult?.files ?? []).map((file) => [file.path, file])
88167
88511
  );
88168
88512
  const conflictPaths = /* @__PURE__ */ new Set();
88169
88513
  for (const key of conflictKeys) {
88170
- const path = localResult.recordFiles.get(key) ?? serverResult.recordFiles.get(key);
88514
+ const path = localResult.recordFiles.get(key) ?? serverResult?.recordFiles.get(key);
88171
88515
  if (path) conflictPaths.add(path);
88172
88516
  }
88173
88517
  const emittedPaths = /* @__PURE__ */ new Set([
@@ -88179,7 +88523,7 @@ async function finishFormat4Pull(args) {
88179
88523
  localStatus,
88180
88524
  plans,
88181
88525
  localRecordFiles: localResult.recordFiles,
88182
- serverRecordFiles: serverResult.recordFiles,
88526
+ serverRecordFiles: serverResult?.recordFiles ?? /* @__PURE__ */ new Map(),
88183
88527
  previousRecords: workspace.state.records,
88184
88528
  conflictPaths,
88185
88529
  emittedPaths,
@@ -88229,7 +88573,7 @@ async function finishFormat4Pull(args) {
88229
88573
  if (baseState) records2[key] = { ...baseState };
88230
88574
  continue;
88231
88575
  }
88232
- const file = localResult.recordFiles.get(key) ?? serverResult.recordFiles.get(key);
88576
+ const file = localResult.recordFiles.get(key) ?? serverResult?.recordFiles.get(key) ?? baseState?.file;
88233
88577
  records2[key] = plan.conflicted ? {
88234
88578
  recordKind,
88235
88579
  recordId,
@@ -88248,7 +88592,7 @@ async function finishFormat4Pull(args) {
88248
88592
  }
88249
88593
  for (const [key, serverRecord] of document.records) {
88250
88594
  if (serverRecord.deleted || records2[key]) continue;
88251
- const file = localResult.recordFiles.get(key) ?? serverResult.recordFiles.get(key);
88595
+ const file = localResult.recordFiles.get(key) ?? serverResult?.recordFiles.get(key);
88252
88596
  records2[key] = {
88253
88597
  recordKind: serverRecord.recordKind,
88254
88598
  recordId: serverRecord.recordId,
@@ -88262,30 +88606,37 @@ async function finishFormat4Pull(args) {
88262
88606
  if (record3) record3.projectBinary = state;
88263
88607
  }
88264
88608
  workspace.state.records = records2;
88265
- try {
88266
- const convex = await createConvexClient(workspace);
88267
- const signal = await convex.query(api2.projectExportData.schemaSignal, {
88268
- projectId: workspace.config.projectId,
88269
- versionId: workspace.config.versionId
88270
- });
88271
- workspace.state.headTransactionHash = signal?.transactionHash ?? null;
88272
- } catch {
88273
- workspace.state.headTransactionHash = null;
88609
+ if (nextCursor !== null) {
88610
+ workspace.state.documentRevisionCursor = mutableCursor(nextCursor);
88611
+ }
88612
+ if (observedHeadTransactionHash !== void 0) {
88613
+ workspace.state.headTransactionHash = observedHeadTransactionHash;
88614
+ } else {
88615
+ try {
88616
+ const convex = await createConvexClient(workspace);
88617
+ const signal = await convex.query(api2.projectExportData.schemaSignal, {
88618
+ projectId: workspace.config.projectId,
88619
+ versionId: workspace.config.versionId
88620
+ });
88621
+ workspace.state.headTransactionHash = document.revision !== void 0 && signal?.transactionId === document.revision.latestTransactionId ? signal.transactionHash : null;
88622
+ } catch {
88623
+ workspace.state.headTransactionHash = null;
88624
+ }
88274
88625
  }
88275
88626
  writeWorkspaceState(workspace.root, workspace.state);
88276
88627
  if (conflictCount === 0) {
88277
88628
  writeProjectSourceAnalysisCacheV4(workspace.root, localResult.analysis);
88278
- writeMaterializedConstructionBuildCacheV1(
88279
- workspace.root,
88280
- workspace.state,
88281
- // The cache is a baseline for workspace.state, not the merged authored
88282
- // tree. Retaining a local constructor edit here would bless it as the
88283
- // pulled construction and bypass the P61 recreate-conflict check.
88284
- serverResult.materializedConstructors
88285
- );
88629
+ if (localStatus === null || localStatus.changes.length === 0) {
88630
+ writeMaterializedConstructionBuildCacheV1(
88631
+ workspace.root,
88632
+ workspace.state,
88633
+ localResult.materializedConstructors
88634
+ );
88635
+ }
88286
88636
  }
88637
+ const pulledRecords = changedRecordKeys === null ? `Pulled ${document.records.size} records` : `Pulled ${changedRecordKeys.size} changed record(s)`;
88287
88638
  const summary = [
88288
- `Pulled ${document.records.size} records`,
88639
+ pulledRecords,
88289
88640
  `${written} file(s) updated`,
88290
88641
  `${removed} removed`
88291
88642
  ];
@@ -88296,7 +88647,7 @@ async function finishFormat4Pull(args) {
88296
88647
  if (mergedCount > 0)
88297
88648
  summary.push(color.green(`${mergedCount} merged cleanly`));
88298
88649
  if (conflictCount > 0) summary.push(color.red(`${conflictCount} CONFLICTED`));
88299
- console.log(`${summary.join(" \u2014 ")}.`);
88650
+ progress.succeed(`${summary.join(" \u2014 ")}.`);
88300
88651
  if (conflictCount > 0) {
88301
88652
  warn(
88302
88653
  'Conflict markers were written into the affected files. Edit them to the desired final state and push \u2014 the push is the resolution. To keep one side everywhere, use "neo resolve --mine" or "neo resolve --theirs".'
@@ -88310,6 +88661,98 @@ async function finishFormat4Pull(args) {
88310
88661
  for (const path of binaries.conflicts) console.log(` ${sym.fail} ${path}`);
88311
88662
  }
88312
88663
  }
88664
+ function deltaRequiresSourceProjectionV4(args) {
88665
+ const mainLocale = workspaceMainLocale(args.workspace.state.records);
88666
+ for (const key of args.changedRecordKeys) {
88667
+ const previous = args.workspace.state.records[key];
88668
+ const local = args.localStatus.reconstructed.get(key)?.fullData;
88669
+ const plan = args.plans.get(key);
88670
+ const server = args.document.records.get(key);
88671
+ if (server === void 0) {
88672
+ if (previous?.file !== void 0 || local !== void 0) return true;
88673
+ continue;
88674
+ }
88675
+ if (local === void 0 || plan?.emitData === void 0) {
88676
+ if (previous?.file !== void 0 || previous === void 0) return true;
88677
+ continue;
88678
+ }
88679
+ if (!sourceAuthoredRecordsSemanticallyEqual(
88680
+ server.recordKind,
88681
+ plan.emitData,
88682
+ local,
88683
+ mainLocale
88684
+ )) {
88685
+ return true;
88686
+ }
88687
+ }
88688
+ return false;
88689
+ }
88690
+ function finishRecordOnlyDeltaPull(args) {
88691
+ const cachedExpressions = readMaterializedConstructionBuildCacheV1(
88692
+ args.workspace.root,
88693
+ args.workspace.state
88694
+ );
88695
+ const records2 = { ...args.workspace.state.records };
88696
+ for (const key of args.changedRecordKeys) {
88697
+ const server = args.document.records.get(key);
88698
+ if (server === void 0) {
88699
+ delete records2[key];
88700
+ continue;
88701
+ }
88702
+ const previous = records2[key];
88703
+ records2[key] = {
88704
+ recordKind: server.recordKind,
88705
+ recordId: server.recordId,
88706
+ contentHash: server.contentHash,
88707
+ data: server.data,
88708
+ ...previous?.file === void 0 ? {} : { file: previous.file },
88709
+ ...previous?.projectBinary === void 0 ? {} : { projectBinary: previous.projectBinary }
88710
+ };
88711
+ }
88712
+ args.workspace.state.records = records2;
88713
+ if (args.nextCursor !== null) {
88714
+ args.workspace.state.documentRevisionCursor = mutableCursor(
88715
+ args.nextCursor
88716
+ );
88717
+ }
88718
+ if (args.observedHeadTransactionHash !== void 0) {
88719
+ args.workspace.state.headTransactionHash = args.observedHeadTransactionHash;
88720
+ }
88721
+ writeWorkspaceState(args.workspace.root, args.workspace.state);
88722
+ if (cachedExpressions !== null) {
88723
+ writeMaterializedConstructionBuildCacheV1(
88724
+ args.workspace.root,
88725
+ args.workspace.state,
88726
+ cachedExpressions
88727
+ );
88728
+ }
88729
+ args.progress.succeed(
88730
+ `Pulled ${args.changedRecordKeys.size} changed record(s) \u2014 0 file(s) updated \u2014 0 removed.`
88731
+ );
88732
+ }
88733
+ function cursorFromRevision(revision) {
88734
+ return {
88735
+ createdAt: revision.latestTransactionCreatedAt,
88736
+ transactionIds: revision.latestTransactionId === null ? [] : [revision.latestTransactionId],
88737
+ versionsStamp: revision.versionsStamp
88738
+ };
88739
+ }
88740
+ function mutableCursor(cursor) {
88741
+ return {
88742
+ createdAt: cursor.createdAt,
88743
+ transactionIds: [...cursor.transactionIds],
88744
+ versionsStamp: cursor.versionsStamp
88745
+ };
88746
+ }
88747
+ function cursorsEqual(left, right) {
88748
+ if (left === void 0) return false;
88749
+ if (left.createdAt !== right.createdAt) return false;
88750
+ if (left.versionsStamp !== right.versionsStamp) return false;
88751
+ if (left.transactionIds.length !== right.transactionIds.length) return false;
88752
+ return left.transactionIds.every(
88753
+ (transactionId, index) => transactionId === right.transactionIds[index]
88754
+ );
88755
+ }
88313
88756
  function projectSourcePathsRequiringRewriteV4(args) {
88314
88757
  if (args.destructive || args.localStatus === null) {
88315
88758
  return new Set(args.emittedPaths);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.22.0",
3
+ "version": "0.22.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.22.0 -->
12
+ <!-- reviewed-through-cli: 0.22.2 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -78,7 +78,7 @@ wrappers.
78
78
  The marker near the top of `SKILL.md` must exactly match the package version:
79
79
 
80
80
  ```html
81
- <!-- reviewed-through-cli: 0.22.0 -->
81
+ <!-- reviewed-through-cli: 0.22.2 -->
82
82
  ```
83
83
 
84
84
  The quoted version above is checked too, so this instruction cannot go stale
@@ -28,7 +28,9 @@ neo push --dry-run --force-recompile
28
28
  Use `neo pull --regenerate-source-names` to refresh readable naming while
29
29
  retaining normal merge behavior. Use `--reset` to regenerate canonical source
30
30
  and managed binaries. Use `--force` only to discard local edits for the server
31
- version.
31
+ version. Normal pulls persist a transaction cursor and fetch only changed
32
+ records; an undo/reset metadata change invalidates the cursor and causes a safe
33
+ full-snapshot fallback.
32
34
 
33
35
  Treat `neo push --dry-run` as a full local commit rehearsal: source emission,
34
36
  in-memory pending-ID assignment, transport shapes, hashes, and repeat