@neocompose/cli 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.1] - 2026-08-09
4
+
5
+ ### Changed
6
+
7
+ - Stop `First`, `FirstOrDefault`, and collection `Contains` scans as soon as their result is known. Terminal scans now resolve and charge only visited entries, preserve list/dictionary order and value-reference identity, and avoid intermediate entry arrays (P55).
8
+
9
+ ### Fixed
10
+
11
+ - Seed the owned rows for a field a newly declared member adds to a
12
+ row-backed class default, instead of refusing the push with "no existing
13
+ owned value row". Adding an entry to a row-backed _list_ default has always
14
+ minted its rows; a class field now takes the same path, so replacing a class
15
+ member (`Name = "Blue"` → `Kind = .Blue` across every default that sets it)
16
+ pushes without editing each default in the web editor first. An `@id` on the
17
+ new field is honoured when it names an existing row and, when it does not,
18
+ held to the same UUID v4 identity rule list entries follow.
19
+ - Carry only the new rows and the ancestors that reach them in a member value
20
+ seed. Naming every row the pass touched made the seed depend on which
21
+ constructions the materialization cache preserved that run — so a push and
22
+ its own trusted re-lowering disagreed — and handed back rows stripped of the
23
+ `constructorArgs` a seed row cannot carry, which then failed to project back
24
+ to source. Pushing a new row anywhere under `root.Assets` previously failed
25
+ with "Authored value row … is missing memberId".
26
+ - Drop a stored class body key whose member the push deletes, instead of
27
+ carrying it into a schema that no longer has it while the same push
28
+ tombstones the row it names.
29
+ - Treat a member value seed as pushable work on its own. Adding a row to an
30
+ unordered list moves no stored record — membership is the new row's
31
+ `containerId` — so `neo status` and `neo push` reported a clean working copy
32
+ and silently dropped it. `neo status` now reports pending seeds too.
33
+ - Accept a stored row that spells "no class" as an explicit `null`, and adopt
34
+ the declared storage partition for an unstamped row reached through its
35
+ owning slot, in constructed static value graph validation. Rows materialized
36
+ before their partition was stamped (object-tree `Enabled`, P41 §4.3) sit in
37
+ main under a partitioned parent and failed a push over data it never wrote. A
38
+ row already stamped for a different partition is still rejected.
39
+
3
40
  ## [0.25.0] - 2026-08-07
4
41
 
5
42
  ### Added
package/dist/neo.mjs CHANGED
@@ -61613,11 +61613,16 @@ function evalFunction(fn, scope, ctx) {
61613
61613
  }
61614
61614
  return c.includes(target);
61615
61615
  }
61616
- if (Array.isArray(c) && typeof target === "string" && c.includes(target)) {
61617
- return true;
61618
- }
61619
- const entries = collectionEntries(c, ctx);
61620
- return entries.some((e) => jsEqual(e, target));
61616
+ const targetReferenceId = typeof target === "string" ? target : findKnownRowIdByValueReference(target, ctx);
61617
+ let containsResolvedEntry = false;
61618
+ iterateCollection(c, ctx, (entry, _key, valueId) => {
61619
+ if (valueId !== null && valueId === targetReferenceId || jsEqual(entry, target)) {
61620
+ containsResolvedEntry = true;
61621
+ return 1 /* Break */;
61622
+ }
61623
+ return 0 /* Continue */;
61624
+ });
61625
+ return containsResolvedEntry;
61621
61626
  }
61622
61627
  case "decimalOp" /* decimalOp */: {
61623
61628
  const info = fn.info;
@@ -61772,6 +61777,7 @@ function evalFunction(fn, scope, ctx) {
61772
61777
  out[String(key)] = valueId ?? entry;
61773
61778
  }
61774
61779
  }
61780
+ return 0 /* Continue */;
61775
61781
  });
61776
61782
  return out;
61777
61783
  }
