@wildwinter/toolkit 0.2.1 → 0.2.3
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 +22 -0
- package/README.md +17 -2
- package/conformance/extensions/merge.json +46 -1
- package/dist/extensions.cjs +70 -0
- package/dist/extensions.cjs.map +1 -1
- package/dist/extensions.d.cts +25 -1
- package/dist/extensions.d.ts +25 -1
- package/dist/extensions.js +69 -1
- package/dist/extensions.js.map +1 -1
- package/dist/vcs.cjs +223 -0
- package/dist/vcs.cjs.map +1 -0
- package/dist/vcs.d.cts +95 -0
- package/dist/vcs.d.ts +95 -0
- package/dist/vcs.js +214 -0
- package/dist/vcs.js.map +1 -0
- package/extensions.md +6 -1
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @wildwinter/toolkit
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`@wildwinter/toolkit/vcs`: version-control setup for git, Perforce, Plastic SCM and SVN**, lifted from Patter so
|
|
8
|
+
both families take one copy. `vcsConfigFiles(format, vcs, posture)` writes the `vcs-setup.md` guide, git's
|
|
9
|
+
`.gitattributes` and the system's ignore file for a format that says what its files are (`VcsFormat`);
|
|
10
|
+
`bundlePostureOf` reads back whether the compiled bundle is committed or ignored; `runMergetool` is the wrapper a
|
|
11
|
+
system with one global merge tool calls, sending the format's source to its structured merge and anything else to
|
|
12
|
+
the team's own tool. Node-only, as `./archive` is.
|
|
13
|
+
|
|
14
|
+
## 0.2.2
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Comment threads merge message by message** (`./extensions`): `mergeComments(base, ours, theirs)`, and the
|
|
19
|
+
`comments` strategy (`COMMENTS`) for `mergeTree`, lifted from Patter's merge so both families take one rule.
|
|
20
|
+
Two replies to one thread both land, oldest first; a withdrawn message stays withdrawn, and one a side removed
|
|
21
|
+
outright, or lost with its thread, counts as withdrawn by that side; one message edited two ways is a clash.
|
|
22
|
+
A thread's own fields (anchor, marker, `resolved`) merge one by one, and a thread with nothing readable left
|
|
23
|
+
goes. Threads come back oldest first.
|
|
24
|
+
|
|
3
25
|
## 0.2.1
|
|
4
26
|
|
|
5
27
|
### Changed
|
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Small shared pieces of the **authoring tooling** behind
|
|
4
4
|
[Storylet Studio](https://github.com/storylet-studio) and
|
|
5
5
|
[Patter](https://patterkit.com): opaque ids, a stable hash, JSON5 source parsing,
|
|
6
|
-
find/replace matching, the archive guards,
|
|
7
|
-
for Patter extensions and Storylet extensions.
|
|
6
|
+
find/replace matching, the archive guards, version-control setup (in `./vcs`), and (in
|
|
7
|
+
`./extensions`) the spec and merge for Patter extensions and Storylet extensions.
|
|
8
8
|
|
|
9
9
|
**Nothing here runs in a game.** This is the editor/CLI/pipeline side. The
|
|
10
10
|
runtime language lives in [`@wildwinter/expr`](../expr), and unlike that package
|
|
@@ -93,6 +93,21 @@ pack **deterministic**: a fixed entry timestamp, no folder entries, no streaming
|
|
|
93
93
|
A pack of an unchanged project must be byte-identical to the last one, or every
|
|
94
94
|
delivery churns. That is easy to leave out, and silent when you do.
|
|
95
95
|
|
|
96
|
+
## Version control
|
|
97
|
+
|
|
98
|
+
A separate, node-only entry point, because `runMergetool` starts the team's own merge tool:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { VCS_KINDS, vcsConfigFiles, bundlePostureOf, runMergetool } from "@wildwinter/toolkit/vcs";
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A format says what it is called and what its files are (`VcsFormat`: the tool, the git driver's name, the source
|
|
105
|
+
extensions, the bundle, the packed document, the conflict sidecar). `vcsConfigFiles(format, vcs, posture)` is then
|
|
106
|
+
everything a project is set up with for git, Perforce, Plastic SCM or SVN: the `vcs-setup.md` guide, git's
|
|
107
|
+
`.gitattributes`, and the system's ignore file, with the compiled bundle committed or ignored. `bundlePostureOf`
|
|
108
|
+
reads the posture back, so a switch of system keeps it. `runMergetool` is the one global merge tool Perforce,
|
|
109
|
+
Plastic SCM and SVN allow: the format's source goes to its structured merge, anything else to the team's own tool.
|
|
110
|
+
|
|
96
111
|
## Licence
|
|
97
112
|
|
|
98
113
|
MIT
|
|
@@ -49,7 +49,52 @@
|
|
|
49
49
|
"base": { "k": { "a": 1 } }, "ours": { "k": { "a": 2 } }, "theirs": { "k": { "a": 1, "b": 1 } },
|
|
50
50
|
"merged": { "k": { "a": 2, "b": 1 } }, "conflicts": [] },
|
|
51
51
|
{ "name": "a value whose shape no longer fits its strategy merges as one value", "strategy": { "kind": "map", "of": { "kind": "atomic" } },
|
|
52
|
-
"base": { "a": 1 }, "ours": "gone", "theirs": { "a": 2 }, "merged": "gone", "conflicts": [{ "path": "", "kind": "both-changed" }] }
|
|
52
|
+
"base": { "a": 1 }, "ours": "gone", "theirs": { "a": 2 }, "merged": "gone", "conflicts": [{ "path": "", "kind": "both-changed" }] },
|
|
53
|
+
{ "name": "comments: two replies to one thread both land, in time order", "strategy": { "kind": "comments" },
|
|
54
|
+
"base": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
55
|
+
"ours": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T12:00", "body": "Pacing." }] }],
|
|
56
|
+
"theirs": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Cy", "ts": "2026-01-01T11:00", "body": "Ask Bo." }] }],
|
|
57
|
+
"merged": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Cy", "ts": "2026-01-01T11:00", "body": "Ask Bo." }, { "author": "Bo", "ts": "2026-01-01T12:00", "body": "Pacing." }] }],
|
|
58
|
+
"conflicts": [] },
|
|
59
|
+
{ "name": "comments: a message withdrawn on one side stays withdrawn", "strategy": { "kind": "comments" },
|
|
60
|
+
"base": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "Oops" }] }],
|
|
61
|
+
"ours": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "", "deleted": true }] }],
|
|
62
|
+
"theirs": [{ "id": "t1", "anchor": "c1", "resolved": true, "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "Oops" }] }],
|
|
63
|
+
"merged": [{ "id": "t1", "anchor": "c1", "resolved": true, "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "", "deleted": true }] }],
|
|
64
|
+
"conflicts": [] },
|
|
65
|
+
{ "name": "comments: a message one side removed outright is withdrawn, not brought back", "strategy": { "kind": "comments" },
|
|
66
|
+
"base": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "Oops" }] }],
|
|
67
|
+
"ours": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
68
|
+
"theirs": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "Oops" }] }],
|
|
69
|
+
"merged": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T11:00", "body": "", "deleted": true }] }],
|
|
70
|
+
"conflicts": [] },
|
|
71
|
+
{ "name": "comments: a thread one side removed goes, unless the other side replied", "strategy": { "kind": "comments" },
|
|
72
|
+
"base": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] },
|
|
73
|
+
{ "id": "t2", "anchor": "c2", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:30", "body": "Typo" }] }],
|
|
74
|
+
"ours": [],
|
|
75
|
+
"theirs": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }, { "author": "Bo", "ts": "2026-01-01T12:00", "body": "Pacing." }] },
|
|
76
|
+
{ "id": "t2", "anchor": "c2", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:30", "body": "Typo" }] }],
|
|
77
|
+
"merged": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "", "deleted": true }, { "author": "Bo", "ts": "2026-01-01T12:00", "body": "Pacing." }] }],
|
|
78
|
+
"conflicts": [] },
|
|
79
|
+
{ "name": "comments: one message edited two ways clashes and keeps ours", "strategy": { "kind": "comments" },
|
|
80
|
+
"base": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
81
|
+
"ours": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why this?" }] }],
|
|
82
|
+
"theirs": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why that?" }] }],
|
|
83
|
+
"merged": [{ "id": "t1", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why this?" }] }],
|
|
84
|
+
"conflicts": [{ "path": "[t1].messages[Ann|2026-01-01T10:00]", "kind": "both-changed" }] },
|
|
85
|
+
{ "name": "comments: a thread's own fields merge one by one, a marker moved two ways clashes", "strategy": { "kind": "comments" },
|
|
86
|
+
"base": [{ "id": "t1", "anchor": "c1", "mark": { "canvas": "d1", "x": 0, "y": 0 }, "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
87
|
+
"ours": [{ "id": "t1", "anchor": "c1", "mark": { "canvas": "d1", "x": 5, "y": 0 }, "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
88
|
+
"theirs": [{ "id": "t1", "anchor": "c1", "resolved": true, "mark": { "canvas": "d1", "x": 9, "y": 9 }, "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
89
|
+
"merged": [{ "id": "t1", "anchor": "c1", "resolved": true, "mark": { "canvas": "d1", "x": 5, "y": 0 }, "messages": [{ "author": "Ann", "ts": "2026-01-01T10:00", "body": "Why?" }] }],
|
|
90
|
+
"conflicts": [{ "path": "[t1].mark", "kind": "both-changed" }] },
|
|
91
|
+
{ "name": "comments: threads come back oldest first, whatever their ids", "strategy": { "kind": "comments" },
|
|
92
|
+
"base": [],
|
|
93
|
+
"ours": [{ "id": "zz", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T09:00", "body": "First" }] }],
|
|
94
|
+
"theirs": [{ "id": "aa", "anchor": "c2", "messages": [{ "author": "Bo", "ts": "2026-01-02T09:00", "body": "Second" }] }],
|
|
95
|
+
"merged": [{ "id": "zz", "anchor": "c1", "messages": [{ "author": "Ann", "ts": "2026-01-01T09:00", "body": "First" }] },
|
|
96
|
+
{ "id": "aa", "anchor": "c2", "messages": [{ "author": "Bo", "ts": "2026-01-02T09:00", "body": "Second" }] }],
|
|
97
|
+
"conflicts": [] }
|
|
53
98
|
],
|
|
54
99
|
"manifest": {
|
|
55
100
|
"schema": "wildwinter/extension@0", "id": "probe", "name": "Probe", "version": "1.0.0",
|
package/dist/extensions.cjs
CHANGED
|
@@ -641,6 +641,7 @@ function effectiveClaim(claim, hostDecl) {
|
|
|
641
641
|
// src/extensions/merge.ts
|
|
642
642
|
var ATOMIC = { kind: "atomic" };
|
|
643
643
|
var SET = { kind: "set" };
|
|
644
|
+
var COMMENTS = { kind: "comments" };
|
|
644
645
|
var mapOf = (of) => ({ kind: "map", of });
|
|
645
646
|
var keyedBy = (key, of, opts) => ({
|
|
646
647
|
kind: "keyed",
|
|
@@ -683,6 +684,7 @@ function hasShape(spec, v) {
|
|
|
683
684
|
return true;
|
|
684
685
|
case "set":
|
|
685
686
|
case "keyed":
|
|
687
|
+
case "comments":
|
|
686
688
|
return Array.isArray(v);
|
|
687
689
|
case "map":
|
|
688
690
|
case "object":
|
|
@@ -722,6 +724,8 @@ function mergeValue(spec, b, o, t, id, path, ctx) {
|
|
|
722
724
|
}
|
|
723
725
|
case "keyed":
|
|
724
726
|
return mergeKeyed(spec, asArr(b), o, t, path, ctx);
|
|
727
|
+
case "comments":
|
|
728
|
+
return mergeThreads(asArr(b), o, t, path, ctx);
|
|
725
729
|
}
|
|
726
730
|
}
|
|
727
731
|
function mergeKeyed(spec, B, O, T, path, ctx) {
|
|
@@ -800,6 +804,70 @@ function preservedOrder(B, O, T, survivors, path, ctx) {
|
|
|
800
804
|
for (const k of [...survivors.keys()].sort()) if (!result.includes(k)) result.push(k);
|
|
801
805
|
return result;
|
|
802
806
|
}
|
|
807
|
+
function mergeComments(base, ours, theirs, opts) {
|
|
808
|
+
return mergeTree(COMMENTS, base, ours, theirs, opts);
|
|
809
|
+
}
|
|
810
|
+
function mergeThreads(B, O, T, path, ctx) {
|
|
811
|
+
const index = (arr) => {
|
|
812
|
+
const m = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const c of arr) if (isObject(c) && typeof c["id"] === "string") m.set(c["id"], c);
|
|
814
|
+
return m;
|
|
815
|
+
};
|
|
816
|
+
const Bm = index(B), Om = index(O), Tm = index(T);
|
|
817
|
+
const out = [];
|
|
818
|
+
for (const id of /* @__PURE__ */ new Set([...Bm.keys(), ...Om.keys(), ...Tm.keys()])) {
|
|
819
|
+
const b = Bm.get(id), o = Om.get(id), t = Tm.get(id);
|
|
820
|
+
const here = `${path}[${id}]`;
|
|
821
|
+
if (o === void 0 && t === void 0) continue;
|
|
822
|
+
if (![b, o, t].some((c) => Array.isArray(c?.["messages"]))) {
|
|
823
|
+
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;
|
|
824
|
+
if (isObject(v)) out.push(v);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const messages = mergeMessages(b, o, t, id, here, ctx);
|
|
828
|
+
if (messages.length === 0 || messages.every((m) => m["deleted"] === true)) continue;
|
|
829
|
+
const fields = {};
|
|
830
|
+
if (o !== void 0 && t !== void 0) {
|
|
831
|
+
const B0 = b ?? {};
|
|
832
|
+
const flag = (c, k) => k === "resolved" ? c[k] === true ? true : void 0 : c[k];
|
|
833
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(o), ...Object.keys(t), ...Object.keys(B0)])) {
|
|
834
|
+
if (k === "messages") continue;
|
|
835
|
+
const v = merge3(flag(B0, k), flag(o, k), flag(t, k), id, `${here}.${k}`, ctx);
|
|
836
|
+
if (v !== void 0) fields[k] = v;
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
for (const [k, v] of Object.entries(o ?? t)) if (k !== "messages") fields[k] = v;
|
|
840
|
+
}
|
|
841
|
+
out.push({ ...fields, messages });
|
|
842
|
+
}
|
|
843
|
+
const first = (c) => {
|
|
844
|
+
const m = asArr(c["messages"])[0];
|
|
845
|
+
return isObject(m) && typeof m["ts"] === "string" ? m["ts"] : "";
|
|
846
|
+
};
|
|
847
|
+
return out.sort((x, y) => first(x).localeCompare(first(y)) || String(x["id"]).localeCompare(String(y["id"])));
|
|
848
|
+
}
|
|
849
|
+
function mergeMessages(b, o, t, id, here, ctx) {
|
|
850
|
+
const keyOf = (m) => `${String(m["author"] ?? "")}|${String(m["ts"] ?? "")}`;
|
|
851
|
+
const index = (c) => {
|
|
852
|
+
const m = /* @__PURE__ */ new Map();
|
|
853
|
+
for (const x of asArr(c?.["messages"])) if (isObject(x)) m.set(keyOf(x), x);
|
|
854
|
+
return m;
|
|
855
|
+
};
|
|
856
|
+
const Bm = index(b), Om = index(o), Tm = index(t);
|
|
857
|
+
const out = [];
|
|
858
|
+
for (const k of /* @__PURE__ */ new Set([...Bm.keys(), ...Om.keys(), ...Tm.keys()])) {
|
|
859
|
+
const mb = Bm.get(k), mo = Om.get(k), mt = Tm.get(k);
|
|
860
|
+
if ([mb, mo, mt].some((m) => m?.["deleted"] === true) || mb !== void 0 && (mo === void 0 || mt === void 0)) {
|
|
861
|
+
out.push({ ...mo ?? mt ?? mb, body: "", deleted: true });
|
|
862
|
+
} else if (mo !== void 0 && mt !== void 0) {
|
|
863
|
+
out.push(merge3(mb, mo, mt, id, `${here}.messages[${k}]`, ctx));
|
|
864
|
+
} else {
|
|
865
|
+
out.push(mo ?? mt);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const ts = (m) => String(m["ts"] ?? "");
|
|
869
|
+
return out.sort((x, y) => ts(x).localeCompare(ts(y)) || String(x["author"] ?? "").localeCompare(String(y["author"] ?? "")));
|
|
870
|
+
}
|
|
803
871
|
function strategyForType(decl) {
|
|
804
872
|
switch (decl.type) {
|
|
805
873
|
case "flags":
|
|
@@ -908,6 +976,7 @@ function orphanedEntries(data, exists) {
|
|
|
908
976
|
}
|
|
909
977
|
|
|
910
978
|
exports.ATOMIC = ATOMIC;
|
|
979
|
+
exports.COMMENTS = COMMENTS;
|
|
911
980
|
exports.HOSTS = HOSTS;
|
|
912
981
|
exports.MANIFEST_FILE_SUFFIX = MANIFEST_FILE_SUFFIX;
|
|
913
982
|
exports.MANIFEST_SCHEMA = MANIFEST_SCHEMA;
|
|
@@ -928,6 +997,7 @@ exports.keyedBy = keyedBy;
|
|
|
928
997
|
exports.manifestProblems = manifestProblems;
|
|
929
998
|
exports.mapOf = mapOf;
|
|
930
999
|
exports.markUnresolved = markUnresolved;
|
|
1000
|
+
exports.mergeComments = mergeComments;
|
|
931
1001
|
exports.mergeExtensionData = mergeExtensionData;
|
|
932
1002
|
exports.mergeTree = mergeTree;
|
|
933
1003
|
exports.objectOf = objectOf;
|