strom-research 1.16.0-beta.2 → 1.16.0-beta.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.cs.md +2 -0
- package/CHANGELOG.de.md +4 -1
- package/CHANGELOG.md +2 -0
- package/dist/commands/intake.js +76 -2
- package/dist/core/import.js +102 -3
- package/dist/core/tree.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -20,6 +20,8 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
20
20
|
jako jméno po sňatku, nikdy jako přejmenování ani jako další tvar rodného jména; stejné příjmení je stejné jméno,
|
|
21
21
|
ať s ním jde jakékoli křestní. Když se v aplikaci změní nebo smaže, staré zmizí tam, kde ho daly jen úpravy
|
|
22
22
|
v aplikaci; jméno doložené záznamem zůstává. `strom intake` čte `_MARNM` jiných programů jako jméno po sňatku.
|
|
23
|
+
Výzkum založený dřív z takového rodokmenu (MyHeritage, Legacy, PAF) je doplní ze souboru, který má uložený:
|
|
24
|
+
`strom input names I…` je ukáže, `--apply` přidá jen tato jména s citací importu, `--undo` je vezme zpět.
|
|
23
25
|
- **Linie výzkumu.** České texty všude říkají „linie výzkumu“, nikde už „směry“.
|
|
24
26
|
|
|
25
27
|
## 1.15.0
|
package/CHANGELOG.de.md
CHANGED
|
@@ -21,7 +21,10 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
21
21
|
Strom-App zurückschickt, kommt als Name nach der Heirat an, nie als Umbenennung oder als weitere Form des
|
|
22
22
|
Geburtsnamens; derselbe Familienname ist derselbe Name, mit welchem Vornamen auch immer. In der App geändert oder
|
|
23
23
|
entfernt, geht der alte dort, wo ihn nur Änderungen in der App gaben; einer, den ein Eintrag belegt, bleibt.
|
|
24
|
-
`strom intake` liest `_MARNM` anderer Programme als Namen nach der Heirat.
|
|
24
|
+
`strom intake` liest `_MARNM` anderer Programme als Namen nach der Heirat. Eine Forschung, die früher aus so einem
|
|
25
|
+
Stammbaum begonnen wurde (MyHeritage, Legacy, PAF), bekommt sie aus der Datei, die sie aufbewahrt:
|
|
26
|
+
`strom input names I…` zeigt sie, `--apply` fügt nur diese Namen hinzu, mit dem Import als Quelle, `--undo` nimmt
|
|
27
|
+
sie zurück.
|
|
25
28
|
- **Forschungslinien auf Tschechisch.** Die tschechischen Texte sagen jetzt überall „linie výzkumu“ (Forschungslinien).
|
|
26
29
|
|
|
27
30
|
## 1.15.0
|
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,8 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
20
20
|
comes in as a name of the married kind, never as a rename or another form of the birth name; the same surname is
|
|
21
21
|
the same name whatever first name goes with it. Changed or taken off in the app, the old one goes where only edits
|
|
22
22
|
in the app gave it; one a record gives stays. `strom intake` reads `_MARNM` of other programs as a married name.
|
|
23
|
+
A research started earlier from such a family tree (MyHeritage, Legacy, PAF) gets them from the file it keeps:
|
|
24
|
+
`strom input names I…` shows them, `--apply` adds only these names, citing the import, `--undo` takes them back.
|
|
23
25
|
- **Lines of research in Czech.** The Czech texts say „linie výzkumu“ (lines of research) everywhere now.
|
|
24
26
|
|
|
25
27
|
## 1.15.0
|
package/dist/commands/intake.js
CHANGED
|
@@ -9,11 +9,11 @@ import path from "node:path";
|
|
|
9
9
|
import { register } from "../cli/registry.js";
|
|
10
10
|
import { lines, moreLine, paginate, table, truncate } from "../cli/format.js";
|
|
11
11
|
import { notFound, UsageError } from "../core/errors.js";
|
|
12
|
-
import { likelyDuplicates } from "../core/people.js";
|
|
13
12
|
import { collectFiles, fileSha256, inputPath, MAX_IN_TREE, mimeOf, storeShared } from "../core/media.js";
|
|
14
13
|
import { create, normId, requireRecord, update } from "../core/records.js";
|
|
15
14
|
import { phrase } from "../core/phrases.js";
|
|
16
|
-
import { importGedcom, importStromJson, isStromJson } from "../core/import.js";
|
|
15
|
+
import { addMarriedNames, importedTree, importGedcom, importStromJson, isStromJson, marriedNamesPlan, undoMarriedNames } from "../core/import.js";
|
|
16
|
+
import { gedcomName, likelyDuplicates } from "../core/people.js";
|
|
17
17
|
import { decodeText, safeFolderName } from "../core/text.js";
|
|
18
18
|
import { amendInput, checkOriginalMeta } from "../core/originals.js";
|
|
19
19
|
import { batchStatus, closeBatch, listBatches, readBatch } from "../core/batches.js";
|
|
@@ -153,6 +153,13 @@ register({
|
|
|
153
153
|
skipped++;
|
|
154
154
|
if (forTask)
|
|
155
155
|
handed.push(known.get(sha));
|
|
156
|
+
// a family tree imported before strom read its married names (_MARNM): said how they come in
|
|
157
|
+
const had = tree.get(known.get(sha));
|
|
158
|
+
if (!quiet && had && !importedTree(had)) {
|
|
159
|
+
const missing = marriedNamesPlan(tree, had, decodeText(fs.readFileSync(file))).add.length;
|
|
160
|
+
if (missing)
|
|
161
|
+
report.push(` ${had.id}: ${missing} married name(s) it gives were left out when it was imported → strom input names ${had.id}`);
|
|
162
|
+
}
|
|
156
163
|
continue;
|
|
157
164
|
}
|
|
158
165
|
const size = fs.statSync(file).size;
|
|
@@ -315,6 +322,73 @@ register({
|
|
|
315
322
|
const got = tree.withTreeLock(() => amendInput(tree, id, { persons, note: typeof opts.note === "string" ? opts.note : undefined }));
|
|
316
323
|
return { text: tree.written.length ? lines(...tree.written.map((o) => o.summary)) : `${id}: nothing new`, data: got };
|
|
317
324
|
},
|
|
325
|
+
}, {
|
|
326
|
+
path: ["input", "names"],
|
|
327
|
+
summary: "The married names (_MARNM) of a family tree imported before strom read them: shown, --apply adds them",
|
|
328
|
+
group: "inputs",
|
|
329
|
+
tree: true,
|
|
330
|
+
writes: true,
|
|
331
|
+
description: "strom before 1.16 left out the married names other programs write as _MARNM (MyHeritage, Legacy, PAF). This reads\n" +
|
|
332
|
+
"the file the research was started from again — the input it keeps — and finds the people imported from it by the\n" +
|
|
333
|
+
"IDs the import kept (nobody else). Only names of the married kind are added, beside the name each person is shown\n" +
|
|
334
|
+
"by, citing the import's source; a person who has the name already stays as they are; people of the file not in the\n" +
|
|
335
|
+
"research are only listed. Without --apply only shown; --apply is one commit; --undo takes back what it added.\n" +
|
|
336
|
+
"Another export of the tree (another file) is not paired: give the input the research has (strom input list).",
|
|
337
|
+
args: [{ name: "input", description: "the imported family tree: its input (I…), or the same file", required: true }],
|
|
338
|
+
options: [
|
|
339
|
+
{ name: "apply", type: "boolean", description: "add them (without: only show)" },
|
|
340
|
+
{ name: "undo", type: "boolean", description: "take back the married names it added from this input" },
|
|
341
|
+
],
|
|
342
|
+
examples: ["strom input names I0004", "strom input names ~/Downloads/rodokmen.ged --apply", "strom input names I0004 --undo"],
|
|
343
|
+
run(ctx, { args, opts }) {
|
|
344
|
+
const tree = ctx.tree();
|
|
345
|
+
const arg = args[0];
|
|
346
|
+
const inputs = tree.list("input");
|
|
347
|
+
let input = /^I?\d+$/i.test(arg.trim()) ? inputs.find((i) => i.id === normId(arg, "input")) : undefined;
|
|
348
|
+
if (!input) {
|
|
349
|
+
const p = ctx.resolvePath(arg);
|
|
350
|
+
if (!fs.existsSync(p) || !fs.statSync(p).isFile()) {
|
|
351
|
+
// an input's ID this research does not have (a file of that name neither)
|
|
352
|
+
if (/^I?\d+$/i.test(arg.trim()))
|
|
353
|
+
throw new UsageError(`no input ${normId(arg, "input")} in this research`, { hint: "strom input list", code: "input.names.missing", params: { input: normId(arg, "input") } });
|
|
354
|
+
throw notFound(arg);
|
|
355
|
+
}
|
|
356
|
+
const sha = fileSha256(p);
|
|
357
|
+
input = inputs.find((i) => i.sha === sha);
|
|
358
|
+
if (!input)
|
|
359
|
+
throw new UsageError(`${ctx.display(p)} is not a family tree this research was started from (another file, another export)`, {
|
|
360
|
+
hint: "the research keeps the file it imported: strom input list — then strom input names I…",
|
|
361
|
+
code: "input.names.other-file",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const why = importedTree(input);
|
|
365
|
+
if (why)
|
|
366
|
+
throw new UsageError(why, { hint: "strom input list — a family tree imported from GEDCOM (kind tree)", code: "input.names.not-imported", params: { input: input.id } });
|
|
367
|
+
const name = (id) => {
|
|
368
|
+
const p = tree.get(id);
|
|
369
|
+
return p?.names[0] ? gedcomName(p.names[0]).replace(/\//g, "").replace(/\s+/g, " ").trim() : id;
|
|
370
|
+
};
|
|
371
|
+
if (opts.undo) {
|
|
372
|
+
const gone = undoMarriedNames(tree, input);
|
|
373
|
+
return {
|
|
374
|
+
text: gone.length ? lines(...gone.map((g) => ` ${g.person} ${name(g.person)}: − ${gedcomName(g.name)}`), `${gone.length} married name(s) from ${input.id} taken back`) : `${input.id}: no married name of it to take back`,
|
|
375
|
+
data: { input: input.id, undone: gone.map((g) => ({ person: g.person, name: gedcomName(g.name) })) },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const file = inputPath(tree, input);
|
|
379
|
+
if (!file || !fs.existsSync(file))
|
|
380
|
+
throw new UsageError(`the file of ${input.id} is not here (${input.file ?? "none"})`, { hint: `strom input show ${input.id}`, code: "input.names.no-file", params: { input: input.id } });
|
|
381
|
+
const plan = marriedNamesPlan(tree, input, decodeText(fs.readFileSync(file)));
|
|
382
|
+
if (opts.apply && plan.add.length)
|
|
383
|
+
addMarriedNames(tree, plan);
|
|
384
|
+
const done = !!opts.apply && !tree.dryRun;
|
|
385
|
+
const text = lines(`${input.id} ${input.name}: ${plan.add.length} married name(s) ${done ? "added" : "to add"}, ${plan.have.length} already there, ${plan.unpaired.length} person(s) of the file not in the research`, ...plan.add.map((a) => ` ${a.person} ${name(a.person)}: + ${gedcomName(a.name)} (married)`), plan.unpaired.length ? "not in the research (nothing added):" : undefined, ...plan.unpaired.map((u) => ` ${u.xref} ${u.name.replace(/\//g, "").replace(/\s+/g, " ").trim()}: ${u.married.map(gedcomName).join(", ")}`), plan.add.length && !opts.apply ? `\nadd them: strom input names ${input.id} --apply` : undefined, done && plan.add.length ? `take them back: strom input names ${input.id} --undo` : undefined);
|
|
386
|
+
const row = (a) => ({ person: a.person, xref: a.xref, name: gedcomName(a.name) });
|
|
387
|
+
return {
|
|
388
|
+
text,
|
|
389
|
+
data: { input: input.id, source: plan.source, applied: done, add: plan.add.map(row), have: plan.have.map(row), unpaired: plan.unpaired.map((u) => ({ xref: u.xref, name: u.name, married: u.married.map(gedcomName) })) },
|
|
390
|
+
};
|
|
391
|
+
},
|
|
318
392
|
}, {
|
|
319
393
|
path: ["input", "batch"],
|
|
320
394
|
summary: "Batches of material the Strom app sent: how many files, how far they are sorted",
|
package/dist/core/import.js
CHANGED
|
@@ -11,7 +11,8 @@ import { foldText } from "./text.js";
|
|
|
11
11
|
import { roleWord } from "./roles.js";
|
|
12
12
|
import { validateRecord } from "./validate.js";
|
|
13
13
|
import { normalizeDate } from "./gdate.js";
|
|
14
|
-
import { appNoSurname, cleanTitle, noName, notAName, parseName, sameName, surnameAfterGiven, withoutTitles } from "./people.js";
|
|
14
|
+
import { appNoSurname, cleanTitle, gedcomName, noName, notAName, parseName, sameName, surnameAfterGiven, withoutTitles } from "./people.js";
|
|
15
|
+
import { update } from "./records.js";
|
|
15
16
|
import { now } from "./tree.js";
|
|
16
17
|
import { appUnknownIsNoSurname, appWritesSexUnknown } from "./stromapp.js";
|
|
17
18
|
import { children, parseGedcomText, val } from "../gedcom/parse.js";
|
|
@@ -197,6 +198,11 @@ export function marnmNames(r, givenOf) {
|
|
|
197
198
|
add(m.value, names[0] ? givenOf(names[0]) : "");
|
|
198
199
|
return out;
|
|
199
200
|
}
|
|
201
|
+
/** The married names (_MARNM) of a person of a file as strom intake takes them: with the given name of their NAME (none for what stands in place of a name). */
|
|
202
|
+
function importedMarnm(r, file) {
|
|
203
|
+
const given = (n) => (notAName(readGedName(n, file).name.given) ? "" : readGedName(n, file).name.given.replace(/\//g, "|"));
|
|
204
|
+
return marnmNames(r, given);
|
|
205
|
+
}
|
|
200
206
|
export function importGedcom(tree, text, opts) {
|
|
201
207
|
const { records, problems } = parseGedcomText(text);
|
|
202
208
|
/** A file of the Strom app (its HEAD: 1 SOUR STROM): its own ways of writing are read as it means them. */
|
|
@@ -370,8 +376,7 @@ export function importGedcom(tree, text, opts) {
|
|
|
370
376
|
})
|
|
371
377
|
.map((n) => (n.given || n.surname ? n : { ...n, given: "?", surname: "" }));
|
|
372
378
|
// a married name other programs give apart (_MARNM): a name of the married kind, unless the person has it already
|
|
373
|
-
|
|
374
|
-
return [...names, ...marnmNames(r, given).filter((m) => !names.some((n) => sameName(n, m)))];
|
|
379
|
+
return [...names, ...importedMarnm(r, fromApp).filter((m) => !names.some((n) => sameName(n, m)))];
|
|
375
380
|
};
|
|
376
381
|
/** The Strom app keeps what a birth or death entry adds in the person's note ("Úmrtí: Věk: 61 let"): back to the fact; the other lines are returned. */
|
|
377
382
|
const factNotes = (r, personEvents) => {
|
|
@@ -532,6 +537,100 @@ export function importGedcom(tree, text, opts) {
|
|
|
532
537
|
* research's tree coming back"); a file that names no research (the standard file, another program) only where most
|
|
533
538
|
* of the people its IDs name are alike by name — and then each by its name too. Merged since: the one it went into.
|
|
534
539
|
*/
|
|
540
|
+
// ── the married names an earlier import left out ─────────────────────────────
|
|
541
|
+
//
|
|
542
|
+
// strom before 1.16 read no _MARNM (MyHeritage, Legacy, PAF): a research started from such a file lacks the names
|
|
543
|
+
// after a marriage. Taken in again from the very file it was started from (its input), by the IDs the import kept
|
|
544
|
+
// (refs gedcom:<sha> + the file's xref) — nobody else, nothing but these names.
|
|
545
|
+
/** Where a married name taken in later cites the import: the person's xref in the file and the tag. */
|
|
546
|
+
const MARNM_LOCATOR = " _MARNM";
|
|
547
|
+
/** The input a family tree was imported from (a GEDCOM file kept by the research), else why not. */
|
|
548
|
+
export function importedTree(input) {
|
|
549
|
+
if (input.kind !== "tree" || !input.sha || !/\.ged$/i.test(input.name))
|
|
550
|
+
return `${input.id} is not a GEDCOM family tree`;
|
|
551
|
+
if (!input.source || !input.imported)
|
|
552
|
+
return `${input.id} was not imported (strom intake --no-import)`;
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
/** What the input's file gives as married names (_MARNM) that the people imported from it lack. */
|
|
556
|
+
export function marriedNamesPlan(tree, input, text) {
|
|
557
|
+
const { records } = parseGedcomText(text);
|
|
558
|
+
const file = stromAppFile(records.find((r) => r.tag === "HEAD"));
|
|
559
|
+
const system = `gedcom:${input.sha.slice(0, 12)}`;
|
|
560
|
+
// the people the import made of the file (or added to): by the xref it kept; one merged since is the one it went into
|
|
561
|
+
const byXref = new Map();
|
|
562
|
+
for (const p of tree.list("person"))
|
|
563
|
+
for (const r of p.refs ?? [])
|
|
564
|
+
if (r.system === system)
|
|
565
|
+
byXref.set(r.id, p);
|
|
566
|
+
const live = (p) => {
|
|
567
|
+
for (let i = 0; p && i < 20; i++) {
|
|
568
|
+
if (!p.mergedInto)
|
|
569
|
+
return p.retracted ? undefined : p;
|
|
570
|
+
p = tree.get(p.mergedInto);
|
|
571
|
+
}
|
|
572
|
+
return undefined;
|
|
573
|
+
};
|
|
574
|
+
const plan = { input: input.id, source: input.source, add: [], have: [], unpaired: [] };
|
|
575
|
+
for (const r of records.filter((x) => x.tag === "INDI" && x.xref)) {
|
|
576
|
+
// NFC as every record (a file may write a name decomposed)
|
|
577
|
+
const married = importedMarnm(r, file).map((n) => ({ ...n, given: n.given.normalize("NFC"), surname: n.surname.normalize("NFC") }));
|
|
578
|
+
if (!married.length)
|
|
579
|
+
continue;
|
|
580
|
+
const p = live(byXref.get(r.xref));
|
|
581
|
+
if (!p) {
|
|
582
|
+
plan.unpaired.push({ xref: r.xref, name: (children(r, "NAME")[0]?.value ?? "?").replace(/\s+/g, " ").trim(), married });
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
for (const name of married) {
|
|
586
|
+
const had = p.names.some((n) => sameName(n, name)) || plan.add.some((a) => a.person === p.id && sameName(a.name, name));
|
|
587
|
+
(had ? plan.have : plan.add).push({ person: p.id, xref: r.xref, name });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return plan;
|
|
591
|
+
}
|
|
592
|
+
/** Add the names of a plan: each of the married kind, last, citing the import (the person's xref and _MARNM). */
|
|
593
|
+
export function addMarriedNames(tree, plan) {
|
|
594
|
+
tree.withTreeLock(() => {
|
|
595
|
+
for (const a of plan.add) {
|
|
596
|
+
const name = { ...a.name, citations: [{ source: plan.source, locator: `${a.xref.replace(/@/g, "")}${MARNM_LOCATOR}` }] };
|
|
597
|
+
update(tree, a.person, "person", (p) => (p.names.some((n) => sameName(n, name)) ? p : { ...p, names: [...p.names, name] }), {
|
|
598
|
+
op: "person.name",
|
|
599
|
+
summary: `${a.person} name ${gedcomName(name)} (married) ← ${plan.source}`,
|
|
600
|
+
reason: `the married name ${plan.input} gives (_MARNM), left out when it was imported`,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/** Take back what addMarriedNames added from this input: the names citing its import there (another citation kept: only this one goes). */
|
|
606
|
+
export function undoMarriedNames(tree, inputRec) {
|
|
607
|
+
const source = inputRec.source;
|
|
608
|
+
const ours = (c) => c.source === source && !!c.locator?.endsWith(MARNM_LOCATOR);
|
|
609
|
+
const gone = [];
|
|
610
|
+
if (!source)
|
|
611
|
+
return gone;
|
|
612
|
+
tree.withTreeLock(() => {
|
|
613
|
+
for (const p of tree.list("person")) {
|
|
614
|
+
const at = p.names.flatMap((n, i) => (i > 0 && n.kind === "married" && n.citations?.some(ours) ? [i] : []));
|
|
615
|
+
if (!at.length)
|
|
616
|
+
continue;
|
|
617
|
+
const hit = at.map((i) => p.names[i]);
|
|
618
|
+
update(tree, p.id, "person", (x) => ({
|
|
619
|
+
...x,
|
|
620
|
+
names: x.names.flatMap((n, i) => {
|
|
621
|
+
if (!at.includes(i))
|
|
622
|
+
return [n];
|
|
623
|
+
// cited by something else too: only this citation goes
|
|
624
|
+
const rest = (n.citations ?? []).filter((c) => !ours(c));
|
|
625
|
+
return rest.length ? [{ ...n, citations: rest }] : [];
|
|
626
|
+
}),
|
|
627
|
+
}), { op: "name.remove", summary: `${p.id} name ${hit.map(gedcomName).join(", ")} taken back`, reason: `the married names taken in from ${inputRec.id} taken back` });
|
|
628
|
+
for (const n of hit)
|
|
629
|
+
gone.push({ person: p.id, name: n });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
return gone;
|
|
633
|
+
}
|
|
535
634
|
function oursByRefn(tree, treeId, people) {
|
|
536
635
|
if (treeId && treeId !== tree.config.id)
|
|
537
636
|
return people.map(() => undefined);
|
package/dist/core/tree.js
CHANGED
|
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
|
|
|
27
27
|
export const TREE_FILE = "strom.json";
|
|
28
28
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
29
29
|
// at a release the release's own (package.json)
|
|
30
|
-
export const VERSION = "1.16.0-beta.
|
|
30
|
+
export const VERSION = "1.16.0-beta.3";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|