@@ -61783,10 +61789,9 @@ function evalFunction(fn, scope, ctx) {
61783
61789
  const sentinel = /* @__PURE__ */ Symbol("not-found");
61784
61790
  let found = sentinel;
61785
61791
  iterateCollection(c, ctx, (entry, key) => {
61786
- if (found !== sentinel) return;
61787
61792
  if (!innerFn) {
61788
61793
  found = entry;
61789
- return;
61794
+ return 1 /* Break */;
61790
61795
  }
61791
61796
  consumeBudget(ctx, "workUnits", 1, "work unit");
61792
61797
  const innerScope = pushParams(
@@ -61803,7 +61808,9 @@ function evalFunction(fn, scope, ctx) {
61803
61808
  );
61804
61809
  if (result.kind === "return" && result.value === true) {
61805
61810
  found = entry;
61811
+ return 1 /* Break */;
61806
61812
  }
61813
+ return 0 /* Continue */;
61807
61814
  });
61808
61815
  if (found !== sentinel) return found;
61809
61816
  if (fn.type === "first" /* first */) {
@@ -61841,6 +61848,7 @@ function evalFunction(fn, scope, ctx) {
61841
61848
  );
61842
61849
  out.push(result.value);
61843
61850
  }
61851
+ return 0 /* Continue */;
61844
61852
  });
61845
61853
  return out;
61846
61854
  }
