strom-research 1.6.0 → 1.6.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.
- package/assets/lang/cs.json +3 -0
- package/assets/lang/de.json +3 -0
- package/dist/cli/ui.js +3 -0
- package/dist/commands/session.js +5 -1
- package/dist/commands/sync.js +7 -1
- package/dist/core/sync.js +106 -2
- package/dist/core/tree.js +1 -1
- package/dist/gedcom/export.js +1 -1
- package/package.json +1 -1
package/assets/lang/cs.json
CHANGED
|
@@ -488,10 +488,13 @@
|
|
|
488
488
|
"ui.sync.child.new": "{n}. {who}: dítě — {child}",
|
|
489
489
|
"ui.sync.child.gone": "{n}. {who}: už to není jejich dítě — {child}",
|
|
490
490
|
"ui.sync.person.gone": "{n}. už v něm není: {name}",
|
|
491
|
+
"ui.sync.place.coords": "{n}. {place}: poloha na mapě {from} → {at}",
|
|
492
|
+
"ui.sync.place.located": "{n}. {place}: poloha na mapě {at}",
|
|
491
493
|
"ui.sync.do.add": "přidá se jako vodítko",
|
|
492
494
|
"ui.sync.do.correct": "opraví vodítko výzkumu",
|
|
493
495
|
"ui.sync.do.conflict": "rozpor k rozhodnutí (výzkum to má ze záznamu)",
|
|
494
496
|
"ui.sync.do.user": "platí vaše úprava (údaj ze záznamu se stáhne s důvodem)",
|
|
497
|
+
"ui.sync.do.set": "výzkum převezme vaši polohu (žádný zápis ji nedokládá)",
|
|
495
498
|
"ui.sync.do.pick": "jen když ho vyberete (--only)",
|
|
496
499
|
"ui.sync.do.report": "nic se nemění — řekněte agentovi, co jste tím mysleli",
|
|
497
500
|
"ui.sync.edits": "Vaše úpravy údajů doložených záznamem: {mode} (strom config set sync.edits conflict|user)",
|
package/assets/lang/de.json
CHANGED
|
@@ -488,10 +488,13 @@
|
|
|
488
488
|
"ui.sync.child.new": "{n}. {who}: ein Kind — {child}",
|
|
489
489
|
"ui.sync.child.gone": "{n}. {who}: nicht mehr ihr Kind — {child}",
|
|
490
490
|
"ui.sync.person.gone": "{n}. nicht mehr darin: {name}",
|
|
491
|
+
"ui.sync.place.coords": "{n}. {place}: Lage auf der Karte {from} → {at}",
|
|
492
|
+
"ui.sync.place.located": "{n}. {place}: Lage auf der Karte {at}",
|
|
491
493
|
"ui.sync.do.add": "als Hinweis übernommen",
|
|
492
494
|
"ui.sync.do.correct": "berichtigt den Hinweis der Forschung",
|
|
493
495
|
"ui.sync.do.conflict": "ein Widerspruch zum Entscheiden (die Forschung hat es aus einem Eintrag)",
|
|
494
496
|
"ui.sync.do.user": "Ihre Änderung gilt (die Angabe aus dem Eintrag wird mit Grund zurückgezogen)",
|
|
497
|
+
"ui.sync.do.set": "die Forschung übernimmt Ihre Lage (kein Eintrag belegt sie)",
|
|
495
498
|
"ui.sync.do.pick": "nur wenn Sie sie auswählen (--only)",
|
|
496
499
|
"ui.sync.do.report": "nichts ändert sich — sagen Sie dem Agenten, was Sie meinten",
|
|
497
500
|
"ui.sync.edits": "Ihre Änderungen an Angaben, die ein Eintrag belegt: {mode} (strom config set sync.edits conflict|user)",
|
package/dist/cli/ui.js
CHANGED
|
@@ -420,10 +420,13 @@ export const UI = {
|
|
|
420
420
|
"ui.sync.child.new": "{n}. {who}: a child — {child}",
|
|
421
421
|
"ui.sync.child.gone": "{n}. {who}: no longer their child — {child}",
|
|
422
422
|
"ui.sync.person.gone": "{n}. no longer in it: {name}",
|
|
423
|
+
"ui.sync.place.coords": "{n}. {place}: on the map {from} → {at}",
|
|
424
|
+
"ui.sync.place.located": "{n}. {place}: on the map at {at}",
|
|
423
425
|
"ui.sync.do.add": "added as a lead",
|
|
424
426
|
"ui.sync.do.correct": "corrects the research's lead",
|
|
425
427
|
"ui.sync.do.conflict": "a conflict for you to decide (the research has it from a record)",
|
|
426
428
|
"ui.sync.do.user": "your edit wins (the record's fact withdrawn with the reason)",
|
|
429
|
+
"ui.sync.do.set": "the research takes your position (no record proves one)",
|
|
427
430
|
"ui.sync.do.pick": "only when you pick it (--only)",
|
|
428
431
|
"ui.sync.do.report": "nothing changes — tell the agent what you meant",
|
|
429
432
|
"ui.sync.edits": "Your edits to facts a record proves: {mode} (strom config set sync.edits conflict|user)",
|
package/dist/commands/session.js
CHANGED
|
@@ -613,6 +613,8 @@ register({
|
|
|
613
613
|
// Close what the agent left open, record the metrics, export, commit.
|
|
614
614
|
const after = Tree.open(root, runEnv);
|
|
615
615
|
let s = after.get(session.id);
|
|
616
|
+
// strom worked for the agent when it closed the session itself or recorded something
|
|
617
|
+
const ranStrom = s.state !== "open" || sessionWrites(after, s.id) > 0;
|
|
616
618
|
if (s.state === "open")
|
|
617
619
|
s = closeSession(after, s, {
|
|
618
620
|
summary: result.outcome === "stopped" ? phrase(after.lang, "session.user") : phrase(after.lang, "session.agent", { outcome: result.outcome }),
|
|
@@ -656,7 +658,9 @@ register({
|
|
|
656
658
|
break;
|
|
657
659
|
}
|
|
658
660
|
// An agent that may not run strom can do nothing: stop, do not burn session after session.
|
|
659
|
-
|
|
661
|
+
// A refused command that only starts with strom (strom … && sed …, a loop) is no such thing
|
|
662
|
+
// when strom worked for it otherwise.
|
|
663
|
+
if (!ranStrom && result.denied?.some((d) => /^Bash: (\S*\/)?strom\b/.test(d))) {
|
|
660
664
|
stopCode = "denied";
|
|
661
665
|
break;
|
|
662
666
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -21,6 +21,8 @@ function factText(f, lang) {
|
|
|
21
21
|
return "—";
|
|
22
22
|
return [f.value, humanDate(f.date, lang), humanPlace(f.place, undefined, lang)].filter(Boolean).join(", ") || "—";
|
|
23
23
|
}
|
|
24
|
+
/** A position on the map as a map shows it: degrees, six decimals at most. */
|
|
25
|
+
const point = (p) => `${+p.lat.toFixed(6)}, ${+p.lon.toFixed(6)}`;
|
|
24
26
|
/** One change as the user reads it, with what strom does with it. */
|
|
25
27
|
function changeLine(tree, c, incoming, lang) {
|
|
26
28
|
const name = (key) => {
|
|
@@ -44,8 +46,12 @@ function changeLine(tree, c, incoming, lang) {
|
|
|
44
46
|
text: truncate(c.text ?? "", 120),
|
|
45
47
|
child: name(c.child),
|
|
46
48
|
kids: c.kind === "family.new" && c.text ? ` + ${c.text.split(" ").filter(Boolean).map(name).join(", ")}` : "",
|
|
49
|
+
place: c.place?.name ?? "",
|
|
50
|
+
at: c.place ? point(c.place) : "",
|
|
51
|
+
from: c.place?.was ? point(c.place.was) : "",
|
|
47
52
|
};
|
|
48
|
-
|
|
53
|
+
const key = c.kind === "place.coords" && !c.place?.was ? "ui.sync.place.located" : `ui.sync.${c.kind}`;
|
|
54
|
+
return `${ui(lang, key, v)} → ${ui(lang, `ui.sync.do.${c.action}`)}`;
|
|
49
55
|
}
|
|
50
56
|
function planText(tree, plan, incoming, file, lang, edits) {
|
|
51
57
|
const shown = file.includes(" ") ? `"${file}"` : file;
|
package/dist/core/sync.js
CHANGED
|
@@ -40,6 +40,45 @@ const kindOf = (k) => (k === "CHR" ? "BAPM" : k);
|
|
|
40
40
|
export const factKey = (f) => [kindOf(f.kind), f.date ?? "", exact(f.place), exact(f.value), f.date || f.place ? "" : fold(f.label)].join("|");
|
|
41
41
|
/** A name for a comparison: "? /Novák/", "Jan //" and "Jan /?/" are the names the research has. */
|
|
42
42
|
const nameKey = (n) => exact(n.replace(/[/?]/g, " "));
|
|
43
|
+
/** A place's name as the Strom app keys its coordinates (placeKey of its places.ts): no accents, case, stops or commas. */
|
|
44
|
+
export const placeKey = (s) => foldText(s.replace(/[.,;]/g, " ")).replace(/\s+/g, " ").trim();
|
|
45
|
+
/** The Strom app's own key of a place's coordinates in its JSON (places.ts). */
|
|
46
|
+
const appPlaceKey = (s) => s
|
|
47
|
+
.normalize("NFD")
|
|
48
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[.,;]/g, " ")
|
|
51
|
+
.replace(/\s+/g, " ")
|
|
52
|
+
.trim();
|
|
53
|
+
/** GEDCOM's N50.042 / E15.358 (also a bare or negative number): degrees, the south and west negative. */
|
|
54
|
+
function degrees(v, neg, max) {
|
|
55
|
+
const m = /^\s*([NSEW])?\s*(-?\d{1,3}(?:\.\d+)?)\s*$/i.exec(v ?? "");
|
|
56
|
+
if (!m)
|
|
57
|
+
return undefined;
|
|
58
|
+
const n = Number(m[2]) * (m[1]?.toUpperCase() === neg ? -1 : 1);
|
|
59
|
+
return Number.isFinite(n) && Math.abs(n) <= max ? n : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** The places of a GEDCOM with their MAP > LATI/LONG, wherever a PLAC is. */
|
|
62
|
+
function gedPlaces(records) {
|
|
63
|
+
const out = new Map();
|
|
64
|
+
const walk = (n) => {
|
|
65
|
+
for (const c of n.children) {
|
|
66
|
+
const map = c.tag === "PLAC" ? c.children.find((x) => x.tag === "MAP") : undefined;
|
|
67
|
+
const lat = map ? degrees(val(map, "LATI"), "S", 90) : undefined;
|
|
68
|
+
const lon = map ? degrees(val(map, "LONG"), "W", 180) : undefined;
|
|
69
|
+
const name = c.value.replace(/\s+/g, " ").trim();
|
|
70
|
+
if (lat !== undefined && lon !== undefined && name && !out.has(placeKey(name)))
|
|
71
|
+
out.set(placeKey(name), { name, lat, lon });
|
|
72
|
+
walk(c);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (const r of records)
|
|
76
|
+
if (r.tag === "INDI" || r.tag === "FAM")
|
|
77
|
+
walk(r);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** The same point: the Strom app keeps six decimals. */
|
|
81
|
+
const samePoint = (a, b) => Math.abs(a.lat - b.lat) < 5e-7 && Math.abs(a.lon - b.lon) < 5e-7;
|
|
43
82
|
/** The Strom app's "Birth (alternative record)": a second birth or death of ours coming back as an event of its own. */
|
|
44
83
|
const ALTERNATIVE = {
|
|
45
84
|
"birth (alternative record)": "BIRT", "death (alternative record)": "DEAT",
|
|
@@ -155,7 +194,7 @@ export function readGedcom(text) {
|
|
|
155
194
|
}
|
|
156
195
|
const treeId = val(head, "_STROM_TREE");
|
|
157
196
|
const at = val(head, "_STROM_HEAD");
|
|
158
|
-
return { format: "gedcom", ...(treeId ? { treeId } : {}), ...(at ? { head: at } : {}), persons, families, problems };
|
|
197
|
+
return { format: "gedcom", ...(treeId ? { treeId } : {}), ...(at ? { head: at } : {}), persons, families, places: gedPlaces(records), problems };
|
|
159
198
|
}
|
|
160
199
|
/** The Strom app's life events → our kinds. */
|
|
161
200
|
const APP_EVENTS = {
|
|
@@ -236,8 +275,19 @@ export function readStromJson(data) {
|
|
|
236
275
|
}
|
|
237
276
|
families.push({ partners, children: (u.childIds ?? []).map((c) => keys.get(c)).filter((k) => !!k), facts });
|
|
238
277
|
}
|
|
278
|
+
// the app's coordinates are kept for the place's name (its placeKey): the names are its facts'
|
|
279
|
+
const places = new Map();
|
|
280
|
+
const geo = d.places && typeof d.places === "object" ? d.places : {};
|
|
281
|
+
for (const f of [...persons.values()].flatMap((p) => p.facts).concat(families.flatMap((x) => x.facts))) {
|
|
282
|
+
const k = f.place ? placeKey(f.place) : "";
|
|
283
|
+
const g = f.place ? geo[appPlaceKey(f.place)] : undefined;
|
|
284
|
+
if (!g || places.has(k) || typeof g.lat !== "number" || typeof g.lon !== "number")
|
|
285
|
+
continue;
|
|
286
|
+
if (Math.abs(g.lat) <= 90 && Math.abs(g.lon) <= 180)
|
|
287
|
+
places.set(k, { name: f.place, lat: g.lat, lon: g.lon });
|
|
288
|
+
}
|
|
239
289
|
const r = d.research;
|
|
240
|
-
return { format: "strom-json", ...(r?.id ? { treeId: r.id } : {}), ...(r?.head ? { head: r.head } : {}), persons, families, problems: [] };
|
|
290
|
+
return { format: "strom-json", ...(r?.id ? { treeId: r.id } : {}), ...(r?.head ? { head: r.head } : {}), persons, families, places, problems: [] };
|
|
241
291
|
}
|
|
242
292
|
/** A file coming back: GEDCOM or the Strom app's JSON; empty or unreadable throws. */
|
|
243
293
|
export function readTreeFile(file) {
|
|
@@ -635,8 +685,32 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
635
685
|
push({ kind: "child.gone", action: "report", family: fam.id, partners, child: c });
|
|
636
686
|
}
|
|
637
687
|
}
|
|
688
|
+
// places on the map: no record proves a position, so the user's is taken — set where the research has none,
|
|
689
|
+
// or changed in the app since it was given (the research's as it was given); anything else only when picked
|
|
690
|
+
const ourPlaces = researchPlaces(tree);
|
|
691
|
+
for (const [k, p] of incoming.places ?? []) {
|
|
692
|
+
const mine = ourPlaces.get(k);
|
|
693
|
+
const here = mine?.coords;
|
|
694
|
+
if (here && samePoint(here, p))
|
|
695
|
+
continue;
|
|
696
|
+
const given = base?.places?.get(k);
|
|
697
|
+
if (given && samePoint(given, p))
|
|
698
|
+
continue; // as it was given: the research moved it since
|
|
699
|
+
const action = !here || (given && samePoint(given, here)) ? "set" : "pick";
|
|
700
|
+
push({ kind: "place.coords", action, place: { ...p, ...(mine ? { id: mine.id } : {}), ...(here ? { was: here } : {}) } });
|
|
701
|
+
}
|
|
638
702
|
return { changes, base: !!base, ...(incoming.head ? { head: incoming.head } : {}), partial, identity };
|
|
639
703
|
}
|
|
704
|
+
/** The research's places by placeKey of each of their names — one with coordinates first, as the export gives them. */
|
|
705
|
+
function researchPlaces(tree) {
|
|
706
|
+
const out = new Map();
|
|
707
|
+
for (const p of tree.list("place"))
|
|
708
|
+
if (!p.retracted)
|
|
709
|
+
for (const n of p.names)
|
|
710
|
+
if (!out.has(placeKey(n.name)) || (p.coords && !out.get(placeKey(n.name)).coords))
|
|
711
|
+
out.set(placeKey(n.name), p);
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
640
714
|
/** Does the research's fact say all the file's fact says, and more: the same kind, and each of its date, place, value the same or missing in the file. */
|
|
641
715
|
/**
|
|
642
716
|
* The Strom app keeps a couple's marriages and divorces as the course of one union: two marriages with no divorce
|
|
@@ -798,6 +872,25 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
798
872
|
applied.push({ do: "child.add", id: c.family, before: child });
|
|
799
873
|
break;
|
|
800
874
|
}
|
|
875
|
+
case "place.coords": {
|
|
876
|
+
const p = c.place;
|
|
877
|
+
if (!p)
|
|
878
|
+
break;
|
|
879
|
+
const coords = { lat: p.lat, lon: p.lon };
|
|
880
|
+
const mine = p.id ? tree.get(p.id) : undefined;
|
|
881
|
+
if (mine && !mine.retracted) {
|
|
882
|
+
update(tree, mine.id, "place", ({ unlocated: _found, ...rest }) => ({ ...rest, coords }), {
|
|
883
|
+
op: "place.edit",
|
|
884
|
+
summary: `${mine.id} at ${p.lat}, ${p.lon}: ${reason}`,
|
|
885
|
+
});
|
|
886
|
+
applied.push({ do: "place.edit", id: mine.id, before: JSON.stringify({ coords: mine.coords, unlocated: mine.unlocated }) });
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
const made = create(tree, "place", { names: [{ name: p.name }], coords, jurisdictions: [], note: `the position from ${reason}` }, (id) => `+${id} place "${p.name}" (from ${source.id})`);
|
|
890
|
+
applied.push({ do: "place.add", id: made.id });
|
|
891
|
+
}
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
801
894
|
default:
|
|
802
895
|
break;
|
|
803
896
|
}
|
|
@@ -858,6 +951,17 @@ export function undoSync(tree, input) {
|
|
|
858
951
|
if (!tree.get(a.id)?.retracted)
|
|
859
952
|
retractPerson(tree, a.id, reason);
|
|
860
953
|
break;
|
|
954
|
+
case "place.edit": {
|
|
955
|
+
const b = JSON.parse(String(a.before ?? "{}"));
|
|
956
|
+
update(tree, a.id, "place", ({ coords: _now, unlocated: _u, ...p }) => ({ ...p, ...(b.coords ? { coords: b.coords } : {}), ...(b.unlocated ? { unlocated: b.unlocated } : {}) }), {
|
|
957
|
+
op: "place.edit",
|
|
958
|
+
summary: `${a.id} position back: ${reason}`,
|
|
959
|
+
});
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
case "place.add":
|
|
963
|
+
update(tree, a.id, "place", (p) => ({ ...p, retracted: { at: now(), reason } }), { op: "place.retract", summary: `${a.id} retracted: ${reason}` });
|
|
964
|
+
break;
|
|
861
965
|
}
|
|
862
966
|
n++;
|
|
863
967
|
}
|
package/dist/core/tree.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as git from "./git.js";
|
|
|
23
23
|
import { commitSeal, createKey, fingerprint, loadKey, sha256, signOp } from "./seal.js";
|
|
24
24
|
import { stringifyCanonical } from "./json.js";
|
|
25
25
|
export const TREE_FILE = "strom.json";
|
|
26
|
-
export const VERSION = "1.6.
|
|
26
|
+
export const VERSION = "1.6.1";
|
|
27
27
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
28
28
|
.strom/
|
|
29
29
|
/media/
|
package/dist/gedcom/export.js
CHANGED
|
@@ -120,7 +120,7 @@ export function exportGedcom(tree, opts = {}) {
|
|
|
120
120
|
// Places with coordinates, looked up by folded name.
|
|
121
121
|
const coords = new Map();
|
|
122
122
|
for (const pl of tree.list("place"))
|
|
123
|
-
if (pl.coords)
|
|
123
|
+
if (pl.coords && !pl.retracted)
|
|
124
124
|
for (const n of pl.names)
|
|
125
125
|
coords.set(foldText(n.name), pl.coords);
|
|
126
126
|
// What the research knows beyond the facts (the Strom profile, opts.research): read once.
|