strom-research 1.22.2 → 1.23.0-beta.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.
- package/CHANGELOG.cs.md +18 -0
- package/CHANGELOG.de.md +22 -0
- package/CHANGELOG.md +18 -0
- package/assets/lang/cs.json +13 -0
- package/assets/lang/de.json +13 -0
- package/dist/brief/brief.js +1 -1
- package/dist/cli/seal.js +4 -2
- package/dist/cli/ui.js +11 -0
- package/dist/commands/checks.js +34 -11
- package/dist/commands/people.js +35 -6
- package/dist/commands/sync.js +28 -26
- package/dist/core/actions.js +61 -10
- package/dist/core/changelog.js +5 -0
- package/dist/core/import.js +10 -4
- package/dist/core/integrity.js +30 -5
- package/dist/core/live.js +1 -1
- package/dist/core/overview.js +3 -2
- package/dist/core/people.js +14 -1
- package/dist/core/phrases.js +2 -0
- package/dist/core/retractions.js +6 -4
- package/dist/core/stromapp.js +21 -0
- package/dist/core/sync.js +110 -8
- package/dist/core/tree.js +1 -1
- package/dist/core/validate.js +4 -0
- package/dist/gedcom/export.js +14 -2
- package/dist/gedcom/validate.js +13 -5
- package/package.json +4 -1
package/dist/core/actions.js
CHANGED
|
@@ -5,7 +5,7 @@ import { normalizeDate, reversedRange } from "./gdate.js";
|
|
|
5
5
|
import { normalizeAge } from "./age.js";
|
|
6
6
|
import { checkStatus, defaultStatus } from "./evidence.js";
|
|
7
7
|
import { eventKind, FAMILY_EVENT_KINDS, NOTE_MAX, STATUSES, PARTICIPANT_ROLES, NAME_KINDS, RECORD_TYPES, } from "./model.js";
|
|
8
|
-
import { cleanTitle, displayName, familiesAsChild, familiesAsPartner, gedcomName, gedcomTitledName, closesCircle, isAncestorOf, isBirthFamily, keepSides, notAName, parseName, primaryName, sameName, slashInName } from "./people.js";
|
|
8
|
+
import { cleanTitle, displayName, familiesAsChild, familiesAsPartner, gedcomName, gedcomTitledName, closesCircle, isAncestorOf, isBirthFamily, keepSides, notAName, parseName, primaryName, sameName, slashInName, withoutMark } from "./people.js";
|
|
9
9
|
import { foldText } from "./text.js";
|
|
10
10
|
import { roleWord } from "./roles.js";
|
|
11
11
|
import { checkParticipant, trailingNote } from "./participants.js";
|
|
@@ -651,8 +651,8 @@ export function addName(tree, id, input) {
|
|
|
651
651
|
throw new UsageError(`invalid --kind "${input.kind}"`, { hint: NAME_KINDS.join(", ") });
|
|
652
652
|
name.kind = input.kind;
|
|
653
653
|
}
|
|
654
|
-
if (input.
|
|
655
|
-
throw new UsageError(`a person is
|
|
654
|
+
if (input.first && name.kind && name.kind !== "birth")
|
|
655
|
+
throw new UsageError(`a person's identity is a birth name, not a ${name.kind} one`);
|
|
656
656
|
if (input.citation)
|
|
657
657
|
checkCitation(tree, input.citation);
|
|
658
658
|
return tree.withTreeLock(() => {
|
|
@@ -667,11 +667,12 @@ export function addName(tree, id, input) {
|
|
|
667
667
|
const rekind = name.kind !== undefined && name.kind !== cur.kind;
|
|
668
668
|
const retitled = withTitles(cur, input.prefix, input.suffix);
|
|
669
669
|
const titles = retitled.prefix !== cur.prefix || retitled.suffix !== cur.suffix;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
670
|
+
const marks = !!input.primary && !cur.primary;
|
|
671
|
+
if (!input.citation && !marks && !input.first && !rekind && !titles)
|
|
672
|
+
throw new UsageError(input.primary ? `${id} is shown by ${gedcomName(cur)} already` : `${id} already has the name ${gedcomName(name)}`, { hint: `cite it: strom name add ${id} "${gedcomName(name)}" --cite S…` });
|
|
673
|
+
added = { ...retitled, ...(rekind ? { kind: name.kind } : {}), ...(input.primary ? { primary: true } : {}), ...(input.citation ? { citations: withCitation(cur.citations, input.citation, `${id} ${gedcomName(name)}`) } : {}) };
|
|
673
674
|
names = p.names.map((n, j) => (j === i ? added : n));
|
|
674
|
-
if (input.
|
|
675
|
+
if (input.first)
|
|
675
676
|
names = [added, ...names.filter((n) => n !== added)];
|
|
676
677
|
}
|
|
677
678
|
else {
|
|
@@ -681,19 +682,62 @@ export function addName(tree, id, input) {
|
|
|
681
682
|
const plain = !name.kind || name.kind === "birth";
|
|
682
683
|
// "Markéta" of the family memory is "Markéta /Růžičková/" of the register: one name, now complete.
|
|
683
684
|
completes = Boolean(!input.other && plain && !primary.surname && name.surname && foldText(primary.given) === foldText(name.given) && !primary.citations?.length);
|
|
685
|
+
// the name completed was the one the person is shown by: the full one is
|
|
686
|
+
if (input.primary || (completes && primary.primary))
|
|
687
|
+
added = { ...added, primary: true };
|
|
684
688
|
const rest = completes ? p.names.filter((n) => n !== primary) : p.names;
|
|
685
|
-
names = !input.other && (input.
|
|
689
|
+
names = !input.other && (input.first || (plain && !primary.surname && name.surname)) ? [added, ...rest] : [...rest, added];
|
|
686
690
|
}
|
|
691
|
+
// one name the person is shown by: the one marked now
|
|
692
|
+
if (added.primary)
|
|
693
|
+
names = names.map((n) => (n === added || !n.primary ? n : withoutMark(n)));
|
|
687
694
|
const updated = { ...p, names, updated: now() };
|
|
688
695
|
tree.put(updated, {
|
|
689
696
|
op: "person.name",
|
|
690
697
|
targets: [id],
|
|
691
|
-
summary: `${id} name ${gedcomTitledName(added)}${added.kind ? ` (${added.kind})` : ""}${input.citation ? ` ← ${input.citation.source}` : ""}`,
|
|
698
|
+
summary: `${id} name ${gedcomTitledName(added)}${added.kind ? ` (${added.kind})` : ""}${input.citation ? ` ← ${input.citation.source}` : ""}${input.primary ? "; shown by it" : ""}`,
|
|
692
699
|
...(completes ? { reason: `the name without a surname is completed: ${gedcomName(added)}` } : {}),
|
|
693
700
|
});
|
|
694
701
|
return { person: updated, name: added };
|
|
695
702
|
});
|
|
696
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Show a person by one of their names (the mark Name.primary: a divorced woman back at her birth surname, spec 024),
|
|
706
|
+
* without adding it again — or, with no name, by the name of their identity again (the mark taken off). The user's
|
|
707
|
+
* choice: never told from a record, never needs one.
|
|
708
|
+
*/
|
|
709
|
+
export function markPrimaryName(tree, id, text, reason) {
|
|
710
|
+
const wanted = text === undefined ? undefined : parseName(text);
|
|
711
|
+
if (wanted && !wanted.given && !wanted.surname)
|
|
712
|
+
throw new UsageError("the name is empty");
|
|
713
|
+
return tree.withTreeLock(() => {
|
|
714
|
+
const p = requirePerson(tree, id);
|
|
715
|
+
const was = p.names.find((n) => n.primary);
|
|
716
|
+
let name;
|
|
717
|
+
if (wanted) {
|
|
718
|
+
const found = p.names.find((n) => sameName(n, wanted));
|
|
719
|
+
if (!found) {
|
|
720
|
+
const gone = p.retractedNames?.find((n) => sameName(n, wanted));
|
|
721
|
+
if (gone)
|
|
722
|
+
throw new UsageError(`the name ${gedcomName(gone)} of ${id} was retracted: ${gone.retracted.reason}`, { hint: `right after all: strom name restore ${id} "${gedcomName(gone)}" --reason "…"`, code: "name.retracted", params: { name: gedcomName(gone), id, reason: gone.retracted.reason } });
|
|
723
|
+
throw new UsageError(`${id} has no name ${gedcomName(wanted)}`, { hint: `its names: ${p.names.map((n) => `"${gedcomName(n)}"`).join(", ")} · a new one: strom name add ${id} "${gedcomName(wanted)}" --primary`, code: "name.none" });
|
|
724
|
+
}
|
|
725
|
+
if (found.primary)
|
|
726
|
+
throw new UsageError(`${id} is shown by ${gedcomName(found)} already`, { hint: `back to the name of their identity: strom name primary ${id} --clear` });
|
|
727
|
+
name = { ...found, primary: true };
|
|
728
|
+
const names = p.names.map((n) => (n === found ? name : n.primary ? withoutMark(n) : n));
|
|
729
|
+
const updated = { ...p, names, updated: now() };
|
|
730
|
+
tree.put(updated, { op: "name.primary", targets: [id], summary: `${id} shown by ${gedcomName(name)}${was ? ` (was ${gedcomName(was)})` : ""}`, ...(reason?.trim() ? { reason: reason.trim() } : {}) });
|
|
731
|
+
return { person: updated, name, ...(was ? { was } : {}) };
|
|
732
|
+
}
|
|
733
|
+
if (!was)
|
|
734
|
+
throw new UsageError(`no name of ${id} is marked: shown by ${gedcomName(primaryName(p))}`, { hint: `strom name primary ${id} "<Given /Surname/>"` });
|
|
735
|
+
const updated = { ...p, names: p.names.map((n) => (n === was ? withoutMark(n) : n)), updated: now() };
|
|
736
|
+
name = primaryName(updated);
|
|
737
|
+
tree.put(updated, { op: "name.primary", targets: [id], summary: `${id} shown by ${gedcomName(name)} again (the mark of ${gedcomName(was)} taken off)`, ...(reason?.trim() ? { reason: reason.trim() } : {}) });
|
|
738
|
+
return { person: updated, name, was };
|
|
739
|
+
});
|
|
740
|
+
}
|
|
697
741
|
/** Change a person's sex or correct the spelling of their name — with a reason, unless an unknown sex is filled in. */
|
|
698
742
|
export function editPerson(tree, id, edit, reason) {
|
|
699
743
|
const sex = edit.sex === undefined ? undefined : parseSex(edit.sex);
|
|
@@ -925,13 +969,20 @@ export function mergePersons(tree, keepId, otherId, reason) {
|
|
|
925
969
|
const names = keep.names.map((n) => {
|
|
926
970
|
const same = other.names.find((o) => sameName(o, n));
|
|
927
971
|
const cites = [...(n.citations ?? []), ...(same?.citations ?? []).filter((c) => !(n.citations ?? []).some((x) => x.source === c.source && x.locator === c.locator))];
|
|
928
|
-
|
|
972
|
+
// the other shown by this same name, the kept one by none marked: shown by it
|
|
973
|
+
const mark = same?.primary && !keep.names.some((k) => k.primary) ? { primary: true } : {};
|
|
974
|
+
return cites.length ? { ...n, ...mark, citations: cites } : { ...n, ...mark };
|
|
929
975
|
});
|
|
930
976
|
// a name the person's research took back stays taken back (the other's retracted come along, for the history)
|
|
931
977
|
const retractedNames = [...(keep.retractedNames ?? []), ...(other.retractedNames ?? [])];
|
|
932
978
|
for (const o of other.names)
|
|
933
979
|
if (!names.some((n) => sameName(n, o)) && !retractedNames.some((n) => sameName(n, o)))
|
|
934
980
|
names.push(o);
|
|
981
|
+
// the name they are shown by: the kept person's, else the other's — one
|
|
982
|
+
const marked = names.find((n) => n.primary);
|
|
983
|
+
for (let i = 0; i < names.length; i++)
|
|
984
|
+
if (names[i].primary && names[i] !== marked)
|
|
985
|
+
names[i] = withoutMark(names[i]);
|
|
935
986
|
const retractedNotes = [...(keep.retractedNotes ?? []), ...(other.retractedNotes ?? [])];
|
|
936
987
|
const refs = [...(keep.refs ?? []), ...(other.refs ?? []).filter((r) => !(keep.refs ?? []).some((k) => k.system === r.system && k.id === r.id))];
|
|
937
988
|
const why = reason.trim();
|
package/dist/core/changelog.js
CHANGED
|
@@ -365,6 +365,11 @@ export function changeLines(tree, ops, subject, lang, was, tasks) {
|
|
|
365
365
|
say(m ? phrase(lang, o.op === "name.retract" ? "log.name.retract" : "log.name.restore", { name: m[1].replace(/\//g, " ").replace(/\s+/g, " ").trim(), who: who(first) }) : changed(o));
|
|
366
366
|
continue;
|
|
367
367
|
}
|
|
368
|
+
if (o.op === "name.primary") {
|
|
369
|
+
const m = /^\S+ shown by (.+?)( again \(|(?: \(was |$))/.exec(o.summary);
|
|
370
|
+
say(m ? phrase(lang, m[2] === " again (" ? "log.name.primary.clear" : "log.name.primary", { name: m[1].replace(/\//g, " ").replace(/\s+/g, " ").trim(), who: who(first) }) : changed(o));
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
368
373
|
if (o.op === "note.remove") {
|
|
369
374
|
const m = /^\S+ note "(.*)" removed:/.exec(o.summary);
|
|
370
375
|
say(m ? phrase(lang, "log.note.remove", { note: m[1], name: name(first) }) : changed(o));
|
package/dist/core/import.js
CHANGED
|
@@ -12,7 +12,7 @@ import { roleWord } from "./roles.js";
|
|
|
12
12
|
import { validateRecord } from "./validate.js";
|
|
13
13
|
import { normalizeDate } from "./gdate.js";
|
|
14
14
|
import { isRetractedName } from "./retractions.js";
|
|
15
|
-
import { appNoSurname, cleanTitle, gedcomName, noName, notAName, parseName, sameName, surnameAfterGiven, withoutTitles } from "./people.js";
|
|
15
|
+
import { appNoSurname, cleanTitle, gedcomName, noName, notAName, parseName, sameName, surnameAfterGiven, withoutMark, withoutTitles } from "./people.js";
|
|
16
16
|
import { update } from "./records.js";
|
|
17
17
|
import { now } from "./tree.js";
|
|
18
18
|
import { appUnknownIsNoSurname, appWritesSexUnknown } from "./stromapp.js";
|
|
@@ -373,9 +373,14 @@ export function importGedcom(tree, text, opts) {
|
|
|
373
373
|
if (notAName(found.given))
|
|
374
374
|
found.given = "?";
|
|
375
375
|
// a slash inside a name ("/⟨K/Č⟩emenská/") is kept as "|": the name stays one GEDCOM can write
|
|
376
|
-
|
|
376
|
+
const name = { ...found, given: found.given.replace(/\//g, "|"), surname: found.surname.replace(/\//g, "|") };
|
|
377
|
+
// the name the person is shown by now (the Strom file's mark, the user's choice): the first one marked
|
|
378
|
+
if (/^y(es)?$/i.test(val(n, "_STROM_PRIMARY")?.trim() ?? ""))
|
|
379
|
+
name.primary = true;
|
|
380
|
+
return name;
|
|
377
381
|
})
|
|
378
|
-
.map((n) => (n.given || n.surname ? n : { ...n, given: "?", surname: "" }))
|
|
382
|
+
.map((n) => (n.given || n.surname ? n : { ...n, given: "?", surname: "" }))
|
|
383
|
+
.map((n, i, all) => (n.primary && all.findIndex((m) => m.primary) !== i ? withoutMark(n) : n));
|
|
379
384
|
// a married name other programs give apart (_MARNM): a name of the married kind, unless the person has it already
|
|
380
385
|
return [...names, ...importedMarnm(r, fromApp).filter((m) => !names.some((n) => sameName(n, m)))];
|
|
381
386
|
};
|
|
@@ -407,7 +412,8 @@ export function importGedcom(tree, text, opts) {
|
|
|
407
412
|
const facts = incoming.filter((e) => !knownFact(cur.events, e));
|
|
408
413
|
result.events -= incoming.length - facts.length;
|
|
409
414
|
// a name the research took back is not taken again from a file that still has it
|
|
410
|
-
|
|
415
|
+
// the names another file adds: never the one the person is shown by (theirs stands)
|
|
416
|
+
const names = namesOf(r).filter((n) => n.given !== "?" && !cur.names.some((x) => sameName(x, n)) && !isRetractedName(cur, n)).map(withoutMark);
|
|
411
417
|
if (!facts.length && !names.length)
|
|
412
418
|
continue;
|
|
413
419
|
const next = { ...cur, names: [...cur.names, ...names], events: [...cur.events, ...facts], refs: [...(cur.refs ?? []), { system, id: r.xref }], updated: now() };
|
package/dist/core/integrity.js
CHANGED
|
@@ -64,6 +64,26 @@ function parseOps(text, file, findings, startLine = 0) {
|
|
|
64
64
|
*/
|
|
65
65
|
export const OTHER_KEY = "other-key";
|
|
66
66
|
const OTHER_KEY_HINT = "the user runs in their terminal: strom seal adopt (unites the keys — never strom repair, which would undo that work)";
|
|
67
|
+
/**
|
|
68
|
+
* Two keys of this computer and changes outside strom at once (a commit made with git on top of the other key's work):
|
|
69
|
+
* one way, never two that contradict each other — strom repair first (it undoes only what was done outside strom, the
|
|
70
|
+
* work of both keys stays: it restores to the newest commit any key of this computer sealed), then strom seal adopt.
|
|
71
|
+
* Never the other way round: uniting commits what is there, a record changed outside strom with it. `hidden`: started
|
|
72
|
+
* by an app packaged for Windows, the terminal's key out of its sight — strom repair there too, in the terminal.
|
|
73
|
+
*/
|
|
74
|
+
export function oneWay(findings, hidden) {
|
|
75
|
+
const repairs = findings.filter((f) => f.level === "error" && f.code !== OTHER_KEY && f.hint?.startsWith("strom repair"));
|
|
76
|
+
if (!repairs.length)
|
|
77
|
+
return findings;
|
|
78
|
+
const where = hidden() ? "the user runs in their terminal: " : "";
|
|
79
|
+
for (const f of repairs)
|
|
80
|
+
f.hint = `${where}strom repair (undoes only what was done outside strom — the work of both keys of this computer stays), then strom seal adopt`;
|
|
81
|
+
for (const f of findings)
|
|
82
|
+
if (f.code === OTHER_KEY)
|
|
83
|
+
f.hint = AFTER_REPAIR_HINT;
|
|
84
|
+
return findings;
|
|
85
|
+
}
|
|
86
|
+
export const AFTER_REPAIR_HINT = "after strom repair, the user runs in their terminal: strom seal adopt (unites the keys)";
|
|
67
87
|
function keysOf(tree, trust = [], united = new Map()) {
|
|
68
88
|
let others;
|
|
69
89
|
let hidden;
|
|
@@ -279,10 +299,11 @@ export function verifyFast(tree, snap = snapshot(tree)) {
|
|
|
279
299
|
const tampered = new Set();
|
|
280
300
|
if (!git.hasHead(tree.root))
|
|
281
301
|
return { findings, tampered: [] };
|
|
282
|
-
|
|
302
|
+
const keys = keysOf(tree);
|
|
303
|
+
headSeal(tree, findings, keys);
|
|
283
304
|
const { changes, head: committed } = snap;
|
|
284
305
|
if (changes.length === 0)
|
|
285
|
-
return { findings, tampered: [] };
|
|
306
|
+
return { findings: united(findings, keys), tampered: [] };
|
|
286
307
|
const opsChanged = changes.filter((c) => c.path.startsWith("data/ops/") && c.path.endsWith(".jsonl"));
|
|
287
308
|
const latest = new Map();
|
|
288
309
|
const cloud = new Set();
|
|
@@ -304,7 +325,7 @@ export function verifyFast(tree, snap = snapshot(tree)) {
|
|
|
304
325
|
}
|
|
305
326
|
const prior = parseOps(before, c.path, findings);
|
|
306
327
|
const added = parseOps(now.slice(before.length), c.path, findings, prior.length);
|
|
307
|
-
checkOps(tree, added, prior.at(-1)?.sig ?? "", c.path, findings);
|
|
328
|
+
checkOps(tree, added, prior.at(-1)?.sig ?? "", c.path, findings, keys);
|
|
308
329
|
for (const op of added)
|
|
309
330
|
for (const f of op.files ?? [])
|
|
310
331
|
latest.set(f.path, f.sha);
|
|
@@ -346,7 +367,11 @@ export function verifyFast(tree, snap = snapshot(tree)) {
|
|
|
346
367
|
}
|
|
347
368
|
if (cloud.size)
|
|
348
369
|
findings.unshift(notDownloadedFinding([...cloud].sort()));
|
|
349
|
-
return { findings, tampered: [...tampered] };
|
|
370
|
+
return { findings: united(findings, keys), tampered: [...tampered] };
|
|
371
|
+
}
|
|
372
|
+
/** Another key of this computer among the findings: one way with what strom repair undoes (oneWay). */
|
|
373
|
+
function united(findings, keys) {
|
|
374
|
+
return findings.some((f) => f.code === OTHER_KEY) ? oneWay(findings, keys.hidden) : findings;
|
|
350
375
|
}
|
|
351
376
|
/**
|
|
352
377
|
* The other keys of this computer a seal.unite of the tree's own key took, each with when (its operations until then
|
|
@@ -442,7 +467,7 @@ export function verifyFull(tree, o = {}) {
|
|
|
442
467
|
tampered.add("strom.json");
|
|
443
468
|
findings.push({ level: "error", code: "config-edited", file: "strom.json", message: "strom.json was changed outside strom", hint: "strom repair" });
|
|
444
469
|
}
|
|
445
|
-
return { findings, tampered: [...tampered] };
|
|
470
|
+
return { findings: united(findings, keys), tampered: [...tampered] };
|
|
446
471
|
}
|
|
447
472
|
/**
|
|
448
473
|
* Whether strom may write here unasked (a settle: nobody asked, nobody to ask): the tree sealed on this computer (not
|
package/dist/core/live.js
CHANGED
|
@@ -918,7 +918,7 @@ function idsOf(applied, known) {
|
|
|
918
918
|
return Object.keys(ids.persons).length || Object.keys(ids.sources).length ? { ids } : {};
|
|
919
919
|
}
|
|
920
920
|
/** What the bridge does that an app may ask about (each added once, never taken away). */
|
|
921
|
-
export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname", "phone", "read.now", "read.sync", "excerpt.get", "log.sources", "material.assign", "material.thumb", "seal.foreign", "tree.state", "disk.full", "tree.notDownloaded", "sync.removed", "tree.kind", "experiments", "experiments.off", "experiments.link", "experiments.split", "history.events", "history.hide", "experiments.pace", "history.first", "history.regions", "history.hide.all", "history.skip", "history.map", "history.delete", "history.delete.restore", "stories.history", "history.place", "surnames.suggest", "surnames.read", "duplicates.judge", "quality.dismiss", "quality.read"];
|
|
921
|
+
export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname", "phone", "read.now", "read.sync", "excerpt.get", "log.sources", "material.assign", "material.thumb", "seal.foreign", "tree.state", "disk.full", "tree.notDownloaded", "sync.removed", "tree.kind", "experiments", "experiments.off", "experiments.link", "experiments.split", "history.events", "history.hide", "experiments.pace", "history.first", "history.regions", "history.hide.all", "history.skip", "history.map", "history.delete", "history.delete.restore", "stories.history", "history.place", "surnames.suggest", "surnames.read", "duplicates.judge", "quality.dismiss", "quality.read", "person.primary"];
|
|
922
922
|
/** /status recent, when it is ready. */
|
|
923
923
|
function recentOf(root, env, head) {
|
|
924
924
|
const recent = head ? recentNow(root, env, head) : undefined;
|
package/dist/core/overview.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ancestors of one person (pedigree), one person's life (card), and what came
|
|
3
3
|
// in lately (recent). Read only, from the records and the operation logs; the
|
|
4
4
|
// commands in commands/browse.ts show them in the research language.
|
|
5
|
-
import { ancestorGenerations, birthEvent, displayName, familiesAsPartner, formatName, lifespan, parentsOf,
|
|
5
|
+
import { ancestorGenerations, birthEvent, currentName, displayName, familiesAsPartner, formatName, lifespan, parentsOf, sameName, titledName } from "./people.js";
|
|
6
6
|
import { dateYears } from "./gdate.js";
|
|
7
7
|
import { informationOf } from "./evidence.js";
|
|
8
8
|
import { imagesIndex } from "./mediaindex.js";
|
|
@@ -159,7 +159,8 @@ export function cardFact(tree, e) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
export function personCard(tree, p) {
|
|
162
|
-
|
|
162
|
+
// the name they are shown by (the one the user chose, else their birth name): the others beside it
|
|
163
|
+
const main = currentName(p);
|
|
163
164
|
const families = familiesAsPartner(tree, p.id).map((f) => {
|
|
164
165
|
const other = f.partners.filter((x) => x !== p.id).map((x) => tree.get(x)).find((x) => x && !x.retracted);
|
|
165
166
|
return {
|
package/dist/core/people.js
CHANGED
|
@@ -179,8 +179,21 @@ export function slashInName(n) {
|
|
|
179
179
|
export function primaryName(p) {
|
|
180
180
|
return p.names.find((n) => !n.kind || n.kind === "birth") ?? p.names[0] ?? { given: "", surname: "" };
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* The name the person is shown by now: the one marked (Name.primary, the user's choice — a divorced woman back at her
|
|
184
|
+
* birth surname), else the name of their identity (primaryName). Only for showing them: the identity a correction, a
|
|
185
|
+
* citation, pairing and a sync go by stays primaryName.
|
|
186
|
+
*/
|
|
187
|
+
export function currentName(p) {
|
|
188
|
+
return p.names.find((n) => n.primary) ?? primaryName(p);
|
|
189
|
+
}
|
|
190
|
+
/** A name without the mark of the one the person is shown by (Name.primary). */
|
|
191
|
+
export function withoutMark(n) {
|
|
192
|
+
const { primary: _p, ...rest } = n;
|
|
193
|
+
return rest;
|
|
194
|
+
}
|
|
182
195
|
export function displayName(p) {
|
|
183
|
-
return formatName(
|
|
196
|
+
return formatName(currentName(p));
|
|
184
197
|
}
|
|
185
198
|
const STATUS_RANK = { proven: 0, probable: 1, possible: 2, lead: 3 };
|
|
186
199
|
/** Best-supported first: status, then the number of citations, then the order they were added. */
|
package/dist/core/phrases.js
CHANGED
|
@@ -271,6 +271,8 @@ export const PHRASES = {
|
|
|
271
271
|
"log.note.edit": "Note corrected: {name}",
|
|
272
272
|
"log.name.retract": "Name retracted: {name} – {who}",
|
|
273
273
|
"log.name.restore": "Name put back: {name} – {who}",
|
|
274
|
+
"log.name.primary": "Shown by the name: {name} – {who}",
|
|
275
|
+
"log.name.primary.clear": "Shown by the birth name again: {name} – {who}",
|
|
274
276
|
"log.note.retract": "Note retracted: {name}",
|
|
275
277
|
"log.note.restore": "Note put back: {name}",
|
|
276
278
|
"log.event.status": "How sure a fact is, changed: {who} – {fact}",
|
package/dist/core/retractions.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { UsageError } from "./errors.js";
|
|
6
6
|
import { findEventOwner, makeNote } from "./actions.js";
|
|
7
7
|
import { EMBEDDED_PREFIXES } from "./model.js";
|
|
8
|
-
import { gedcomName, parseName, primaryName, sameName } from "./people.js";
|
|
8
|
+
import { currentName, gedcomName, parseName, primaryName, sameName, withoutMark } from "./people.js";
|
|
9
9
|
import { now, typeOfId } from "./tree.js";
|
|
10
10
|
function needsReason(reason, what) {
|
|
11
11
|
const why = reason?.trim();
|
|
@@ -48,8 +48,8 @@ export function retractName(tree, id, text, reason) {
|
|
|
48
48
|
});
|
|
49
49
|
const gone = { ...name, retracted: retraction(tree, why) };
|
|
50
50
|
const updated = { ...p, names: p.names.filter((n) => n !== name), retractedNames: [...(p.retractedNames ?? []), gone], updated: now() };
|
|
51
|
-
const shown =
|
|
52
|
-
const moved = name ===
|
|
51
|
+
const shown = currentName(updated);
|
|
52
|
+
const moved = name === currentName(p);
|
|
53
53
|
tree.put(updated, {
|
|
54
54
|
op: "name.retract",
|
|
55
55
|
targets: [id],
|
|
@@ -69,7 +69,9 @@ export function restoreName(tree, id, text, reason) {
|
|
|
69
69
|
throw new UsageError(`${id} has no retracted name ${gedcomName(wanted)}`, { hint: `its retracted names: ${nameList(p.retractedNames ?? [])}`, code: "name.none" });
|
|
70
70
|
if (p.names.some((n) => sameName(n, gone)))
|
|
71
71
|
throw new UsageError(`${id} has the name ${gedcomName(gone)} again`, { hint: `nothing to restore: strom person show ${id}` });
|
|
72
|
-
|
|
72
|
+
// the name they were shown by, back as that one unless another is marked since
|
|
73
|
+
const { retracted: _was, ...back } = gone;
|
|
74
|
+
const name = back.primary && p.names.some((n) => n.primary) ? withoutMark(back) : back;
|
|
73
75
|
const rest = (p.retractedNames ?? []).filter((n) => n !== gone);
|
|
74
76
|
const { retractedNames: _all, ...base } = p;
|
|
75
77
|
const updated = { ...base, names: [...p.names, name], ...(rest.length ? { retractedNames: rest } : {}), updated: now() };
|
package/dist/core/stromapp.js
CHANGED
|
@@ -537,6 +537,27 @@ export function appReadsSurnameGroups(settings, version, gate = APP_READS_SURNAM
|
|
|
537
537
|
return compareVersions(v, APP_READS_SURNAME_MARKERS) >= 0;
|
|
538
538
|
return !!gate && compareVersions(v, gate) >= 0;
|
|
539
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* The first Strom app that knows which name a person is shown by (spec 024, the app's ZADANI_VYZKUM_soucasne-prijmeni.md):
|
|
542
|
+
* it writes 2 _STROM_PRIMARY Y under the NAME the user chose (its Person.primaryName birth | married, the JSON's field
|
|
543
|
+
* of that name) and none where the user chose none. The research writes the mark to every app (an older one skips it in
|
|
544
|
+
* silence and loses nothing); a mark an app sends is the user's choice whatever its version — but a mark missing is
|
|
545
|
+
* the user's "none" only from an app of this version on: from an older one, or one that says no version, nothing
|
|
546
|
+
* changed. Not released yet (undefined): set to the version the app's "je venku" names; another copy of the app (its
|
|
547
|
+
* beta, its development) gets it at once from APP_PRIMARY_NAME_BETA once that is known too.
|
|
548
|
+
*/
|
|
549
|
+
export const APP_KNOWS_PRIMARY_NAME = undefined;
|
|
550
|
+
/** The first beta of the app that writes the mark (another copy of the app gets it from it); undefined: none yet. */
|
|
551
|
+
export const APP_PRIMARY_NAME_BETA = undefined;
|
|
552
|
+
/** Does a file of the Strom app of this version say every choice of the name a person is shown by — a mark missing the user's "none"? */
|
|
553
|
+
export function appKnowsPrimaryName(settings, version) {
|
|
554
|
+
const v = version?.trim();
|
|
555
|
+
if (!v || !isAppVersion(v))
|
|
556
|
+
return false;
|
|
557
|
+
if (anotherCopy(settings) && APP_PRIMARY_NAME_BETA)
|
|
558
|
+
return compareVersions(v, APP_PRIMARY_NAME_BETA) >= 0;
|
|
559
|
+
return !!APP_KNOWS_PRIMARY_NAME && compareVersions(v, APP_KNOWS_PRIMARY_NAME) >= 0;
|
|
560
|
+
}
|
|
540
561
|
/** The address that opens the Strom app to send the user's edited tree to the bridge. */
|
|
541
562
|
export function sendAppUrl(bridge, settings) {
|
|
542
563
|
return `${stromAppUrl(settings)}?send=${encodeURIComponent(bridge)}`;
|
package/dist/core/sync.js
CHANGED
|
@@ -24,7 +24,7 @@ import path from "node:path";
|
|
|
24
24
|
import { CHILD_RELATIONS, UNIONS, eventKind } from "./model.js";
|
|
25
25
|
import { isWeak } from "./evidence.js";
|
|
26
26
|
import { phrase } from "./phrases.js";
|
|
27
|
-
import { addChild, addEvent, addFamily, addName, addNote, editEvent, editPerson, retractEvent, retractPerson } from "./actions.js";
|
|
27
|
+
import { addChild, addEvent, addFamily, addName, addNote, editEvent, editPerson, markPrimaryName, retractEvent, retractPerson } from "./actions.js";
|
|
28
28
|
import { exportGedcom, REFN_TYPE } from "../gedcom/export.js";
|
|
29
29
|
import { children, parseGedcomText, val } from "../gedcom/parse.js";
|
|
30
30
|
import { fromFlexDate, houseOf, importDate, marnmNames, readGedName, readGedSex, stromAppFile } from "./import.js";
|
|
@@ -42,14 +42,14 @@ import * as git from "./git.js";
|
|
|
42
42
|
import { now, Tree, typeOfId } from "./tree.js";
|
|
43
43
|
import { actsAsArchive } from "./state.js";
|
|
44
44
|
import { appSides } from "./conflicts.js";
|
|
45
|
-
import { APP_DATA_SEX_UNKNOWN, appKeepsPeriods, appReadsSurnameGroups, isAppVersion } from "./stromapp.js";
|
|
45
|
+
import { APP_DATA_SEX_UNKNOWN, appKeepsPeriods, appKnowsPrimaryName, appReadsSurnameGroups, isAppVersion } from "./stromapp.js";
|
|
46
46
|
import { Settings } from "./config.js";
|
|
47
47
|
import { applySurnameChange, formsSaid, planSurnames, readMarkLines, readSurnames, surnamesAt, undoSurnameChange } from "./surnames.js";
|
|
48
48
|
import { SURNAME_GROUPS_MARK, SURNAME_REJECTIONS_MARK } from "../gedcom/export.js";
|
|
49
49
|
import { bestGrade, checkReading, readingGrade, readingResult, readingSource, surer, takeReadingImages } from "./readsync.js";
|
|
50
50
|
import { earlierNoteTexts } from "./retractions.js";
|
|
51
51
|
import { mendAll } from "./damaged.js";
|
|
52
|
-
import { cleanTitle, familiesAsPartner, formatName, foundTitles, gedcomTitledName, parseName, primaryName, sameName as sameWords, titledName } from "./people.js";
|
|
52
|
+
import { cleanTitle, currentName, familiesAsPartner, formatName, foundTitles, gedcomTitledName, parseName, primaryName, sameName as sameWords, titledName } from "./people.js";
|
|
53
53
|
/** The words of a _STAT (the Strom app's own, also Legacy's and FTM's) as a union, or nothing it names. */
|
|
54
54
|
function unionOfStat(value) {
|
|
55
55
|
const v = (value ?? "").trim().toLowerCase();
|
|
@@ -480,6 +480,8 @@ export function readGedcom(text) {
|
|
|
480
480
|
const head = records.find((r) => r.tag === "HEAD");
|
|
481
481
|
/** A file of the Strom app (its HEAD: 1 SOUR STROM): its own ways of writing are read as it means them. */
|
|
482
482
|
const fromApp = stromAppFile(head);
|
|
483
|
+
// the research's own file (what it gave the app, the research now): it says every choice of a name shown by
|
|
484
|
+
const research = val(head, "SOUR")?.trim() === "STROM_RESEARCH";
|
|
483
485
|
const notes = new Map(records.filter((r) => r.tag === "NOTE" && r.xref).map((r) => [r.xref, r.value]));
|
|
484
486
|
const noteText = (n) => (/^@[^@]+@$/.test(n.value.trim()) ? (notes.get(n.value.trim()) ?? "") : n.value);
|
|
485
487
|
// the sources: ours by their REFN, the file's own by their xref
|
|
@@ -567,6 +569,12 @@ export function readGedcom(text) {
|
|
|
567
569
|
// a placeholder of the Strom app (an unknown parent drawn in the tree): nobody
|
|
568
570
|
if (!ours && !names.length)
|
|
569
571
|
continue;
|
|
572
|
+
// the name they are shown by (spec 024) as the app sees it: its birth name (the first), its one name after a marriage
|
|
573
|
+
// (the last TYPE married, else the last _MARNM), else none — a mark missing says it only in the research's own file
|
|
574
|
+
const mi = kept.findIndex(({ n }) => /^y(es)?$/i.test(val(n, "_STROM_PRIMARY")?.trim() ?? ""));
|
|
575
|
+
const typed = names.filter((_, i) => i > 0 && i < kept.length && kept[i].name.kind === "married");
|
|
576
|
+
const appMarried = typed.at(-1) ?? married.at(-1);
|
|
577
|
+
const primary = mi === 0 ? "birth" : mi > 0 && names[mi] === appMarried ? `married|${nameKey(names[mi])}` : mi > 0 || research ? "none" : undefined;
|
|
570
578
|
const facts = gedFacts(r, undefined, read);
|
|
571
579
|
// a godparent other programs give the person (1 ASSO): theirs at the baptism, else the birth
|
|
572
580
|
const host = facts.find((f) => kindOf(f.kind) === "BAPM") ?? facts.find((f) => f.kind === "BIRT");
|
|
@@ -587,6 +595,7 @@ export function readGedcom(text) {
|
|
|
587
595
|
...(cites.length ? { cites } : {}),
|
|
588
596
|
...(titles ? { titles } : {}),
|
|
589
597
|
...(married.length ? { married } : {}),
|
|
598
|
+
...(primary ? { primary } : {}),
|
|
590
599
|
});
|
|
591
600
|
}
|
|
592
601
|
// how a child belongs to a family (the child's FAMC + PEDI: adopted, foster, step): none said is birth
|
|
@@ -826,8 +835,11 @@ export function readStromJson(data) {
|
|
|
826
835
|
const marriedSurname = typeof p.marriedSurname === "string" ? p.marriedSurname.replace(/[/\s]+/g, " ").trim() : "";
|
|
827
836
|
const married = marriedSurname ? `${p.firstName ?? ""} /${marriedSurname}/`.trim() : undefined;
|
|
828
837
|
const names = [name, ...(married ? [married] : []), ...(p.nameVariants ?? [])].filter((n) => n.replace(/[/?\s]/g, ""));
|
|
838
|
+
// the name the user chose to show them by (spec 024): the birth one, the one after the marriage; none said: nothing
|
|
839
|
+
const primary = p.primaryName === "birth" ? "birth" : p.primaryName === "married" && married ? `married|${nameKey(married)}` : undefined;
|
|
829
840
|
persons.set(key, {
|
|
830
841
|
key,
|
|
842
|
+
...(primary ? { primary } : {}),
|
|
831
843
|
...(titles ? { titles } : {}),
|
|
832
844
|
names: names.filter((n, i) => n !== married || names.findIndex((m) => nameKey(m) === nameKey(n)) === i),
|
|
833
845
|
...(married && names.findIndex((n) => nameKey(n) === nameKey(married)) > 0 ? { married: [married] } : {}),
|
|
@@ -1917,6 +1929,8 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
1917
1929
|
});
|
|
1918
1930
|
}
|
|
1919
1931
|
const partial = !!base && incoming.persons.size < base.persons.size / 2;
|
|
1932
|
+
// a file that says every choice of the name a person is shown by: its mark missing is the user's "none" (spec 024)
|
|
1933
|
+
const knowsPrimary = appKnowsPrimaryName(new Settings(tree.env, {}), incoming.appVersion);
|
|
1920
1934
|
const changes = [];
|
|
1921
1935
|
const push = (c) => changes.push({ ...c, n: changes.length + 1 });
|
|
1922
1936
|
/** What the file no longer has: an archive takes it away — unless the window that sends never had it (kept, said). */
|
|
@@ -2220,6 +2234,34 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
2220
2234
|
push({ kind: "name.gone", action: nameBacked(tree, mine) ? "report" : "remove", person: id, name, text: gedcomName(mine) });
|
|
2221
2235
|
}
|
|
2222
2236
|
};
|
|
2237
|
+
/**
|
|
2238
|
+
* The name the person is shown by (spec 024): the user's choice, never a record's — compared as the app sees it
|
|
2239
|
+
* (SPerson.primary). Changed in the app only: taken; in the research only: the research's, given the app again; both,
|
|
2240
|
+
* apart: the user's to decide (or their word: sync.edits user, an archive). A mark missing from an app before
|
|
2241
|
+
* APP_KNOWS_PRIMARY_NAME (or one that says no version) changes nothing; without the state it was given only a choice
|
|
2242
|
+
* where the research has none is taken.
|
|
2243
|
+
*/
|
|
2244
|
+
const primaryDiff = (id, name, p, o, b) => {
|
|
2245
|
+
const theirs = p.primary ?? (knowsPrimary ? "none" : undefined);
|
|
2246
|
+
const mine = o.primary ?? "none";
|
|
2247
|
+
if (theirs === undefined || theirs === mine)
|
|
2248
|
+
return;
|
|
2249
|
+
let action;
|
|
2250
|
+
if (b) {
|
|
2251
|
+
const given = b.primary ?? "none";
|
|
2252
|
+
if (theirs === given)
|
|
2253
|
+
return;
|
|
2254
|
+
action = mine !== given ? (edits === "user" ? "user" : "conflict") : "correct";
|
|
2255
|
+
}
|
|
2256
|
+
else {
|
|
2257
|
+
if (mine !== "none" || theirs === "none")
|
|
2258
|
+
return;
|
|
2259
|
+
action = "add";
|
|
2260
|
+
}
|
|
2261
|
+
// as the line says it: the name each side shows them by ("" none chosen)
|
|
2262
|
+
const line = (x, view) => (view === "birth" ? (x.names[0] ?? "") : (x.names.find((n) => `married|${nameKey(n)}` === view) ?? ""));
|
|
2263
|
+
push({ kind: "name.shown", action, person: id, name, shown: { theirs, mine }, text: line(p, theirs), wasName: line(o, mine) });
|
|
2264
|
+
};
|
|
2223
2265
|
/** The user's edits of a person: the file's (`p`) against the research's (`o`) and what the app was given (`b`). */
|
|
2224
2266
|
const personDiff = (id, name, p, o, b) => {
|
|
2225
2267
|
factsDiff({ person: id, name }, p.facts, o.facts, b?.facts, true);
|
|
@@ -2247,6 +2289,7 @@ export function planSync(tree, incoming, edits, opts = {}) {
|
|
|
2247
2289
|
if (!theirMarried.has(n) && !((renamed || both) && n === now) && !ourNames.has(nameKey(n)) && !baseNames.has(nameKey(n)) && !goneNames.has(nameKey(n)))
|
|
2248
2290
|
push({ kind: "name.new", action: "add", person: id, name, text: n });
|
|
2249
2291
|
marriedDiff(id, name, p, o, b);
|
|
2292
|
+
primaryDiff(id, name, p, o, b);
|
|
2250
2293
|
// the titles of the name they are shown by (T07), each apart and never as a change of the name itself: the user's
|
|
2251
2294
|
// edit where the file's differs from what the app was given — one the name lacks added, a lead's corrected, a
|
|
2252
2295
|
// record's name the user's to decide (or their word: sync.edits user, an archive); the research's changed since
|
|
@@ -3927,7 +3970,7 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3927
3970
|
}
|
|
3928
3971
|
else if (c.action === "user") {
|
|
3929
3972
|
// the user's word wins: shown by the new name, the record's kept beside it
|
|
3930
|
-
const { name } = addName(tree, owner, { name: c.text, citation: cite("name"),
|
|
3973
|
+
const { name } = addName(tree, owner, { name: c.text, citation: cite("name"), first: true });
|
|
3931
3974
|
applied.push({ do: "name.primary", id: owner, before: JSON.stringify({ added: exact(`${name.given}|${name.surname}`), shown: exact(`${shown.given}|${shown.surname}`) }) });
|
|
3932
3975
|
}
|
|
3933
3976
|
else {
|
|
@@ -3946,6 +3989,28 @@ export function applySync(tree, plan, incoming, source, only) {
|
|
|
3946
3989
|
}
|
|
3947
3990
|
break;
|
|
3948
3991
|
}
|
|
3992
|
+
case "name.shown": {
|
|
3993
|
+
// the name the user chose to show the person by (spec 024): marked, or the mark taken off
|
|
3994
|
+
if (!owner || !c.shown)
|
|
3995
|
+
break;
|
|
3996
|
+
const p = tree.get(owner);
|
|
3997
|
+
const target = nameOfView(p, c.shown.theirs);
|
|
3998
|
+
const was = p.names.find((n) => n.primary);
|
|
3999
|
+
if (target === undefined || (target === null ? !was : target.primary))
|
|
4000
|
+
break;
|
|
4001
|
+
if (c.action === "conflict") {
|
|
4002
|
+
const who = gedcomName(primaryName(p)).replace(/\//g, "").replace(/\s+/g, " ").trim();
|
|
4003
|
+
const said = (n) => (n ? formatName(n) : ui(tree.lang, "ui.conflict.primary.none"));
|
|
4004
|
+
conflictOfEdit(tree, applied, owner, "NAME", editTitle(who, ui(tree.lang, "ui.conflict.primary"), said(was), said(target)), [
|
|
4005
|
+
{ value: was ? gedcomName(was) : "", note: "the research: the name it shows the person by", text: said(was) },
|
|
4006
|
+
{ source: source.id, value: target ? gedcomName(target) : "", note: "the user's edit", text: said(target) },
|
|
4007
|
+
], undefined, undefined, true);
|
|
4008
|
+
break;
|
|
4009
|
+
}
|
|
4010
|
+
markPrimaryName(tree, owner, target ? gedcomName(target) : undefined, `the user's edit: ${reason}`);
|
|
4011
|
+
applied.push({ do: "name.mark", id: owner, before: was ? gedcomName(was) : "" });
|
|
4012
|
+
break;
|
|
4013
|
+
}
|
|
3949
4014
|
case "name.title": {
|
|
3950
4015
|
if (!owner || !c.title)
|
|
3951
4016
|
break;
|
|
@@ -4274,6 +4339,20 @@ function claimValue(f) {
|
|
|
4274
4339
|
* The title of a conflict of the user's edit, in the research's language — the Strom app shows it in what the research
|
|
4275
4340
|
* knows (found on Mac: "Petr Svoboda: SEX — U × F" in a Czech research): "Petr Svoboda: Pohlaví — neznámé × žena".
|
|
4276
4341
|
*/
|
|
4342
|
+
/**
|
|
4343
|
+
* The research's name of a person the app's view of the name they are shown by names (SPerson.primary): "birth" the
|
|
4344
|
+
* name of their identity, "married|<key>" their name of that key (not the identity's), "none" null; undefined where the
|
|
4345
|
+
* research has no such name.
|
|
4346
|
+
*/
|
|
4347
|
+
function nameOfView(p, view) {
|
|
4348
|
+
if (view === "none")
|
|
4349
|
+
return null;
|
|
4350
|
+
const first = primaryName(p);
|
|
4351
|
+
if (view === "birth")
|
|
4352
|
+
return first;
|
|
4353
|
+
const key = view.startsWith("married|") ? view.slice("married|".length) : undefined;
|
|
4354
|
+
return key === undefined ? undefined : p.names.find((n) => n !== first && nameKey(gedcomName(n)) === key);
|
|
4355
|
+
}
|
|
4277
4356
|
function editTitle(who, what, ours, theirs) {
|
|
4278
4357
|
return `${who.replace(/\//g, "").replace(/\s+/g, " ").trim()}: ${what.charAt(0).toUpperCase()}${what.slice(1)} — ${ours} × ${theirs}`;
|
|
4279
4358
|
}
|
|
@@ -4363,6 +4442,19 @@ export function takeSide(tree, x, side, reason) {
|
|
|
4363
4442
|
setTitle(tree, p.id, part, user.value, from, `the user's edit (${x.id}): ${reason}`);
|
|
4364
4443
|
return `${p.id}: ${titledName(primaryName(tree.get(p.id)))}`;
|
|
4365
4444
|
}
|
|
4445
|
+
// the name the user chose in the Strom app to show the person by (spec 024): marked, or none marked
|
|
4446
|
+
if (x.primary) {
|
|
4447
|
+
const user = x.claims.find((c) => c.note === "the user's edit");
|
|
4448
|
+
const p = x.subject[0] ? tree.get(x.subject[0]) : undefined;
|
|
4449
|
+
if (!p || p.retracted || !user)
|
|
4450
|
+
throw new UsageError(`the person of ${x.id} is no longer in the research`, { hint: `strom conflict show ${x.id}` });
|
|
4451
|
+
const target = user.value ? p.names.find((n) => sameWords(n, parseName(user.value))) : null;
|
|
4452
|
+
if (target === undefined)
|
|
4453
|
+
throw new UsageError(`${p.id} has no name ${user.value} any more`, { hint: `strom person show ${p.id}` });
|
|
4454
|
+
if (target ? !target.primary : p.names.some((n) => n.primary))
|
|
4455
|
+
markPrimaryName(tree, p.id, target ? gedcomName(target) : undefined, `the user's edit (${x.id}): ${reason}`);
|
|
4456
|
+
return `${p.id} shown as ${gedcomName(currentName(tree.get(p.id)))}`;
|
|
4457
|
+
}
|
|
4366
4458
|
// a name or a sex the user gave in the Strom app (found on Mac: --take refused for them, only for facts and parents)
|
|
4367
4459
|
if (!x.edit && (x.fact === "NAME" || x.fact === "SEX")) {
|
|
4368
4460
|
const owner = x.subject[0];
|
|
@@ -4378,7 +4470,7 @@ export function takeSide(tree, x, side, reason) {
|
|
|
4378
4470
|
const words = (v) => foldText(v.replace(/\//g, " ")).replace(/\s+/g, " ").trim();
|
|
4379
4471
|
const kept = p.names.find((n) => words(`${n.given} ${n.surname}`) === words(user.value));
|
|
4380
4472
|
const from = user.source && tree.get(user.source)?.type === "source" && !kept?.citations?.some((c) => c.source === user.source) ? { source: user.source, locator: "name" } : undefined;
|
|
4381
|
-
const { name } = addName(tree, p.id, { name: user.value,
|
|
4473
|
+
const { name } = addName(tree, p.id, { name: user.value, first: true, ...(from ? { citation: from } : {}) });
|
|
4382
4474
|
return `${p.id} shown as ${gedcomName(name)}`;
|
|
4383
4475
|
}
|
|
4384
4476
|
if (!x.edit)
|
|
@@ -4415,11 +4507,11 @@ function parentsText(tree, incoming, keys) {
|
|
|
4415
4507
|
};
|
|
4416
4508
|
return keys.map(shown).join(" & ") || "?";
|
|
4417
4509
|
}
|
|
4418
|
-
function conflictOfEdit(tree, applied, owner, fact, title, claims, edit, parents) {
|
|
4510
|
+
function conflictOfEdit(tree, applied, owner, fact, title, claims, edit, parents, primary) {
|
|
4419
4511
|
const [ours, theirs] = claims;
|
|
4420
4512
|
const open = tree
|
|
4421
4513
|
.list("conflict")
|
|
4422
|
-
.find((x) => x.state === "open" && x.fact === fact && x.subject[0] === owner && x.claims.length === 2 && x.claims[0].note === ours.note && x.claims[0].value === ours.value && x.claims[1].note === theirs.note);
|
|
4514
|
+
.find((x) => x.state === "open" && x.fact === fact && !!x.primary === !!primary && x.subject[0] === owner && x.claims.length === 2 && x.claims[0].note === ours.note && x.claims[0].value === ours.value && x.claims[1].note === theirs.note);
|
|
4423
4515
|
if (open) {
|
|
4424
4516
|
if (open.claims[1].value === theirs.value)
|
|
4425
4517
|
return { open: open.id };
|
|
@@ -4430,7 +4522,7 @@ function conflictOfEdit(tree, applied, owner, fact, title, claims, edit, parents
|
|
|
4430
4522
|
applied.push({ do: "conflict.edit", id: open.id, before: JSON.stringify({ title: open.title, claims: open.claims }) });
|
|
4431
4523
|
return { was: open.claims[1].value };
|
|
4432
4524
|
}
|
|
4433
|
-
const x = create(tree, "conflict", { title: title.slice(0, 200), fact, subject: [owner], claims, ...(edit ? { edit } : {}), ...(parents ? { parents } : {}), state: "open" }, (id) => `+${id} conflict "${title.slice(0, 60)}"`, [owner]);
|
|
4525
|
+
const x = create(tree, "conflict", { title: title.slice(0, 200), fact, subject: [owner], claims, ...(edit ? { edit } : {}), ...(parents ? { parents } : {}), ...(primary ? { primary } : {}), state: "open" }, (id) => `+${id} conflict "${title.slice(0, 60)}"`, [owner]);
|
|
4434
4526
|
applied.push({ do: "conflict.add", id: x.id });
|
|
4435
4527
|
return {};
|
|
4436
4528
|
}
|
|
@@ -4643,6 +4735,16 @@ export function undoSync(tree, input) {
|
|
|
4643
4735
|
});
|
|
4644
4736
|
break;
|
|
4645
4737
|
}
|
|
4738
|
+
case "name.mark": {
|
|
4739
|
+
// the name the person was shown by before the user's choice (none: no mark)
|
|
4740
|
+
const p = tree.get(a.id);
|
|
4741
|
+
const was = String(a.before ?? "");
|
|
4742
|
+
const back = was ? p?.names.find((n) => sameWords(n, parseName(was))) : null;
|
|
4743
|
+
if (!p || back === undefined || (back === null ? !p.names.some((n) => n.primary) : back.primary))
|
|
4744
|
+
break;
|
|
4745
|
+
markPrimaryName(tree, a.id, back ? gedcomName(back) : undefined, reason);
|
|
4746
|
+
break;
|
|
4747
|
+
}
|
|
4646
4748
|
case "name.title": {
|
|
4647
4749
|
// the title as it was, and the citation of the sync it added taken off
|
|
4648
4750
|
const b = JSON.parse(String(a.before ?? "{}"));
|