@@ -64304,6 +64312,7 @@ function snapshotCollectionMembership(collection, ctx) {
64304
64312
  const valid = forEachRawCollectionEntry(collection, ({ raw }) => {
64305
64313
  consumeBudget(ctx, "collectionVisits", 1, "collection visit");
64306
64314
  membership.push(raw);
64315
+ return 0 /* Continue */;
64307
64316
  });
64308
64317
  if (valid) return membership;
64309
64318
  throw new NSGetterRuntimeError(
@@ -64314,21 +64323,23 @@ function forEachRawCollectionEntry(c, callback) {
64314
64323
  if (Array.isArray(c)) {
64315
64324
  for (let key = 0; key < c.length; key += 1) {
64316
64325
  const raw = c[key];
64317
- callback({
64326
+ const control = callback({
64318
64327
  raw,
64319
64328
  key,
64320
64329
  valueId: typeof raw === "string" ? raw : null
64321
64330
  });
64331
+ if (control === 1 /* Break */) break;
64322
64332
  }
64323
64333
  return true;
64324
64334
  }
64325
64335
  if (typeof c === "object" && c !== null) {
64326
64336
  for (const [key, raw] of Object.entries(c)) {
64327
- callback({
64337
+ const control = callback({
64328
64338
  raw,
64329
64339
  key,
64330
64340
  valueId: typeof raw === "string" ? raw : null
64331
64341
  });
64342
+ if (control === 1 /* Break */) break;
64332
64343
  }
64333
64344
  return true;
64334
64345
  }
@@ -64339,6 +64350,7 @@ function collectionEntries(c, ctx) {
64339
64350
  forEachRawCollectionEntry(c, ({ raw }) => {
64340
64351
  consumeBudget(ctx, "collectionVisits", 1, "collection visit");
64341
64352
  entries.push(resolveValueIfId(raw, ctx));
64353
+ return 0 /* Continue */;
64342
64354
  });
64343
64355
  return entries;
64344
64356
  }
@@ -64346,7 +64358,7 @@ function iterateCollection(c, ctx, callback) {
64346
64358
  forEachRawCollectionEntry(c, ({ raw, key, valueId }) => {
64347
64359
  consumeBudget(ctx, "collectionVisits", 1, "collection visit");
64348
64360
  const entry = resolveValueIfId(raw, ctx);
64349
- callback(entry, key, valueId);
64361
+ return callback(entry, key, valueId);
64350
64362
  });
64351
64363
  }
64352
64364
  function pushParams(parent, parameters, positional, isList) {
@@ -78101,13 +78113,16 @@ var init_project_version_static_value_writes = __esm({
78101
78113
  );
78102
78114
  }
78103
78115
  const normalizedExpected = normalizeMapKey(args.expectedMapKey);
78116
+ const normalizedStored = normalizeMapKey(value.mapKey);
78104
78117
  if (this.createdValueIds.has(value.id)) {
78105
78118
  this.visitedCreatedValueIds.add(value.id);
78106
78119
  if (normalizedExpected === null) delete value.mapKey;
78107
78120
  else value.mapKey = normalizedExpected;
78108
- } else if (normalizeMapKey(value.mapKey) !== normalizedExpected) {
78121
+ } else if (normalizedStored === null && normalizedExpected !== null) {
78122
+ value.mapKey = normalizedExpected;
78123
+ } else if (normalizedStored !== normalizedExpected) {
78109
78124
  throw new Error(
78110
- `${args.path} cannot bind value "${value.id}" from storage partition ${JSON.stringify(normalizeMapKey(value.mapKey) ?? "main")}; expected ${JSON.stringify(normalizedExpected ?? "main")}.`
78125
+ `${args.path} cannot bind value "${value.id}" from storage partition ${JSON.stringify(normalizedStored ?? "main")}; expected ${JSON.stringify(normalizedExpected ?? "main")}.`
78111
78126
  );
78112
78127
  }
78113
78128
  if (args.graphValueIds.has(value.id)) {
@@ -78144,7 +78159,7 @@ var init_project_version_static_value_writes = __esm({
78144
78159
  }
78145
78160
  return;
78146
78161
  }
78147
- if (member.kind !== 7 /* Class */ && value.classId !== void 0) {
78162
+ if (member.kind !== 7 /* Class */ && value.classId !== void 0 && value.classId !== null) {
78148
78163
  throw new Error(
78149
78164
  `${path} value "${value.id}" has classId but the member is not Class.`
78150
78165
  );
@@ -86434,6 +86449,7 @@ function buildValueLowerContext(state, manifest, options = {}) {
86434
86449
  pendingValues: registry.pendingValues,
86435
86450
  pendingLocalizedTexts: registry.pendingLocalizedTexts,
86436
86451
  loweredMemberIdByValueId: /* @__PURE__ */ new Map(),
86452
+ storedClassSchemaMemberIds: /* @__PURE__ */ new Map(),
86437
86453
  pendingBindingMembersByClassId: registry.pendingBindingMembersByClassId,
86438
86454
  declaredConstructors: declaredConstructorIndex,
86439
86455
  initAuthoredRowIds: seedInitAuthoredRowIds(
@@ -86657,14 +86673,12 @@ function lowerMemberDefaultSourcesV4(state, analysis, manifest, options = {}) {
86657
86673
  (row) => !existingPendingValueIds.has(row.id)
86658
86674
  );
86659
86675
  if (pendingRows.length > 0) {
86660
- const existingRows = [...context.reconstructed].filter(
86661
- ([key, record3]) => !existingReconstructedKeys.has(key) && record3.recordKind === "value"
86662
- ).map(
86663
- ([, record3]) => staticValueSeedRow(
86664
- record3.fileFields,
86665
- context.loweredMemberIdByValueId.get(record3.recordId)
86666
- )
86667
- );
86676
+ const existingRows = pendingSeedGraphRows({
86677
+ context,
86678
+ reconstructedBefore: existingReconstructedKeys,
86679
+ pendingRows,
86680
+ rootBody: lowered.value
86681
+ });
86668
86682
  const bindingMembers = [
86669
86683
  ...context.pendingBindingMembersByClassId.values()
86670
86684
  ].filter(
@@ -87247,21 +87261,49 @@ function lowerRowBackedDefault(context, member, backed, baseData3, binding) {
87247
87261
  `Default value ${binding.label}.${assignment.name} is constructor-projected and must be set through its named constructor argument.`
87248
87262
  );
87249
87263
  }
87264
+ const childValueMember = recursivePartialMember(member, childMember);
87265
+ const childSlice = assignmentSlices.get(assignment.name);
87250
87266
  const existingId = isObjectRecord2(baseBody) ? baseBody[assignment.name] : void 0;
87251
- const childId = typeof existingId === "string" ? existingId : annotatedValue(assignment.value).id;
87252
- if (childId === null || childId === void 0) {
87267
+ const annotatedId = annotatedValue(assignment.value).id;
87268
+ const storedId = typeof existingId === "string" ? existingId : annotatedId;
87269
+ if (storedId !== null && storedId !== void 0) {
87270
+ if (typeof existingId === "string" || context.state[`value:${storedId}`] !== void 0) {
87271
+ body[assignment.name] = lowerValueRow(
87272
+ context,
87273
+ childValueMember,
87274
+ assignment.value,
87275
+ storedId,
87276
+ binding,
87277
+ environment,
87278
+ childSlice
87279
+ );
87280
+ continue;
87281
+ }
87282
+ if (!isPendingId(storedId) && !isUuidV4Id(storedId)) {
87283
+ context.loweringFailures.push({
87284
+ message: `Default value ${binding.label}.${assignment.name} is annotated @id("${storedId}"), which names no existing row, and a new row's durable identity must be a UUID v4. Fix the id if it meant an existing row, or drop the @id to mint a fresh identity.`,
87285
+ site: referenceSite(binding, assignment.value)
87286
+ });
87287
+ }
87288
+ }
87289
+ const placedId = sourceValueSymbol(context, assignment.value);
87290
+ if (placedId !== null) {
87253
87291
  throw new Error(
87254
- `Default value ${binding.label}.${assignment.name} has no existing owned value row. Creating new default rows from source is not implemented by this slice.`
87292
+ `Default value ${binding.label}.${assignment.name} cannot place existing value ${placedId} into a new structural slot. Create the nested value inline so the atomic construction transaction can own it.`
87255
87293
  );
87256
87294
  }
87257
- body[assignment.name] = lowerValueRow(
87295
+ body[assignment.name] = lowerSeedChild(
87258
87296
  context,
87259
- recursivePartialMember(member, childMember),
87297
+ childValueMember,
87260
87298
  assignment.value,
87261
- childId,
87262
87299
  binding,
87300
+ `${binding.label}.${assignment.name}`,
87301
+ /* @__PURE__ */ new Map(),
87302
+ /* @__PURE__ */ new Map(),
87303
+ void 0,
87263
87304
  environment,
87264
- assignmentSlices.get(assignment.name)
87305
+ void 0,
87306
+ childSlice
87265
87307
  );
87266
87308
  }
87267
87309
  return {
@@ -87402,14 +87444,13 @@ function lowerStoredBinding(context, binding, memberValueIds, seeds) {
87402
87444
  );
87403
87445
  if (pendingRows.length > 0) {
87404
87446
  const root = reconstructedOrStateValue(context, valueId);
87405
- const existingRows = [...context.reconstructed].filter(
87406
- ([key, record3]) => key !== `value:${valueId}` && !existingReconstructedKeys.has(key) && record3.recordKind === "value"
87407
- ).map(
87408
- ([, record3]) => staticValueSeedRow(
87409
- record3.fileFields,
87410
- context.loweredMemberIdByValueId.get(record3.recordId)
87411
- )
87412
- );
87447
+ const existingRows = pendingSeedGraphRows({
87448
+ context,
87449
+ reconstructedBefore: existingReconstructedKeys,
87450
+ pendingRows,
87451
+ rootBody: root.value,
87452
+ rootValueId: valueId
87453
+ });
87413
87454
  const bindingMembers = [
87414
87455
  ...context.pendingBindingMembersByClassId.values()
87415
87456
  ].filter((pending) => !existingPendingBindingMemberIds.has(pending.id));
@@ -87444,6 +87485,105 @@ function memberValueId(memberId) {
87444
87485
  function reconstructedOrStateValue(context, valueId) {
87445
87486
  return context.reconstructed.get(`value:${valueId}`)?.fileFields ?? valueBase(context, valueId);
87446
87487
  }
87488
+ function pendingSeedGraphRows(args) {
87489
+ const candidates = /* @__PURE__ */ new Map();
87490
+ for (const [key, record3] of args.context.reconstructed) {
87491
+ if (args.reconstructedBefore.has(key)) continue;
87492
+ if (record3.recordKind !== "value") continue;
87493
+ if (record3.recordId === args.rootValueId) continue;
87494
+ candidates.set(record3.recordId, record3.fileFields);
87495
+ }
87496
+ const pendingIds = new Set(args.pendingRows.map((row) => row.id));
87497
+ const bodies = /* @__PURE__ */ new Map();
87498
+ for (const [id2, fields] of candidates) bodies.set(id2, fields.value);
87499
+ for (const row of args.pendingRows) bodies.set(row.id, row.value);
87500
+ const containedIds = /* @__PURE__ */ new Map();
87501
+ const addContained = (containerId, id2) => {
87502
+ if (typeof containerId !== "string") return;
87503
+ const siblings = containedIds.get(containerId);
87504
+ if (siblings === void 0) containedIds.set(containerId, [id2]);
87505
+ else siblings.push(id2);
87506
+ };
87507
+ for (const [id2, fields] of candidates) addContained(fields.containerId, id2);
87508
+ for (const row of args.pendingRows) addContained(row.containerId, row.id);
87509
+ const collectReferenced = (body, into) => {
87510
+ if (typeof body === "string") {
87511
+ if (bodies.has(body)) into.add(body);
87512
+ return;
87513
+ }
87514
+ if (Array.isArray(body)) {
87515
+ for (const entry of body) collectReferenced(entry, into);
87516
+ return;
87517
+ }
87518
+ if (!isObjectRecord2(body)) return;
87519
+ for (const entry of Object.values(body)) collectReferenced(entry, into);
87520
+ };
87521
+ const childIds = (id2) => {
87522
+ const children = new Set(containedIds.get(id2) ?? []);
87523
+ collectReferenced(bodies.get(id2), children);
87524
+ children.delete(id2);
87525
+ return children;
87526
+ };
87527
+ const reachesPending = /* @__PURE__ */ new Map();
87528
+ const visit = (id2) => {
87529
+ const settled = reachesPending.get(id2);
87530
+ if (settled !== void 0) return settled;
87531
+ reachesPending.set(id2, false);
87532
+ let reaches = pendingIds.has(id2);
87533
+ for (const childId of childIds(id2)) {
87534
+ if (visit(childId)) reaches = true;
87535
+ }
87536
+ reachesPending.set(id2, reaches);
87537
+ return reaches;
87538
+ };
87539
+ const roots = new Set(
87540
+ args.rootValueId === void 0 ? [] : containedIds.get(args.rootValueId) ?? []
87541
+ );
87542
+ collectReferenced(args.rootBody, roots);
87543
+ for (const id2 of roots) visit(id2);
87544
+ const rows = [];
87545
+ for (const [id2, fields] of candidates) {
87546
+ if (reachesPending.get(id2) !== true) continue;
87547
+ rows.push(
87548
+ staticValueSeedRow(fields, args.context.loweredMemberIdByValueId.get(id2))
87549
+ );
87550
+ }
87551
+ return rows;
87552
+ }
87553
+ function retainedStoredClassBody(context, storedClassId, baseBody) {
87554
+ const storedMemberIds = storedClassSchemaMemberIds(context, storedClassId);
87555
+ const body = {};
87556
+ for (const [key, childId] of Object.entries(baseBody)) {
87557
+ const storedMemberId = storedMemberIds.get(key);
87558
+ if (storedMemberId !== void 0 && !context.members.has(storedMemberId)) {
87559
+ continue;
87560
+ }
87561
+ body[key] = childId;
87562
+ }
87563
+ return body;
87564
+ }
87565
+ function storedClassSchemaMemberIds(context, classId) {
87566
+ const cached = context.storedClassSchemaMemberIds.get(classId);
87567
+ if (cached !== void 0) return cached;
87568
+ const memberIds = /* @__PURE__ */ new Map();
87569
+ let current = classId;
87570
+ const visited = /* @__PURE__ */ new Set();
87571
+ while (current !== null && !visited.has(current)) {
87572
+ visited.add(current);
87573
+ const data = stateData(context, "class", current);
87574
+ if (data === null) break;
87575
+ if (isObjectRecord2(data.schema)) {
87576
+ for (const [key, memberId] of Object.entries(data.schema)) {
87577
+ if (typeof memberId === "string" && !memberIds.has(key)) {
87578
+ memberIds.set(key, memberId);
87579
+ }
87580
+ }
87581
+ }
87582
+ current = stringOrNull(data.extendsClassId);
87583
+ }
87584
+ context.storedClassSchemaMemberIds.set(classId, memberIds);
87585
+ return memberIds;
87586
+ }
87447
87587
  function staticValueSeedRow(value, loweredMemberId) {
87448
87588
  const id2 = stringOrNull(value.id);
87449
87589
  if (id2 === null) throw new Error("Authored value row is missing id.");
@@ -88195,7 +88335,11 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
88195
88335
  String(base.id ?? member.name)
88196
88336
  );
88197
88337
  const baseBody = isObjectRecord2(base.value) ? base.value : {};
88198
- const body = { ...baseBody };
88338
+ const body = retainedStoredClassBody(
88339
+ context,
88340
+ currentClassId,
88341
+ baseBody
88342
+ );
88199
88343
  const assignmentSlices = objectInitializerSlices(authoredSlice);
88200
88344
  if (materializedConstruction !== "preserve") {
88201
88345
  lowerConstructorProjections(
@@ -100954,7 +101098,9 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
100954
101098
  changes: status.changes,
100955
101099
  binaryChanges: status.binaryChanges ?? []
100956
101100
  });
100957
- if (status.changes.length === 0) return status;
101101
+ if (status.changes.length === 0 && status.authoredValueSeeds.size === 0) {
101102
+ return status;
101103
+ }
100958
101104
  const initConversions = status.initConversions ?? [];
100959
101105
  if (initConversions.length > 0 && options.json !== true) {
100960
101106
  for (const line of describeInitConversions(initConversions)) warn(line);
@@ -101059,7 +101205,7 @@ async function runPush(workspace, options, preparationOverride) {
101059
101205
  const status = candidate.status;
101060
101206
  const local = candidate.preparedLocal;
101061
101207
  preparation?.stop();
101062
- if (status.changes.length === 0) {
101208
+ if (status.changes.length === 0 && status.authoredValueSeeds.size === 0) {
101063
101209
  if (options.json === true) {
101064
101210
  console.log(
101065
101211
  JSON.stringify({
@@ -101575,7 +101721,7 @@ async function prepareLocalCandidateV4(workspace, options = {}) {
101575
101721
  let sourceHash;
101576
101722
  let preparedLocal = null;
101577
101723
  let documentStatus = status;
101578
- if (status.changes.length > 0) {
101724
+ if (status.changes.length > 0 || status.authoredValueSeeds.size > 0) {
101579
101725
  try {
101580
101726
  documentStatus = pushOptions.dryRun ? cloneStatusForDryRun(status) : status;
101581
101727
  preparedLocal = await prepareLocalPushArtifactsV4(
@@ -103116,7 +103262,7 @@ var init_registry2 = __esm({
103116
103262
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
103117
103263
  formatVersion: 3,
103118
103264
  contractVersion: "3.9",
103119
- cliVersion: "0.25.0",
103265
+ cliVersion: "0.25.1",
103120
103266
  projectFileUploadBatchSize: 32,
103121
103267
  documentRecords: {
103122
103268
  member: {
@@ -109811,7 +109957,12 @@ async function main() {
109811
109957
  `${paintChangeKind(binary.action === "create" ? "create" : "update")} project-file bytes ${binary.symbol} (${binary.path}) \u2014 ${binary.action}`
109812
109958
  );
109813
109959
  }
109814
- if (status.conflictedFiles.length === 0 && status.parseErrors.length === 0 && status.changes.length === 0 && (status.binaryChanges?.length ?? 0) === 0) {
109960
+ if (status.authoredValueSeeds.size > 0) {
109961
+ console.log(
109962
+ ` ${paintChangeKind("create")} ${status.authoredValueSeeds.size} member value seed(s)`
109963
+ );
109964
+ }
109965
+ if (status.conflictedFiles.length === 0 && status.parseErrors.length === 0 && status.changes.length === 0 && status.authoredValueSeeds.size === 0 && (status.binaryChanges?.length ?? 0) === 0) {
109815
109966
  console.log(`${sym.ok} Working copy is clean.`);
109816
109967
  }
109817
109968
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.25.0 -->
12
+ <!-- reviewed-through-cli: 0.25.1 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -41,11 +41,12 @@ Read the relevant specs in full:
41
41
  - [P50 loops](https://github.com/ryanbliss/neo-compose-specs/blob/main/new-features/p50-neoscript-for-and-foreach-loops.md)
42
42
  - [P51 switch](https://github.com/ryanbliss/neo-compose-specs/blob/main/new-features/p51-neoscript-switch-statements.md)
43
43
  - [P52 try/catch](https://github.com/ryanbliss/neo-compose-specs/blob/main/new-features/p52-neoscript-try-catch-blocks.md)
44
+ - [P55 breakable collection iteration](https://github.com/ryanbliss/neo-compose-specs/blob/main/neoscript/p55-breakable-collection-iteration.md)
44
45
  - [P60 delegate parameters and selector targeting](https://github.com/ryanbliss/neo-compose-specs/blob/main/new-features/p60-closure-parameters-and-selector-targeting.md)
45
46
  - [P61 initializer materialization](https://github.com/ryanbliss/neo-compose-specs/blob/main/new-features/complete/p61-initializer-materialization.md)
46
47
  - [P64 NeoScript unit testing and push hooks](https://github.com/ryanbliss/neo-compose-specs/blob/main/new-features/p64-neoscript-unit-testing-and-push-hooks.md)
47
48
 
48
- P38–P44, P47–P52, P60–P61, and the initial P64 test/push-hook vertical slice
49
+ P38–P44, P47–P52, P55, P60–P61, and the initial P64 test/push-hook vertical slice
49
50
  are implemented. P45 remains deliberately deferred; do not
50
51
  build or teach runtime-child provenance. P46 is a hardening proposal, not an
51
52
  authoring capability. P52's document header still says proposed, but try/catch
@@ -82,7 +83,7 @@ wrappers.
82
83
  The marker near the top of `SKILL.md` must exactly match the package version:
83
84
 
84
85
  ```html
85
- <!-- reviewed-through-cli: 0.25.0 -->
86
+ <!-- reviewed-through-cli: 0.25.1 -->
86
87
  ```
87
88
 
88
89
  The quoted version above is checked too, so this instruction cannot go stale