strom-research 1.10.0 → 1.11.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.
- package/dist/commands/output.js +3 -2
- package/dist/commands/sync.js +11 -8
- package/dist/core/actions.js +7 -2
- package/dist/core/live.js +3 -2
- package/dist/core/stromapp.js +21 -0
- package/dist/core/sync.js +103 -16
- package/dist/core/tree.js +1 -1
- package/dist/gedcom/export.js +2 -2
- package/dist/gedcom/validate.js +1 -1
- package/package.json +1 -1
package/dist/commands/output.js
CHANGED
|
@@ -13,7 +13,7 @@ import { UsageError } from "../core/errors.js";
|
|
|
13
13
|
import { excerptSettings, planExcerpts } from "../core/excerpt.js";
|
|
14
14
|
import { runGit, tracked } from "../core/git.js";
|
|
15
15
|
import { Settings } from "../core/config.js";
|
|
16
|
-
import { appOpensLinks, appShowsEdges, appShowsStoryDrafts } from "../core/stromapp.js";
|
|
16
|
+
import { appOpensLinks, appShowsCoupleEvents, appShowsEdges, appShowsStoryDrafts } from "../core/stromapp.js";
|
|
17
17
|
import { EXCERPT_QUALITIES, EXCERPT_SCOPES } from "../core/model.js";
|
|
18
18
|
function findingRows(f) {
|
|
19
19
|
return table(f.slice(0, 30).map((x) => [x.level === "error" ? "ERROR" : "warn", `line ${x.line}`, x.message]));
|
|
@@ -77,7 +77,8 @@ export function writeTreeGed(tree, opts) {
|
|
|
77
77
|
const edges = opts.for === "strom" && appShowsEdges(new Settings(tree.env, {}), opts.stromVersion);
|
|
78
78
|
// …and the new versions of stories the user approved, for an app that shows them
|
|
79
79
|
const storyDrafts = opts.for === "strom" && appShowsStoryDrafts(new Settings(tree.env, {}), opts.stromVersion);
|
|
80
|
-
const
|
|
80
|
+
const coupleResi = opts.for === "strom" && appShowsCoupleEvents(new Settings(tree.env, {}), opts.stromVersion);
|
|
81
|
+
const result = exportGedcom(tree, { for: opts.for, stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}) });
|
|
81
82
|
const findings = validateGedcom(result.text, opts.for === "standard" ? { strict: true } : {});
|
|
82
83
|
const file = opts.out ?? gedFile(tree, opts.for, opts.research);
|
|
83
84
|
if (!hasGedErrors(findings) && !tree.dryRun) {
|
package/dist/commands/sync.js
CHANGED
|
@@ -17,16 +17,17 @@ import { applySync, nothingSince, planSync, readTreeFile, receivedSince, undoSyn
|
|
|
17
17
|
import { startLive } from "../core/live.js";
|
|
18
18
|
import { appSendsChanges, installedStromApp, sendAppUrl, stromAppUrl } from "../core/stromapp.js";
|
|
19
19
|
import { chromiumBrowser, openInBrowser, openWebApp } from "../core/chromium.js";
|
|
20
|
-
function factText(f, lang) {
|
|
20
|
+
function factText(f, lang, name = (k) => k) {
|
|
21
21
|
if (!f)
|
|
22
22
|
return "—";
|
|
23
|
-
return [f.value, humanDate(f.date, lang), humanPlace(f.place, undefined, lang), detailText(f, lang)].filter(Boolean).join(", ") || "—";
|
|
23
|
+
return [f.value, humanDate(f.date, lang), humanPlace(f.place, undefined, lang), detailText(f, lang, name)].filter(Boolean).join(", ") || "—";
|
|
24
24
|
}
|
|
25
|
-
/** What a fact says besides: its house, age and cause. */
|
|
26
|
-
function detailText(f, lang) {
|
|
25
|
+
/** What a fact says besides: its house, age (a couple's: each partner's) and cause. */
|
|
26
|
+
function detailText(f, lang, name = (k) => k) {
|
|
27
27
|
if (!f)
|
|
28
28
|
return "";
|
|
29
|
-
|
|
29
|
+
const ages = Object.entries(f.ages ?? {}).map(([who, a]) => `${name(who)} ${humanAge(a, lang)}`);
|
|
30
|
+
return [f.house && ui(lang, "ui.sync.house", { x: f.house }), f.age && humanAge(f.age, lang), ...ages, f.cause && ui(lang, "ui.sync.cause", { x: f.cause })].filter(Boolean).join(", ");
|
|
30
31
|
}
|
|
31
32
|
/** A position on the map as a map shows it: degrees, six decimals at most. */
|
|
32
33
|
const point = (p) => `${+p.lat.toFixed(6)}, ${+p.lon.toFixed(6)}`;
|
|
@@ -41,15 +42,17 @@ function changeLine(tree, c, incoming, lang) {
|
|
|
41
42
|
return p ? `${(p.names[0] ? `${p.names[0].given} ${p.names[0].surname}` : key).trim()} [${key}]` : key;
|
|
42
43
|
};
|
|
43
44
|
const who = c.partners ? c.partners.map(name).join(" & ") || c.family || "?" : name(c.person);
|
|
45
|
+
// a partner in their age: the given name
|
|
46
|
+
const short = (key) => (key.startsWith("x:") ? name(key) : (tree.get(key)?.names[0]?.given ?? key));
|
|
44
47
|
const kind = (f) => (f ? eventName(f.kind, lang, f.label) : "");
|
|
45
48
|
const v = {
|
|
46
49
|
n: c.n,
|
|
47
50
|
who,
|
|
48
51
|
name: name(c.person),
|
|
49
52
|
kind: kind(c.fact ?? c.was),
|
|
50
|
-
fact: `${kind(c.fact ?? c.was)} ${factText(c.fact ?? c.was, lang)}`,
|
|
51
|
-
was: c.kind === "fact.detail" ? detailText(c.was, lang) || "—" : factText(c.was, lang),
|
|
52
|
-
now: c.kind === "fact.detail" ? detailText(c.fact, lang) : factText(c.fact, lang),
|
|
53
|
+
fact: `${kind(c.fact ?? c.was)} ${factText(c.fact ?? c.was, lang, short)}`,
|
|
54
|
+
was: c.kind === "fact.detail" ? detailText(c.was, lang, short) || "—" : factText(c.was, lang, short),
|
|
55
|
+
now: c.kind === "fact.detail" ? detailText(c.fact, lang, short) : factText(c.fact, lang, short),
|
|
53
56
|
when: [humanDate(c.fact?.date, lang), humanPlace(c.fact?.place, undefined, lang)].filter(Boolean).join(", "),
|
|
54
57
|
text: truncate(c.text ?? "", 120),
|
|
55
58
|
child: name(c.child),
|
package/dist/core/actions.js
CHANGED
|
@@ -591,7 +591,8 @@ export function editEvent(tree, eventId, edit, reason) {
|
|
|
591
591
|
const date = edit.date === undefined ? undefined : parseDate(edit.date);
|
|
592
592
|
// an empty age takes it away
|
|
593
593
|
const age = edit.age === undefined ? undefined : edit.age.trim() ? parseAge(edit.age) : "";
|
|
594
|
-
|
|
594
|
+
// an empty age of a partner takes it away
|
|
595
|
+
const ages = edit.ages ? Object.fromEntries(Object.entries(edit.ages).map(([who, a]) => [who, a.trim() ? parseAge(a, who) : ""])) : undefined;
|
|
595
596
|
return replaceEvent(tree, eventId, (e) => {
|
|
596
597
|
const changed = [];
|
|
597
598
|
const set = (k, v) => {
|
|
@@ -621,7 +622,11 @@ export function editEvent(tree, eventId, edit, reason) {
|
|
|
621
622
|
for (const [who, a] of Object.entries(ages))
|
|
622
623
|
if (e.ages?.[who] !== undefined && e.ages[who] !== a)
|
|
623
624
|
changed.push(`age of ${who}`);
|
|
624
|
-
|
|
625
|
+
const merged = Object.fromEntries(Object.entries({ ...e.ages, ...ages }).filter(([, a]) => a));
|
|
626
|
+
if (Object.keys(merged).length)
|
|
627
|
+
next.ages = merged;
|
|
628
|
+
else
|
|
629
|
+
delete next.ages;
|
|
625
630
|
}
|
|
626
631
|
const key = (p) => `${p.role}|${p.person ?? foldText(p.name ?? "")}`;
|
|
627
632
|
if (edit.drop?.length) {
|
package/dist/core/live.js
CHANGED
|
@@ -49,7 +49,7 @@ import { rankTasks } from "./queue.js";
|
|
|
49
49
|
import { humanTask } from "../cli/human.js";
|
|
50
50
|
import { knownNewerVersion } from "./update.js";
|
|
51
51
|
import { gitProgram, runGit } from "./git.js";
|
|
52
|
-
import { appOpensLinks, appShowsEdges, appShowsStoryDrafts, isStromAppOrigin } from "./stromapp.js";
|
|
52
|
+
import { appOpensLinks, appShowsCoupleEvents, appShowsEdges, appShowsStoryDrafts, isStromAppOrigin } from "./stromapp.js";
|
|
53
53
|
import { Settings } from "./config.js";
|
|
54
54
|
import { linkActions, linkHandlerState } from "./links.js";
|
|
55
55
|
import { stromLauncher } from "./self.js";
|
|
@@ -596,11 +596,12 @@ export function serveLive(root, env) {
|
|
|
596
596
|
// …and where the tree ends, for an app that shows it
|
|
597
597
|
const edges = appShowsEdges(new Settings(env, {}));
|
|
598
598
|
const storyDrafts = appShowsStoryDrafts(new Settings(env, {}));
|
|
599
|
+
const coupleResi = appShowsCoupleEvents(new Settings(env, {}));
|
|
599
600
|
const offered = opens ? links(env) : [];
|
|
600
601
|
ged = {
|
|
601
602
|
head: h,
|
|
602
603
|
links: offered.join(" "),
|
|
603
|
-
text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(offered.length ? { links: offered } : {}) }).text,
|
|
604
|
+
text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(offered.length ? { links: offered } : {}) }).text,
|
|
604
605
|
};
|
|
605
606
|
}
|
|
606
607
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Strom-Head": h }).end(ged.text);
|
package/dist/core/stromapp.js
CHANGED
|
@@ -82,6 +82,27 @@ export function appShowsEdges(settings, version) {
|
|
|
82
82
|
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The Strom app keeps a couple's events as events (the app's spec docs/ZADANI_VYZKUM_udalosti-paru.md, its 3.8.0):
|
|
87
|
+
* their residence goes to it as 1 RESI under FAM (before: an EVEN named so — an older app keeps either in the couple's
|
|
88
|
+
* note). stromapp.info from 3.8.0 (2026-10-02); another copy of the app (its beta, its development) is taken as current.
|
|
89
|
+
* An app of unknown version: today's.
|
|
90
|
+
*/
|
|
91
|
+
export const APP_SHOWS_COUPLE_EVENTS = "3.8.0";
|
|
92
|
+
export function appShowsCoupleEvents(settings, version) {
|
|
93
|
+
if (stromAppUrl(settings) !== STROM_APP_URL)
|
|
94
|
+
return true;
|
|
95
|
+
if (!APP_SHOWS_COUPLE_EVENTS)
|
|
96
|
+
return false;
|
|
97
|
+
if (!version)
|
|
98
|
+
return true;
|
|
99
|
+
const n = (v) => v.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
100
|
+
const [a, b] = [n(version), n(APP_SHOWS_COUPLE_EVENTS)];
|
|
101
|
+
for (let i = 0; i < 3; i++)
|
|
102
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
103
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
85
106
|
/**
|
|
86
107
|
* The Strom app shows the new version of a story the user approved, waiting beside it (the app's spec
|
|
87
108
|
* docs/ZADANI_VYZKUM_vypraveni-zamek.md): 2 _DRAFT under _STORY, and in the bridge's waiting (kind story). stromapp.info
|
package/dist/core/sync.js
CHANGED
|
@@ -28,6 +28,7 @@ import { fromFlexDate, houseOf, importDate } from "./import.js";
|
|
|
28
28
|
import { normalizeAge } from "./age.js";
|
|
29
29
|
import { create, update } from "./records.js";
|
|
30
30
|
import { foldText } from "./text.js";
|
|
31
|
+
import { labels } from "../gedcom/labels.js";
|
|
31
32
|
import { UsageError } from "./errors.js";
|
|
32
33
|
import * as git from "./git.js";
|
|
33
34
|
import { now, Tree } from "./tree.js";
|
|
@@ -88,8 +89,8 @@ const ALTERNATIVE = {
|
|
|
88
89
|
};
|
|
89
90
|
/** Facts there is one of: an edit of one is a change, not a second fact. */
|
|
90
91
|
const ONE = new Set(["BIRT", "BAPM", "DEAT", "BURI", "CREM", "MARR", "DIV"]);
|
|
91
|
-
function gedFacts(node) {
|
|
92
|
-
return cleanFacts(rawFacts(node));
|
|
92
|
+
function gedFacts(node, couple) {
|
|
93
|
+
return cleanFacts(rawFacts(node, couple));
|
|
93
94
|
}
|
|
94
95
|
/** What the Strom app adds of its own: an alternative birth or death as an event, a "Birth record" beside a birth, every couple married. */
|
|
95
96
|
function cleanFacts(facts) {
|
|
@@ -109,7 +110,7 @@ function cleanFacts(facts) {
|
|
|
109
110
|
// a marriage with nothing known of it: every couple of the Strom app is "married"
|
|
110
111
|
!(f.kind === "MARR" && !f.date && !f.place && !f.value));
|
|
111
112
|
}
|
|
112
|
-
function rawFacts(node) {
|
|
113
|
+
function rawFacts(node, couple) {
|
|
113
114
|
const out = [];
|
|
114
115
|
for (const c of node.children) {
|
|
115
116
|
if (["NAME", "SEX", "REFN", "NOTE", "FAMC", "FAMS", "HUSB", "WIFE", "CHIL", "SOUR", "OBJE", "ASSO", "_STORY"].includes(c.tag))
|
|
@@ -129,7 +130,20 @@ function rawFacts(node) {
|
|
|
129
130
|
f.value = value.replace(/\s+/g, " ");
|
|
130
131
|
if (kind === "EVEN")
|
|
131
132
|
f.label = val(c, "TYPE") ?? f.value ?? "EVEN";
|
|
132
|
-
|
|
133
|
+
// a couple's residence written as an event named so (for a Strom app before its couple's events): their residence
|
|
134
|
+
if (couple && kind === "EVEN" && COUPLE_RESIDENCE.has(fold(f.label))) {
|
|
135
|
+
f.kind = "RESI";
|
|
136
|
+
delete f.label;
|
|
137
|
+
}
|
|
138
|
+
details(f, val(c, "CAUS"), couple ? undefined : val(c, "AGE"), val(c, "ADDR"));
|
|
139
|
+
if (couple)
|
|
140
|
+
for (const role of ["HUSB", "WIFE"]) {
|
|
141
|
+
const who = couple[role];
|
|
142
|
+
const age = val(c.children.find((x) => x.tag === role), "AGE");
|
|
143
|
+
const a = who && age?.trim() ? normalizeAge(age) : undefined;
|
|
144
|
+
if (a)
|
|
145
|
+
f.ages = { ...f.ages, [who]: a };
|
|
146
|
+
}
|
|
133
147
|
const id = val(c, "_EID");
|
|
134
148
|
if (id)
|
|
135
149
|
f.id = id;
|
|
@@ -149,8 +163,21 @@ function details(f, cause, age, house) {
|
|
|
149
163
|
if (h)
|
|
150
164
|
f.house = h;
|
|
151
165
|
}
|
|
152
|
-
/** The details of a fact the research compares
|
|
166
|
+
/** The details of a fact the research compares, besides the partners' ages. */
|
|
153
167
|
const DETAILS = ["cause", "age", "house"];
|
|
168
|
+
/** A couple's residence as an event of that name (the export's label in any language). */
|
|
169
|
+
const COUPLE_RESIDENCE = new Set(["en", "cs", "de", "pl", "sk"].map((l) => fold(labels(l)("RESI"))));
|
|
170
|
+
/** What a fact says besides, one entry each: cause, age, house, and "ages:<key>" for a partner's age. */
|
|
171
|
+
function detailsOf(f) {
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const d of DETAILS)
|
|
174
|
+
if (f[d])
|
|
175
|
+
out[d] = f[d];
|
|
176
|
+
for (const [who, a] of Object.entries(f.ages ?? {}))
|
|
177
|
+
if (a)
|
|
178
|
+
out[`ages:${who}`] = a;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
154
181
|
/** Every note under a record, its facts' and citations' too, as one text. */
|
|
155
182
|
function allNotes(n, noteText) {
|
|
156
183
|
const out = [];
|
|
@@ -206,7 +233,7 @@ export function readGedcom(text) {
|
|
|
206
233
|
const families = [];
|
|
207
234
|
for (const r of records.filter((x) => x.tag === "FAM")) {
|
|
208
235
|
const who = (tag) => children(r, tag).map((c) => keys.get(c.value.trim())).filter((k) => !!k && persons.has(k));
|
|
209
|
-
families.push({ partners: [...who("HUSB"), ...who("WIFE")], children: who("CHIL"), facts: gedFacts(r) });
|
|
236
|
+
families.push({ partners: [...who("HUSB"), ...who("WIFE")], children: who("CHIL"), facts: gedFacts(r, { HUSB: who("HUSB")[0], WIFE: who("WIFE")[0] }) });
|
|
210
237
|
}
|
|
211
238
|
const treeId = val(head, "_STROM_TREE");
|
|
212
239
|
const at = val(head, "_STROM_HEAD");
|
|
@@ -219,6 +246,11 @@ const APP_EVENTS = {
|
|
|
219
246
|
firstCommunion: "FCOM", barMitzvah: "BARM", batMitzvah: "BASM", ordination: "ORDN", adoption: "ADOP", naturalization: "NATU",
|
|
220
247
|
will: "WILL", probate: "PROB", title: "TITL", nationality: "NATI", cremation: "CREM",
|
|
221
248
|
};
|
|
249
|
+
/** The Strom app's events of a couple (its 3.8) → GEDCOM tags. */
|
|
250
|
+
const APP_COUPLE_EVENTS = {
|
|
251
|
+
engagement: "ENGA", banns: "MARB", marriageLicence: "MARL", marriageContract: "MARC", marriageSettlement: "MARS",
|
|
252
|
+
residence: "RESI", census: "CENS", divorceFiled: "DIVF", annulment: "ANUL", custom: "EVEN",
|
|
253
|
+
};
|
|
222
254
|
/** Facts whose value is what they are (the GEDCOM tag's value). */
|
|
223
255
|
const VALUE_KINDS = new Set(["OCCU", "RELI", "TITL", "NATI"]);
|
|
224
256
|
/** A tree of the Strom app (its JSON) as a snapshot. */
|
|
@@ -277,6 +309,17 @@ export function readStromJson(data) {
|
|
|
277
309
|
said: p.notes ?? "",
|
|
278
310
|
});
|
|
279
311
|
}
|
|
312
|
+
// a partner's age by the app's person id → by the person's key
|
|
313
|
+
const partnersAges = (ages) => {
|
|
314
|
+
const out = {};
|
|
315
|
+
for (const [id, raw] of Object.entries(ages ?? {})) {
|
|
316
|
+
const key = keys.get(id);
|
|
317
|
+
const a = key && typeof raw === "string" && raw.trim() ? normalizeAge(raw) : undefined;
|
|
318
|
+
if (key && a)
|
|
319
|
+
out[key] = a;
|
|
320
|
+
}
|
|
321
|
+
return Object.keys(out).length ? out : undefined;
|
|
322
|
+
};
|
|
280
323
|
const families = [];
|
|
281
324
|
for (const u of Object.values(d.partnerships ?? {})) {
|
|
282
325
|
const partners = [u.person1Id, u.person2Id].map((x) => (x ? keys.get(x) : undefined)).filter((k) => !!k);
|
|
@@ -289,8 +332,31 @@ export function readStromJson(data) {
|
|
|
289
332
|
if (u.startPlace?.trim())
|
|
290
333
|
f.place = u.startPlace.trim();
|
|
291
334
|
details(f, undefined, undefined, u.address);
|
|
335
|
+
const ages = partnersAges(u.ages);
|
|
336
|
+
if (ages)
|
|
337
|
+
f.ages = ages;
|
|
292
338
|
facts.push(f);
|
|
293
339
|
}
|
|
340
|
+
// the couple's other events (the app's 3.8)
|
|
341
|
+
for (const e of u.events ?? []) {
|
|
342
|
+
const kind = APP_COUPLE_EVENTS[e.type];
|
|
343
|
+
if (!kind)
|
|
344
|
+
continue;
|
|
345
|
+
const f = { kind };
|
|
346
|
+
const g = fromFlexDate(e.date);
|
|
347
|
+
if (g)
|
|
348
|
+
f.date = g;
|
|
349
|
+
if (e.place?.trim())
|
|
350
|
+
f.place = e.place.trim().replace(/\s+/g, " ");
|
|
351
|
+
if (kind === "EVEN")
|
|
352
|
+
f.label = e.customLabel || "EVEN";
|
|
353
|
+
details(f, e.cause, undefined, e.address);
|
|
354
|
+
const ages = partnersAges(e.ages);
|
|
355
|
+
if (ages)
|
|
356
|
+
f.ages = ages;
|
|
357
|
+
if (f.date || f.place || f.label || kind !== "EVEN")
|
|
358
|
+
facts.push(f);
|
|
359
|
+
}
|
|
294
360
|
// the divorce (the app's 3.7 keeps its place too)
|
|
295
361
|
if (u.status === "divorced" && (u.endDate || u.endPlace)) {
|
|
296
362
|
const f = { kind: "DIV" };
|
|
@@ -612,15 +678,19 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
612
678
|
if (ourKeys.has(k)) {
|
|
613
679
|
// the research has it: what the file says of it besides (its cause, age, house) may be the user's — paired
|
|
614
680
|
// with the same details first (one lived in several houses of a place: each its own fact)
|
|
615
|
-
const sameDetails = (a, b) =>
|
|
681
|
+
const sameDetails = (a, b) => {
|
|
682
|
+
const [x, y] = [detailsOf(a), detailsOf(b)];
|
|
683
|
+
return Object.keys(x).every((d) => exact(x[d]) === exact(y[d]));
|
|
684
|
+
};
|
|
616
685
|
const mates = our.filter((x) => factKey(x) === k);
|
|
617
686
|
if (mates.some((x) => sameDetails(f, x)) || was?.some((x) => factKey(x) === k && sameDetails(f, x)))
|
|
618
687
|
continue;
|
|
619
688
|
const o = mates.find((x) => !inc.some((i) => factKey(i) === k && sameDetails(i, x))) ?? mates[0];
|
|
620
689
|
const b = was?.find((x) => factKey(x) === k && (!x.id || x.id === o.id));
|
|
621
|
-
const
|
|
690
|
+
const [fd, od, bd] = [detailsOf(f), detailsOf(o), b ? detailsOf(b) : undefined];
|
|
691
|
+
const changed = Object.keys(fd).filter((d) => exact(fd[d]) !== exact(od[d]) && !(bd && exact(fd[d]) === exact(bd[d])));
|
|
622
692
|
if (changed.length) {
|
|
623
|
-
const added = changed.every((d) => !
|
|
693
|
+
const added = changed.every((d) => !od[d]);
|
|
624
694
|
const action = added ? "add" : !was ? "pick" : recordBacked(tree, eventById(tree, o.id)) ? "conflict" : "correct";
|
|
625
695
|
if (action !== "conflict" || !openConflict(tree, owner.person ?? owner.family, f))
|
|
626
696
|
push({ kind: "fact.detail", action, ...owner, fact: f, was: o });
|
|
@@ -896,13 +966,17 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
896
966
|
applied.push({ do: "conflict.add", id: x.id });
|
|
897
967
|
break;
|
|
898
968
|
}
|
|
899
|
-
const
|
|
900
|
-
|
|
969
|
+
const [fd, md] = [detailsOf(c.fact), detailsOf(mine)];
|
|
970
|
+
const changed = Object.keys(fd).filter((d) => fd[d] !== md[d] && (!d.startsWith("ages:") || !!who(d.slice(5))));
|
|
971
|
+
if (!changed.length)
|
|
972
|
+
break;
|
|
973
|
+
editEvent(tree, mine.id, detailEdit(Object.fromEntries(changed.map((d) => [d.startsWith("ages:") ? `ages:${who(d.slice(5))}` : d, fd[d]]))), `${c.action === "add" ? "added from" : "corrected in"} ${reason}`);
|
|
974
|
+
const set = Object.fromEntries(changed.map((d) => [d, fd[d]]));
|
|
901
975
|
update(tree, owner, owner.startsWith("F") ? "family" : "person", (o) => ({ ...o, events: o.events.map((e) => (e.id === mine.id && !e.citations.some((x) => x.source === source.id) ? { ...e, citations: [...e.citations, cite(`${c.fact.kind} ${Object.keys(set).join(" ")}`)] } : e)) }), {
|
|
902
976
|
op: "event.cite",
|
|
903
977
|
summary: `${mine.id} cites ${source.id}`,
|
|
904
978
|
});
|
|
905
|
-
applied.push({ do: "event.detail", id: mine.id, before: JSON.stringify(Object.fromEntries(
|
|
979
|
+
applied.push({ do: "event.detail", id: mine.id, before: JSON.stringify(Object.fromEntries(changed.map((d) => [d.startsWith("ages:") ? `ages:${who(d.slice(5))}` : d, md[d] ?? ""]))) });
|
|
906
980
|
break;
|
|
907
981
|
}
|
|
908
982
|
case "name.new": {
|
|
@@ -975,8 +1049,20 @@ function nameOf(gedName) {
|
|
|
975
1049
|
return { given: gedName.trim(), surname: "" };
|
|
976
1050
|
return { given: `${m[1].trim()} ${m[3].trim()}`.trim(), surname: m[2].trim() };
|
|
977
1051
|
}
|
|
1052
|
+
/** Details ("ages:P0001" among them) as an edit of the fact; an empty one takes it away. */
|
|
1053
|
+
function detailEdit(d) {
|
|
1054
|
+
const out = {};
|
|
1055
|
+
for (const [k, v] of Object.entries(d)) {
|
|
1056
|
+
if (k.startsWith("ages:"))
|
|
1057
|
+
out.ages = { ...out.ages, [k.slice(5)]: v };
|
|
1058
|
+
else if (k === "cause" || k === "age" || k === "house")
|
|
1059
|
+
out[k] = v;
|
|
1060
|
+
}
|
|
1061
|
+
return out;
|
|
1062
|
+
}
|
|
978
1063
|
function describe(f) {
|
|
979
|
-
|
|
1064
|
+
const ages = Object.entries(f.ages ?? {}).map(([who, a]) => `${who} aged ${a}`);
|
|
1065
|
+
return [f.value, f.date, f.place, f.house && `house ${f.house}`, f.age && `aged ${f.age}`, ...ages, f.cause && `cause ${f.cause}`].filter(Boolean).join(", ") || "—";
|
|
980
1066
|
}
|
|
981
1067
|
/** Undo one sync: what it added withdrawn, what it corrected put back, its conflicts closed — each with the reason. */
|
|
982
1068
|
export function undoSync(tree, input) {
|
|
@@ -998,9 +1084,10 @@ export function undoSync(tree, input) {
|
|
|
998
1084
|
// what it was put back: a detail it had, and none where it had none
|
|
999
1085
|
const e = eventById(tree, a.id);
|
|
1000
1086
|
const before = JSON.parse(String(a.before ?? "{}"));
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1087
|
+
const now = e ? detailsOf(e) : {};
|
|
1088
|
+
const back = Object.fromEntries(Object.entries(before).filter(([d, v]) => v !== (now[d] ?? "")));
|
|
1089
|
+
if (e && Object.keys(back).length)
|
|
1090
|
+
editEvent(tree, a.id, detailEdit(back), reason);
|
|
1004
1091
|
break;
|
|
1005
1092
|
}
|
|
1006
1093
|
case "event.retract":
|
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.
|
|
26
|
+
export const VERSION = "1.11.0";
|
|
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
|
@@ -360,8 +360,8 @@ export function exportGedcom(tree, opts = {}) {
|
|
|
360
360
|
const allowed = on === "INDI" ? INDI_TAGS : FAM_TAGS;
|
|
361
361
|
let tag = e.kind;
|
|
362
362
|
let typeLabel = e.label;
|
|
363
|
-
if (on === "FAM" && tag === "RESI" && strict) {
|
|
364
|
-
// standard on FAM; Strom
|
|
363
|
+
if (on === "FAM" && tag === "RESI" && (strict || opts.coupleResi)) {
|
|
364
|
+
// standard on FAM; for a Strom app before its couple's events an EVEN named so
|
|
365
365
|
}
|
|
366
366
|
else if (AS_EVEN[tag] || !allowed.has(tag)) {
|
|
367
367
|
typeLabel = e.label ?? (AS_EVEN[tag] ? L(AS_EVEN[tag]) : L(tag) ?? tag);
|
package/dist/gedcom/validate.js
CHANGED
|
@@ -11,7 +11,7 @@ const INDI_EVENTS = [
|
|
|
11
11
|
"NATU", "RELI", "TITL", "NATI", "ADOP", "WILL", "PROB", "BURI", "CREM", "CENS", "EVEN", "RETI", "BLES", "CAST", "DSCR", "IDNO",
|
|
12
12
|
"NCHI", "NMR", "PROP", "SSN", "FACT",
|
|
13
13
|
];
|
|
14
|
-
const FAM_EVENTS = ["MARR", "DIV", "MARB", "MARC", "MARL", "MARS", "ANUL", "DIVF", "ENGA", "CENS", "EVEN", "NCHI"];
|
|
14
|
+
const FAM_EVENTS = ["MARR", "DIV", "MARB", "MARC", "MARL", "MARS", "ANUL", "DIVF", "ENGA", "CENS", "RESI", "EVEN", "NCHI"];
|
|
15
15
|
/** Allowed child tags by parent context ("INDI", "INDI.BIRT", …). CONC/CONT are allowed under anything with text. */
|
|
16
16
|
const CHILDREN = {
|
|
17
17
|
HEAD: ["SOUR", "DEST", "DATE", "SUBM", "SUBN", "FILE", "COPR", "GEDC", "CHAR", "LANG", "PLAC", "NOTE", "_STROM_TREE", "_STROM_HEAD", "_STROM_LINKS", "_STROM_ASOF"],
|