@wildwinter/toolkit 0.2.1 → 0.2.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.
@@ -209,6 +209,8 @@ declare function renameStages(target: TargetDecl, entry: Readonly<Record<string,
209
209
  * sorted by key, or in the authored order with `order: "preserve"`
210
210
  * object(fields) named keys by their own strategy, every other key by `other`
211
211
  * (atomic when absent)
212
+ * comments a list of comment threads, merged message by message
213
+ * (`mergeComments`)
212
214
  */
213
215
  type Strategy = {
214
216
  readonly kind: "atomic";
@@ -227,9 +229,12 @@ type Strategy = {
227
229
  readonly kind: "object";
228
230
  readonly fields: Readonly<Record<string, Strategy>>;
229
231
  readonly other?: Strategy;
232
+ } | {
233
+ readonly kind: "comments";
230
234
  };
231
235
  declare const ATOMIC: Strategy;
232
236
  declare const SET: Strategy;
237
+ declare const COMMENTS: Strategy;
233
238
  declare const mapOf: (of: Strategy) => Strategy;
234
239
  declare const keyedBy: (key: string, of: Strategy, opts?: {
235
240
  order?: "sorted" | "preserve";
@@ -267,6 +272,25 @@ interface TreeMergeOptions {
267
272
  declare function stableStringify(value: unknown): string;
268
273
  /** Three-way merge `base`, `ours` and `theirs` by `strategy`. A clash keeps ours. */
269
274
  declare function mergeTree(strategy: Strategy, base: unknown, ours: unknown, theirs: unknown, opts?: TreeMergeOptions): TreeMergeResult;
275
+ /**
276
+ * Three-way merge two lists of comment threads, the shape both families'
277
+ * comments share: `{ id, messages: [{ author, ts, body, deleted? }], resolved?,
278
+ * ...}`. Lifted from Patter's merge (patterkit design/family-parity-2026-10.md
279
+ * item 5), where a thread merged whole made two replies to one thread clash.
280
+ *
281
+ * - Messages are keyed by author and timestamp, and are the union of both
282
+ * sides', oldest first. A withdrawn message (a tombstone: body emptied,
283
+ * `deleted` set) stays withdrawn whichever side withdrew it, and a message a
284
+ * side removed outright, or lost with its whole thread, counts as withdrawn
285
+ * by that side. One message edited two ways is a clash that keeps ours.
286
+ * - A thread's other fields (its anchor, its marker, `resolved`) merge one by
287
+ * one, a field changed two ways being a clash; a thread one side removed
288
+ * takes the other side's.
289
+ * - A thread with nothing readable left goes, as the editors drop it.
290
+ *
291
+ * Threads come back oldest first, by their first message, then by id.
292
+ */
293
+ declare function mergeComments(base: unknown, ours: unknown, theirs: unknown, opts?: TreeMergeOptions): TreeMergeResult;
270
294
  /** How a value of this type merges (the README's table). */
271
295
  declare function strategyForType(decl: TypeDecl): Strategy;
272
296
  /** How one node's entry merges: field by field, each by its type. */
@@ -321,4 +345,4 @@ declare const atKey: (base: string, key: string) => string;
321
345
  /** `base` and then list position `i`. */
322
346
  declare const atIndex: (base: string, i: number) => string;
323
347
 
324
- export { ATOMIC, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
348
+ export { ATOMIC, COMMENTS, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
@@ -209,6 +209,8 @@ declare function renameStages(target: TargetDecl, entry: Readonly<Record<string,
209
209
  * sorted by key, or in the authored order with `order: "preserve"`
210
210
  * object(fields) named keys by their own strategy, every other key by `other`
211
211
  * (atomic when absent)
212
+ * comments a list of comment threads, merged message by message
213
+ * (`mergeComments`)
212
214
  */
213
215
  type Strategy = {
214
216
  readonly kind: "atomic";
@@ -227,9 +229,12 @@ type Strategy = {
227
229
  readonly kind: "object";
228
230
  readonly fields: Readonly<Record<string, Strategy>>;
229
231
  readonly other?: Strategy;
232
+ } | {
233
+ readonly kind: "comments";
230
234
  };
231
235
  declare const ATOMIC: Strategy;
232
236
  declare const SET: Strategy;
237
+ declare const COMMENTS: Strategy;
233
238
  declare const mapOf: (of: Strategy) => Strategy;
234
239
  declare const keyedBy: (key: string, of: Strategy, opts?: {
235
240
  order?: "sorted" | "preserve";
@@ -267,6 +272,25 @@ interface TreeMergeOptions {
267
272
  declare function stableStringify(value: unknown): string;
268
273
  /** Three-way merge `base`, `ours` and `theirs` by `strategy`. A clash keeps ours. */
269
274
  declare function mergeTree(strategy: Strategy, base: unknown, ours: unknown, theirs: unknown, opts?: TreeMergeOptions): TreeMergeResult;
275
+ /**
276
+ * Three-way merge two lists of comment threads, the shape both families'
277
+ * comments share: `{ id, messages: [{ author, ts, body, deleted? }], resolved?,
278
+ * ...}`. Lifted from Patter's merge (patterkit design/family-parity-2026-10.md
279
+ * item 5), where a thread merged whole made two replies to one thread clash.
280
+ *
281
+ * - Messages are keyed by author and timestamp, and are the union of both
282
+ * sides', oldest first. A withdrawn message (a tombstone: body emptied,
283
+ * `deleted` set) stays withdrawn whichever side withdrew it, and a message a
284
+ * side removed outright, or lost with its whole thread, counts as withdrawn
285
+ * by that side. One message edited two ways is a clash that keeps ours.
286
+ * - A thread's other fields (its anchor, its marker, `resolved`) merge one by
287
+ * one, a field changed two ways being a clash; a thread one side removed
288
+ * takes the other side's.
289
+ * - A thread with nothing readable left goes, as the editors drop it.
290
+ *
291
+ * Threads come back oldest first, by their first message, then by id.
292
+ */
293
+ declare function mergeComments(base: unknown, ours: unknown, theirs: unknown, opts?: TreeMergeOptions): TreeMergeResult;
270
294
  /** How a value of this type merges (the README's table). */
271
295
  declare function strategyForType(decl: TypeDecl): Strategy;
272
296
  /** How one node's entry merges: field by field, each by its type. */
@@ -321,4 +345,4 @@ declare const atKey: (base: string, key: string) => string;
321
345
  /** `base` and then list position `i`. */
322
346
  declare const atIndex: (base: string, i: number) => string;
323
347
 
324
- export { ATOMIC, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
348
+ export { ATOMIC, COMMENTS, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
@@ -631,6 +631,7 @@ function effectiveClaim(claim, hostDecl) {
631
631
  // src/extensions/merge.ts
632
632
  var ATOMIC = { kind: "atomic" };
633
633
  var SET = { kind: "set" };
634
+ var COMMENTS = { kind: "comments" };
634
635
  var mapOf = (of) => ({ kind: "map", of });
635
636
  var keyedBy = (key, of, opts) => ({
636
637
  kind: "keyed",
@@ -673,6 +674,7 @@ function hasShape(spec, v) {
673
674
  return true;
674
675
  case "set":
675
676
  case "keyed":
677
+ case "comments":
676
678
  return Array.isArray(v);
677
679
  case "map":
678
680
  case "object":
@@ -712,6 +714,8 @@ function mergeValue(spec, b, o, t, id, path, ctx) {
712
714
  }
713
715
  case "keyed":
714
716
  return mergeKeyed(spec, asArr(b), o, t, path, ctx);
717
+ case "comments":
718
+ return mergeThreads(asArr(b), o, t, path, ctx);
715
719
  }
716
720
  }
717
721
  function mergeKeyed(spec, B, O, T, path, ctx) {
@@ -790,6 +794,70 @@ function preservedOrder(B, O, T, survivors, path, ctx) {
790
794
  for (const k of [...survivors.keys()].sort()) if (!result.includes(k)) result.push(k);
791
795
  return result;
792
796
  }
797
+ function mergeComments(base, ours, theirs, opts) {
798
+ return mergeTree(COMMENTS, base, ours, theirs, opts);
799
+ }
800
+ function mergeThreads(B, O, T, path, ctx) {
801
+ const index = (arr) => {
802
+ const m = /* @__PURE__ */ new Map();
803
+ for (const c of arr) if (isObject(c) && typeof c["id"] === "string") m.set(c["id"], c);
804
+ return m;
805
+ };
806
+ const Bm = index(B), Om = index(O), Tm = index(T);
807
+ const out = [];
808
+ for (const id of /* @__PURE__ */ new Set([...Bm.keys(), ...Om.keys(), ...Tm.keys()])) {
809
+ const b = Bm.get(id), o = Om.get(id), t = Tm.get(id);
810
+ const here = `${path}[${id}]`;
811
+ if (o === void 0 && t === void 0) continue;
812
+ if (![b, o, t].some((c) => Array.isArray(c?.["messages"]))) {
813
+ const v = o !== void 0 && t !== void 0 ? merge3(b, o, t, id, here, ctx) : b !== void 0 && ctx.eq(o ?? t, b) ? void 0 : o ?? t;
814
+ if (isObject(v)) out.push(v);
815
+ continue;
816
+ }
817
+ const messages = mergeMessages(b, o, t, id, here, ctx);
818
+ if (messages.length === 0 || messages.every((m) => m["deleted"] === true)) continue;
819
+ const fields = {};
820
+ if (o !== void 0 && t !== void 0) {
821
+ const B0 = b ?? {};
822
+ const flag = (c, k) => k === "resolved" ? c[k] === true ? true : void 0 : c[k];
823
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(o), ...Object.keys(t), ...Object.keys(B0)])) {
824
+ if (k === "messages") continue;
825
+ const v = merge3(flag(B0, k), flag(o, k), flag(t, k), id, `${here}.${k}`, ctx);
826
+ if (v !== void 0) fields[k] = v;
827
+ }
828
+ } else {
829
+ for (const [k, v] of Object.entries(o ?? t)) if (k !== "messages") fields[k] = v;
830
+ }
831
+ out.push({ ...fields, messages });
832
+ }
833
+ const first = (c) => {
834
+ const m = asArr(c["messages"])[0];
835
+ return isObject(m) && typeof m["ts"] === "string" ? m["ts"] : "";
836
+ };
837
+ return out.sort((x, y) => first(x).localeCompare(first(y)) || String(x["id"]).localeCompare(String(y["id"])));
838
+ }
839
+ function mergeMessages(b, o, t, id, here, ctx) {
840
+ const keyOf = (m) => `${String(m["author"] ?? "")}|${String(m["ts"] ?? "")}`;
841
+ const index = (c) => {
842
+ const m = /* @__PURE__ */ new Map();
843
+ for (const x of asArr(c?.["messages"])) if (isObject(x)) m.set(keyOf(x), x);
844
+ return m;
845
+ };
846
+ const Bm = index(b), Om = index(o), Tm = index(t);
847
+ const out = [];
848
+ for (const k of /* @__PURE__ */ new Set([...Bm.keys(), ...Om.keys(), ...Tm.keys()])) {
849
+ const mb = Bm.get(k), mo = Om.get(k), mt = Tm.get(k);
850
+ if ([mb, mo, mt].some((m) => m?.["deleted"] === true) || mb !== void 0 && (mo === void 0 || mt === void 0)) {
851
+ out.push({ ...mo ?? mt ?? mb, body: "", deleted: true });
852
+ } else if (mo !== void 0 && mt !== void 0) {
853
+ out.push(merge3(mb, mo, mt, id, `${here}.messages[${k}]`, ctx));
854
+ } else {
855
+ out.push(mo ?? mt);
856
+ }
857
+ }
858
+ const ts = (m) => String(m["ts"] ?? "");
859
+ return out.sort((x, y) => ts(x).localeCompare(ts(y)) || String(x["author"] ?? "").localeCompare(String(y["author"] ?? "")));
860
+ }
793
861
  function strategyForType(decl) {
794
862
  switch (decl.type) {
795
863
  case "flags":
@@ -897,6 +965,6 @@ function orphanedEntries(data, exists) {
897
965
  return out;
898
966
  }
899
967
 
900
- export { ATOMIC, HOSTS, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, SCALAR_TYPES, SET, UNRESOLVED_KEY, VALUE_TYPES, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
968
+ export { ATOMIC, COMMENTS, HOSTS, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, SCALAR_TYPES, SET, UNRESOLVED_KEY, VALUE_TYPES, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
901
969
  //# sourceMappingURL=extensions.js.map
902
970
  //# sourceMappingURL=extensions.js.map