@wildwinter/toolkit 0.2.0 → 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.
@@ -150,6 +150,17 @@ declare function resolvedClaims(manifest: Manifest, host: HostName, chosen?: Rea
150
150
  declare function claimClashes(claims: readonly ResolvedClaim[]): Problem[];
151
151
  /** One manifest's declarations for one host and node kind, or undefined. */
152
152
  declare function targetOf(manifest: Manifest | undefined, host: HostName, kind: string): TargetDecl | undefined;
153
+ /**
154
+ * A claim as a host draws and checks its field: the claim's label, help, value help and constraints, over the
155
+ * host's own declaration for what the claim leaves out (an enum's or flags' values, a quality's stages, the
156
+ * default). The host's vocabulary stands when the claim names none, as the host's ladder always does.
157
+ */
158
+ declare function effectiveClaim<C extends ClaimDecl>(claim: C, hostDecl: {
159
+ readonly type?: string;
160
+ readonly values?: readonly string[];
161
+ readonly stages?: readonly string[];
162
+ readonly default?: unknown;
163
+ }): C;
153
164
 
154
165
  /** What the host knows about its nodes, for references. */
155
166
  interface ValueContext {
@@ -198,6 +209,8 @@ declare function renameStages(target: TargetDecl, entry: Readonly<Record<string,
198
209
  * sorted by key, or in the authored order with `order: "preserve"`
199
210
  * object(fields) named keys by their own strategy, every other key by `other`
200
211
  * (atomic when absent)
212
+ * comments a list of comment threads, merged message by message
213
+ * (`mergeComments`)
201
214
  */
202
215
  type Strategy = {
203
216
  readonly kind: "atomic";
@@ -216,9 +229,12 @@ type Strategy = {
216
229
  readonly kind: "object";
217
230
  readonly fields: Readonly<Record<string, Strategy>>;
218
231
  readonly other?: Strategy;
232
+ } | {
233
+ readonly kind: "comments";
219
234
  };
220
235
  declare const ATOMIC: Strategy;
221
236
  declare const SET: Strategy;
237
+ declare const COMMENTS: Strategy;
222
238
  declare const mapOf: (of: Strategy) => Strategy;
223
239
  declare const keyedBy: (key: string, of: Strategy, opts?: {
224
240
  order?: "sorted" | "preserve";
@@ -256,6 +272,25 @@ interface TreeMergeOptions {
256
272
  declare function stableStringify(value: unknown): string;
257
273
  /** Three-way merge `base`, `ours` and `theirs` by `strategy`. A clash keeps ours. */
258
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;
259
294
  /** How a value of this type merges (the README's table). */
260
295
  declare function strategyForType(decl: TypeDecl): Strategy;
261
296
  /** How one node's entry merges: field by field, each by its type. */
@@ -310,4 +345,4 @@ declare const atKey: (base: string, key: string) => string;
310
345
  /** `base` and then list position `i`. */
311
346
  declare const atIndex: (base: string, i: number) => string;
312
347
 
313
- 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, 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 };
@@ -150,6 +150,17 @@ declare function resolvedClaims(manifest: Manifest, host: HostName, chosen?: Rea
150
150
  declare function claimClashes(claims: readonly ResolvedClaim[]): Problem[];
151
151
  /** One manifest's declarations for one host and node kind, or undefined. */
152
152
  declare function targetOf(manifest: Manifest | undefined, host: HostName, kind: string): TargetDecl | undefined;
153
+ /**
154
+ * A claim as a host draws and checks its field: the claim's label, help, value help and constraints, over the
155
+ * host's own declaration for what the claim leaves out (an enum's or flags' values, a quality's stages, the
156
+ * default). The host's vocabulary stands when the claim names none, as the host's ladder always does.
157
+ */
158
+ declare function effectiveClaim<C extends ClaimDecl>(claim: C, hostDecl: {
159
+ readonly type?: string;
160
+ readonly values?: readonly string[];
161
+ readonly stages?: readonly string[];
162
+ readonly default?: unknown;
163
+ }): C;
153
164
 
154
165
  /** What the host knows about its nodes, for references. */
155
166
  interface ValueContext {
@@ -198,6 +209,8 @@ declare function renameStages(target: TargetDecl, entry: Readonly<Record<string,
198
209
  * sorted by key, or in the authored order with `order: "preserve"`
199
210
  * object(fields) named keys by their own strategy, every other key by `other`
200
211
  * (atomic when absent)
212
+ * comments a list of comment threads, merged message by message
213
+ * (`mergeComments`)
201
214
  */
202
215
  type Strategy = {
203
216
  readonly kind: "atomic";
@@ -216,9 +229,12 @@ type Strategy = {
216
229
  readonly kind: "object";
217
230
  readonly fields: Readonly<Record<string, Strategy>>;
218
231
  readonly other?: Strategy;
232
+ } | {
233
+ readonly kind: "comments";
219
234
  };
220
235
  declare const ATOMIC: Strategy;
221
236
  declare const SET: Strategy;
237
+ declare const COMMENTS: Strategy;
222
238
  declare const mapOf: (of: Strategy) => Strategy;
223
239
  declare const keyedBy: (key: string, of: Strategy, opts?: {
224
240
  order?: "sorted" | "preserve";
@@ -256,6 +272,25 @@ interface TreeMergeOptions {
256
272
  declare function stableStringify(value: unknown): string;
257
273
  /** Three-way merge `base`, `ours` and `theirs` by `strategy`. A clash keeps ours. */
258
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;
259
294
  /** How a value of this type merges (the README's table). */
260
295
  declare function strategyForType(decl: TypeDecl): Strategy;
261
296
  /** How one node's entry merges: field by field, each by its type. */
@@ -310,4 +345,4 @@ declare const atKey: (base: string, key: string) => string;
310
345
  /** `base` and then list position `i`. */
311
346
  declare const atIndex: (base: string, i: number) => string;
312
347
 
313
- 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, 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 };
@@ -396,7 +396,7 @@ function targetProblems(host, kind, target, path, settings, out) {
396
396
  if (!SCALAR_TYPES.includes(claim.type)) {
397
397
  out.push(err2("claim-type", atKey(at, "type"), "a claim presents a host field, which is a scalar"));
398
398
  } else {
399
- typeProblems(host, claim, at, false, out);
399
+ typeProblems(host, claim, at, false, out, true);
400
400
  }
401
401
  if (typeof claim.setting !== "string" || !settings.has(claim.setting)) {
402
402
  out.push(err2("claim-setting", atKey(at, "setting"), `a claim names its field through a setting, and "${String(claim.setting)}" is not one of the manifest's settings`));
@@ -439,7 +439,7 @@ function fieldListProblems(host, fields, path, topLevel, out) {
439
439
  }
440
440
  });
441
441
  }
442
- function typeProblems(host, decl, path, named, out) {
442
+ function typeProblems(host, decl, path, named, out, claim = false) {
443
443
  const typeAt = atKey(path, "type");
444
444
  if (decl.type === "string") {
445
445
  out.push(err2("string-type", typeAt, 'the text type is spelled "text"'));
@@ -455,6 +455,7 @@ function typeProblems(host, decl, path, named, out) {
455
455
  case "enum":
456
456
  case "flags": {
457
457
  const values = decl.values;
458
+ if (claim && values === void 0) break;
458
459
  if (!Array.isArray(values) || values.length === 0) {
459
460
  out.push(err2("values-empty", at("values"), `an ${decl.type === "enum" ? "enum" : "flags field"} lists its values`));
460
461
  sound = false;
@@ -473,6 +474,7 @@ function typeProblems(host, decl, path, named, out) {
473
474
  break;
474
475
  }
475
476
  case "quality": {
477
+ if (claim && decl.stages === void 0) break;
476
478
  for (const p of qualityDeclarationProblems(decl.stages)) {
477
479
  out.push(err2(
478
480
  `quality-${p.kind === "default-not-a-stage" ? "default" : p.kind}`,
@@ -532,7 +534,11 @@ function typeProblems(host, decl, path, named, out) {
532
534
  }
533
535
  }
534
536
  if (decl.renamedStages !== void 0) renamedStagesProblems(decl, at("renamedStages"), out);
535
- if (decl.valueHelp !== void 0) {
537
+ if (decl.valueHelp !== void 0 && claim) {
538
+ if (!isObject(decl.valueHelp) || !["enum", "flags", "quality"].includes(decl.type)) {
539
+ out.push(err2("value-help", at("valueHelp"), "value help describes the values of an enum or flags field, or a quality's stages"));
540
+ }
541
+ } else if (decl.valueHelp !== void 0) {
536
542
  const vocabulary = decl.type === "quality" ? decl.stages : decl.type === "enum" || decl.type === "flags" ? decl.values : void 0;
537
543
  if (vocabulary === void 0 || !isObject(decl.valueHelp)) {
538
544
  out.push(err2("value-help", at("valueHelp"), "value help describes the values of an enum or flags field, or a quality's stages"));
@@ -613,10 +619,19 @@ function targetOf(manifest, host, kind) {
613
619
  const kinds = manifest?.targets[host];
614
620
  return kinds !== void 0 && own(kinds, kind) ? kinds[kind] : void 0;
615
621
  }
622
+ function effectiveClaim(claim, hostDecl) {
623
+ return {
624
+ ...claim,
625
+ ...claim.values === void 0 && hostDecl.values !== void 0 ? { values: hostDecl.values } : {},
626
+ ...claim.stages === void 0 && hostDecl.stages !== void 0 ? { stages: hostDecl.stages } : {},
627
+ ...claim.default === void 0 && hostDecl.default !== void 0 ? { default: hostDecl.default } : {}
628
+ };
629
+ }
616
630
 
617
631
  // src/extensions/merge.ts
618
632
  var ATOMIC = { kind: "atomic" };
619
633
  var SET = { kind: "set" };
634
+ var COMMENTS = { kind: "comments" };
620
635
  var mapOf = (of) => ({ kind: "map", of });
621
636
  var keyedBy = (key, of, opts) => ({
622
637
  kind: "keyed",
@@ -659,6 +674,7 @@ function hasShape(spec, v) {
659
674
  return true;
660
675
  case "set":
661
676
  case "keyed":
677
+ case "comments":
662
678
  return Array.isArray(v);
663
679
  case "map":
664
680
  case "object":
@@ -698,6 +714,8 @@ function mergeValue(spec, b, o, t, id, path, ctx) {
698
714
  }
699
715
  case "keyed":
700
716
  return mergeKeyed(spec, asArr(b), o, t, path, ctx);
717
+ case "comments":
718
+ return mergeThreads(asArr(b), o, t, path, ctx);
701
719
  }
702
720
  }
703
721
  function mergeKeyed(spec, B, O, T, path, ctx) {
@@ -776,6 +794,70 @@ function preservedOrder(B, O, T, survivors, path, ctx) {
776
794
  for (const k of [...survivors.keys()].sort()) if (!result.includes(k)) result.push(k);
777
795
  return result;
778
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
+ }
779
861
  function strategyForType(decl) {
780
862
  switch (decl.type) {
781
863
  case "flags":
@@ -883,6 +965,6 @@ function orphanedEntries(data, exists) {
883
965
  return out;
884
966
  }
885
967
 
886
- export { ATOMIC, HOSTS, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, SCALAR_TYPES, SET, UNRESOLVED_KEY, VALUE_TYPES, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, 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 };
887
969
  //# sourceMappingURL=extensions.js.map
888
970
  //# sourceMappingURL=extensions.js.map