@abraca/dabra 2.7.0 → 2.8.0

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.
@@ -15715,6 +15715,19 @@ function makeEntryMap(fields) {
15715
15715
  return m;
15716
15716
  }
15717
15717
  /**
15718
+ * A label is a "placeholder" — i.e. carries no real user title — when it is
15719
+ * empty/whitespace, null/undefined, or the literal `"Untitled"` sentinel
15720
+ * (case-insensitive, so `"untitled"` from a `labelToFilename` round-trip is
15721
+ * caught too). The whole tree-label corruption class boils down to a
15722
+ * placeholder being allowed to overwrite a real title; `patchEntry` refuses
15723
+ * exactly that. Mirrors cou-sh's `isEmptyTreeLabel`.
15724
+ */
15725
+ function isPlaceholderLabel(label) {
15726
+ if (typeof label !== "string") return label == null;
15727
+ const t = label.trim();
15728
+ return t === "" || t.toLowerCase() === "untitled";
15729
+ }
15730
+ /**
15718
15731
  * Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
15719
15732
  * concurrent edit to a *different* field by a peer is preserved instead
15720
15733
  * of being clobbered by a whole-entry write — the whole-entry-LWW fix
@@ -15729,21 +15742,44 @@ function makeEntryMap(fields) {
15729
15742
  * Self-transacting: it batches its writes in one `Y.Doc` transaction
15730
15743
  * (a safe reentrant no-op join when already inside one), so callers
15731
15744
  * don't need to pass or own a transaction.
15745
+ *
15746
+ * ── NO-DESTROY LABEL INVARIANT ──────────────────────────────────────────
15747
+ * A `label` patch that is a placeholder (empty/whitespace/"Untitled") is
15748
+ * DROPPED when the entry already holds a real (non-placeholder) label —
15749
+ * regardless of which consumer (cou-sh title-sync, fs-sync rename
15750
+ * detection, MCP, table renderers, a stale snapshot) tried it. This is the
15751
+ * source-of-truth guard against the "card title silently becomes Untitled
15752
+ * / files renamed to untitled.md" corruption: a placeholder must never win
15753
+ * over a real title. Creating a brand-new entry with an empty label (e.g.
15754
+ * a fresh kanban card) is still allowed — the guard only fires when a real
15755
+ * label already exists. Pass `{ allowLabelClear: true }` to override (the
15756
+ * single legitimate "user explicitly cleared it" path).
15732
15757
  */
15733
- function patchEntry(treeMap, id, patch, removeKeys = []) {
15758
+ function patchEntry(treeMap, id, patch, removeKeys = [], opts = {}) {
15734
15759
  const apply = () => {
15735
15760
  const raw = treeMap.get(id);
15761
+ let effectivePatch = patch;
15762
+ if (!opts.allowLabelClear && Object.hasOwn(patch, "label") && isPlaceholderLabel(patch.label)) {
15763
+ let existingLabel;
15764
+ if (raw != null && typeof raw.get === "function") existingLabel = raw.get("label");
15765
+ else if (raw != null && typeof raw.toJSON === "function") existingLabel = raw.toJSON()?.label;
15766
+ else if (raw != null && typeof raw === "object") existingLabel = raw.label;
15767
+ if (!isPlaceholderLabel(existingLabel)) {
15768
+ const { label: _dropped, ...rest } = patch;
15769
+ effectivePatch = rest;
15770
+ }
15771
+ }
15736
15772
  if (raw instanceof yjs.Map) {
15737
- for (const [k, v] of Object.entries(patch)) if (v === void 0) raw.delete(k);
15773
+ for (const [k, v] of Object.entries(effectivePatch)) if (v === void 0) raw.delete(k);
15738
15774
  else raw.set(k, v);
15739
15775
  for (const k of removeKeys) raw.delete(k);
15740
15776
  return;
15741
15777
  }
15742
15778
  const merged = {
15743
15779
  ...raw == null ? {} : toPlain(raw),
15744
- ...patch
15780
+ ...effectivePatch
15745
15781
  };
15746
- for (const [k, v] of Object.entries(patch)) if (v === void 0) delete merged[k];
15782
+ for (const [k, v] of Object.entries(effectivePatch)) if (v === void 0) delete merged[k];
15747
15783
  for (const k of removeKeys) delete merged[k];
15748
15784
  treeMap.set(id, makeEntryMap(merged));
15749
15785
  };
@@ -20900,6 +20936,7 @@ exports.filenameToLabel = filenameToLabel;
20900
20936
  exports.foldRecords = foldRecords;
20901
20937
  exports.generateMnemonic = generateMnemonic;
20902
20938
  exports.isEncryptedContent = isEncryptedContent;
20939
+ exports.isPlaceholderLabel = isPlaceholderLabel;
20903
20940
  exports.makeEncryptedYMap = makeEncryptedYMap;
20904
20941
  exports.makeEncryptedYText = makeEncryptedYText;
20905
20942
  exports.makeEntryMap = makeEntryMap;