tickmarkr 1.92.0 → 1.92.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.
@@ -88,7 +88,7 @@ function setScalarPreservingComment(doc, path, value, authoredComment) {
88
88
  if (isScalar(existing))
89
89
  existing.value = value;
90
90
  else
91
- doc.setIn(path, doc.createNode(value));
91
+ setAt(doc, path, doc.createNode(value));
92
92
  if (authoredComment !== undefined) {
93
93
  const written = doc.getIn(path, true);
94
94
  if (isScalar(written))
@@ -112,11 +112,11 @@ function setStringSequencePreservingComments(doc, path, values) {
112
112
  if (comments.length)
113
113
  tombstone.comment = comments.join("\n");
114
114
  }
115
- doc.setIn(path, tombstone);
115
+ setAt(doc, path, tombstone);
116
116
  return;
117
117
  }
118
118
  if (!isSeq(existing)) {
119
- doc.setIn(path, doc.createNode(values));
119
+ setAt(doc, path, doc.createNode(values));
120
120
  return;
121
121
  }
122
122
  const available = [...existing.items];
@@ -127,6 +127,34 @@ function setStringSequencePreservingComments(doc, path, values) {
127
127
  return doc.createNode(value);
128
128
  });
129
129
  }
130
+ // OBS-533: yaml's setIn/deleteIn throw on any non-collection intermediate ("Expected YAML
131
+ // collection at spec. Remaining path: pin"), and a v1.1 null tombstone (`spec:` under
132
+ // routing.map) is exactly that scalar — deepMerge prunes it before schema validation, so the
133
+ // overlay loads clean and the first fleet write under the tombstoned key crashed the TUI.
134
+ // Total forms: a delete treats the masked subtree as absent (the operator's tombstone already
135
+ // denies it, and survives untouched); a set clears the scalar and rebuilds what fleet owns —
136
+ // the routing.allow precedent below.
137
+ function deleteAt(doc, path) {
138
+ for (let depth = 1; depth < path.length; depth++) {
139
+ if (!isMap(doc.getIn(path.slice(0, depth), true)))
140
+ return;
141
+ }
142
+ // deleteIn throws on an empty document — only delete keys that exist.
143
+ if (doc.getIn(path) !== undefined)
144
+ doc.deleteIn(path);
145
+ }
146
+ function setAt(doc, path, value) {
147
+ for (let depth = 1; depth < path.length; depth++) {
148
+ const node = doc.getIn(path.slice(0, depth), true);
149
+ if (node === undefined)
150
+ break;
151
+ if (!isMap(node)) {
152
+ doc.deleteIn(path.slice(0, depth));
153
+ break;
154
+ }
155
+ }
156
+ doc.setIn(path, value);
157
+ }
130
158
  function deleteEmptyMap(doc, path) {
131
159
  const node = doc.getIn(path, true);
132
160
  if (isMap(node) && node.items.length === 0)
@@ -150,27 +178,26 @@ export function renderFleetOverlayWrite(priorBytes, write) {
150
178
  const allowNode = doc.getIn(["routing", "allow"], true);
151
179
  if (allowNode !== undefined && !isMap(allowNode))
152
180
  doc.deleteIn(["routing", "allow"]);
153
- // deleteIn throws on an empty document — only delete keys that exist.
154
181
  if (form.adapters.length) {
155
182
  setStringSequencePreservingComments(doc, ["routing", "allow", "adapters"], form.adapters);
156
183
  }
157
- else if (doc.getIn(["routing", "allow", "adapters"]) !== undefined) {
158
- doc.deleteIn(["routing", "allow", "adapters"]);
184
+ else {
185
+ deleteAt(doc, ["routing", "allow", "adapters"]);
159
186
  }
160
187
  if (form.models.length) {
161
188
  setStringSequencePreservingComments(doc, ["routing", "allow", "models"], form.models);
162
189
  }
163
- else if (doc.getIn(["routing", "allow", "models"]) !== undefined) {
164
- doc.deleteIn(["routing", "allow", "models"]);
190
+ else {
191
+ deleteAt(doc, ["routing", "allow", "models"]);
165
192
  }
166
193
  // Whole fleet out: allow stays present but empty — fail-closed, nothing admitted.
167
194
  if (doc.getIn(["routing", "allow"]) === undefined) {
168
- doc.setIn(["routing", "allow"], doc.createNode({}));
195
+ setAt(doc, ["routing", "allow"], doc.createNode({}));
169
196
  }
170
197
  }
171
- else if (doc.getIn(["routing", "allow"]) !== undefined) {
198
+ else {
172
199
  // Whole fleet in: no restriction to express — the allow block goes away entirely.
173
- doc.deleteIn(["routing", "allow"]);
200
+ deleteAt(doc, ["routing", "allow"]);
174
201
  }
175
202
  // Residuals stay in deny; covered scopes are tombstoned so a lower layer can never
176
203
  // re-exclude behind the operator's back (workers untouched).
@@ -189,9 +216,9 @@ export function renderFleetOverlayWrite(priorBytes, write) {
189
216
  const after = edited.map[shape];
190
217
  if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
191
218
  if (after?.pin === undefined)
192
- doc.deleteIn(["routing", "map", shape, "pin"]);
219
+ deleteAt(doc, ["routing", "map", shape, "pin"]);
193
220
  else
194
- doc.setIn(["routing", "map", shape, "pin"], doc.createNode(after.pin));
221
+ setAt(doc, ["routing", "map", shape, "pin"], doc.createNode(after.pin));
195
222
  }
196
223
  if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
197
224
  const path = ["routing", "map", shape, "pool"];
@@ -200,9 +227,6 @@ export function renderFleetOverlayWrite(priorBytes, write) {
200
227
  setStringSequencePreservingComments(doc, path, null);
201
228
  }
202
229
  else {
203
- const existing = doc.getIn(path, true);
204
- if (existing !== undefined && !isMap(existing))
205
- doc.deleteIn(path);
206
230
  setScalarPreservingComment(doc, [...path, "mode"], after.pool.mode);
207
231
  // Channel order is semantic (ordered walks it; any breaks ties by it) — dedupe, never sort.
208
232
  setStringSequencePreservingComments(doc, [...path, "channels"], [...new Set(after.pool.channels)]);
@@ -213,7 +237,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
213
237
  // the pin/pool declaration owns the whole slot at merge (deepMerge drops the lower
214
238
  // layer's pin/pool/prefer atomically) — an [] mask here would re-declare prefer BESIDE
215
239
  // the pool in one document and fail the exclusivity refine; delete the key instead
216
- doc.deleteIn(["routing", "map", shape, "prefer"]);
240
+ deleteAt(doc, ["routing", "map", shape, "prefer"]);
217
241
  }
218
242
  else {
219
243
  // Clearing a resolved list with no replacing declaration must mask the lower layer
@@ -223,7 +247,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
223
247
  }
224
248
  if (before?.escalate !== after?.escalate) {
225
249
  if (after?.escalate === undefined)
226
- doc.deleteIn(["routing", "map", shape, "escalate"]);
250
+ deleteAt(doc, ["routing", "map", shape, "escalate"]);
227
251
  else
228
252
  setScalarPreservingComment(doc, ["routing", "map", shape, "escalate"], after.escalate);
229
253
  }
@@ -233,7 +257,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
233
257
  continue;
234
258
  const tier = edited.floors[shape];
235
259
  if (tier === undefined)
236
- doc.deleteIn(["routing", "floors", shape]);
260
+ deleteAt(doc, ["routing", "floors", shape]);
237
261
  else
238
262
  setScalarPreservingComment(doc, ["routing", "floors", shape], tier);
239
263
  }
@@ -283,7 +307,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
283
307
  if (JSON.stringify(before) === JSON.stringify(after))
284
308
  continue;
285
309
  if (after === undefined) {
286
- doc.deleteIn([key, "prefer"]);
310
+ deleteAt(doc, [key, "prefer"]);
287
311
  deleteEmptyMap(doc, [key]);
288
312
  }
289
313
  else {
@@ -291,6 +315,11 @@ export function renderFleetOverlayWrite(priorBytes, write) {
291
315
  }
292
316
  }
293
317
  }
318
+ // Every mutation no-oped against an empty or comment-only overlay (contents stays null;
319
+ // deleteAt never creates content): return the prior bytes verbatim — stringifying a null
320
+ // document would write a literal `null` line into the file.
321
+ if (doc.contents === null)
322
+ return priorBytes;
294
323
  // OBS-505: fleet's two-space note style (`tier: mid # note`) applies to INLINE comments only.
295
324
  // The previous commentString prefixed " #" onto EVERY comment line, so block comments — the
296
325
  // whole init scaffold at column 0, and indented operator essays — each gained a stray leading
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.92.0",
3
+ "version": "1.92.1",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",