strom-research 1.16.0-beta.2 → 1.16.0-beta.4
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 +4 -0
- package/CHANGELOG.de.md +6 -1
- package/CHANGELOG.md +4 -0
- package/dist/cli/menu-archives.js +5 -4
- package/dist/cli/menu-links.js +3 -2
- package/dist/cli/menu-waiting.js +1 -1
- package/dist/commands/archives.js +13 -2
- package/dist/commands/intake.js +76 -2
- package/dist/commands/meta.js +1 -1
- package/dist/core/archiveasks.js +4 -3
- package/dist/core/import.js +102 -3
- package/dist/core/terms.js +65 -2
- package/dist/core/tree.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -20,6 +20,10 @@ 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.
|
|
25
|
+
- **Jeden archiv, jedna otázka.** Když obrazy jednoho archivu přicházejí z více adres (jeho web, album, server
|
|
26
|
+
obrazů), svolení archivu se žádá jednou a platí pro všechny, ne adresu po adrese.
|
|
23
27
|
- **Linie výzkumu.** České texty všude říkají „linie výzkumu“, nikde už „směry“.
|
|
24
28
|
|
|
25
29
|
## 1.15.0
|
package/CHANGELOG.de.md
CHANGED
|
@@ -21,7 +21,12 @@ 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.
|
|
28
|
+
- **Ein Archiv, eine Frage.** Kommen die Bilder eines Archivs von mehreren Adressen (seine Seite, ein Album, ein
|
|
29
|
+
Bildserver), wird die Erlaubnis des Archivs einmal erfragt und gilt für alle, nicht Adresse für Adresse.
|
|
25
30
|
- **Forschungslinien auf Tschechisch.** Die tschechischen Texte sagen jetzt überall „linie výzkumu“ (Forschungslinien).
|
|
26
31
|
|
|
27
32
|
## 1.15.0
|
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,10 @@ 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.
|
|
25
|
+
- **One archive, one question.** When one archive's images come from several hosts (its site, an album, an image
|
|
26
|
+
server), the permission it needs is asked once and decided for all of them, not host by host.
|
|
23
27
|
- **Lines of research in Czech.** The Czech texts say „linie výzkumu“ (lines of research) everywhere now.
|
|
24
28
|
|
|
25
29
|
## 1.15.0
|
|
@@ -11,7 +11,7 @@ import { humanDay } from "./human.js";
|
|
|
11
11
|
import { Tree } from "../core/tree.js";
|
|
12
12
|
import { isArchive } from "../core/mode.js";
|
|
13
13
|
import { bareHost, listConnectors, routeOf, routesOf } from "../core/connector.js";
|
|
14
|
-
import { claimOf, loadArchives, rank } from "../core/terms.js";
|
|
14
|
+
import { claimOf, loadArchives, pendingQuestions, questionHosts, rank } from "../core/terms.js";
|
|
15
15
|
import { connectorFor, levelWords, tasksStanding, tasksWaitingOn, termsText, waysOffered, wokenLines } from "../core/archiveasks.js";
|
|
16
16
|
import { wakeForArchive } from "../core/archivewait.js";
|
|
17
17
|
import { archivesOverview, capWords, levelsOf, paceFor } from "../commands/archives.js";
|
|
@@ -53,8 +53,9 @@ export async function decideArchive(ctx, run, lang, root, host, q, all) {
|
|
|
53
53
|
const title = a?.title ?? q?.title ?? c.manifest.title ?? host;
|
|
54
54
|
const tree = root ? Tree.open(root, ctx.env) : undefined;
|
|
55
55
|
const hosts = all && all.length > 1 ? all : [host];
|
|
56
|
-
// the command's target: the downloader for all its hosts at once, else the host
|
|
57
|
-
|
|
56
|
+
// the command's target: the downloader for all its hosts at once, else the host — with every host of its question
|
|
57
|
+
// (strom allow archive decides a question of one archive for all its hosts)
|
|
58
|
+
const target = hosts.length > 1 && hosts.some((h) => !q?.archiveHosts?.includes(h)) ? c.name : host;
|
|
58
59
|
out("");
|
|
59
60
|
out(` ${t("ui.archive.item", { title, host: hosts.join(", ") })}`);
|
|
60
61
|
out(` ${termsText(lang, ctx.env, { host, ...(a?.quote ? { quote: a.quote } : q?.quote ? { quote: q.quote } : {}), ...(a?.terms ?? q?.terms ? { terms: (a?.terms ?? q?.terms) } : {}), ...(a?.quoteBy ? { quoteBy: a.quoteBy } : q?.quoteBy ? { quoteBy: q.quoteBy } : {}) })}`);
|
|
@@ -222,7 +223,7 @@ function hostStates(ctx, lang, c) {
|
|
|
222
223
|
/** Settings → Archives → permissions: each archive of a downloader here, then the same choices as in What waits. */
|
|
223
224
|
export async function archivePermissions(ctx, run, lang, root) {
|
|
224
225
|
const t = translator(lang);
|
|
225
|
-
const pending = (hosts) =>
|
|
226
|
+
const pending = (hosts) => pendingQuestions(ctx.env).find((p) => p.kind === "permission" && questionHosts(p).some((h) => hosts.includes(h)));
|
|
226
227
|
await subMenu(ctx, lang, () => {
|
|
227
228
|
const rows = archivesOverview(ctx);
|
|
228
229
|
const connectors = connectorsHere(ctx);
|
package/dist/cli/menu-links.js
CHANGED
|
@@ -35,6 +35,7 @@ import { foldText, safeFolderName } from "../core/text.js";
|
|
|
35
35
|
import { StromError } from "../core/errors.js";
|
|
36
36
|
import { forgetTree } from "../commands/trees.js";
|
|
37
37
|
import { archiveQuestions } from "../core/archiveasks.js";
|
|
38
|
+
import { questionHosts } from "../core/terms.js";
|
|
38
39
|
import { bareHost, listConnectors } from "../core/connector.js";
|
|
39
40
|
import { decideArchive } from "./menu-archives.js";
|
|
40
41
|
import { inboxOf, requestFile, sameGroup } from "../core/waiting.js";
|
|
@@ -173,11 +174,11 @@ export async function archiveAsked(ctx, run, lang, root, host) {
|
|
|
173
174
|
const tree = Tree.open(root, ctx.env);
|
|
174
175
|
if (isArchive(tree))
|
|
175
176
|
return stop(ctx, t("ui.link.archive.none"));
|
|
176
|
-
const q = archiveQuestions(tree).find((p) => p.kind === "permission" && p.host
|
|
177
|
+
const q = archiveQuestions(tree).find((p) => p.kind === "permission" && questionHosts(p).includes(host));
|
|
177
178
|
const known = q ? true : listConnectors(ctx.settings.shared()?.value, ctx.env).some((c) => c.manifest.hosts.map(bareHost).includes(host));
|
|
178
179
|
if (!known)
|
|
179
180
|
return stop(ctx, t("ui.link.archive.none"));
|
|
180
|
-
return decideArchive(ctx, run, lang, root, host, q);
|
|
181
|
+
return decideArchive(ctx, run, lang, root, host, q, q?.archiveHosts);
|
|
181
182
|
}
|
|
182
183
|
/** A direction of the research paused, ended (each with the person's reason, if they give one) or taken up again. */
|
|
183
184
|
export async function directionDo(ctx, run, lang, root, id, what) {
|
package/dist/cli/menu-waiting.js
CHANGED
|
@@ -426,7 +426,7 @@ async function decide(ctx, run, lang, root, w) {
|
|
|
426
426
|
return void (await decideStory(ctx, run, lang, root, w.story, storyOf(tree, w.story)));
|
|
427
427
|
const q = w.question;
|
|
428
428
|
if (q?.kind === "permission" && q.host)
|
|
429
|
-
return void (await decideArchive(ctx, run, lang, root, q.host, q));
|
|
429
|
+
return void (await decideArchive(ctx, run, lang, root, q.host, q, q.archiveHosts));
|
|
430
430
|
if (q?.kind === "takeover")
|
|
431
431
|
return decideTakeover(ctx, run, lang, q);
|
|
432
432
|
if (q?.kind === "milder" && q.connector) {
|
|
@@ -19,7 +19,7 @@ import { currentSession } from "../core/session.js";
|
|
|
19
19
|
import { noteSessionWait, wakeForArchive } from "../core/archivewait.js";
|
|
20
20
|
import { connectorFor, levelWords, waysOffered, wokenLines } from "../core/archiveasks.js";
|
|
21
21
|
import { claudeHere } from "../cli/menu-parts.js";
|
|
22
|
-
import { acceptConnectors, allows, keepReadings, askLater, claimOf, connectorLevels, grantWay, hostOf, humanPace, keepNetVerdict, keyHost, latchedPace, LEVELS, loadArchives, pendingQuestions, rank, settlePending, revokeWay, setFloor, strictest, takenOverWaiting, windowShown, WAYS, } from "../core/terms.js";
|
|
22
|
+
import { acceptConnectors, allows, keepReadings, askLater, claimOf, connectorLevels, grantWay, hostOf, humanPace, keepNetVerdict, keyHost, latchedPace, LEVELS, loadArchives, pendingQuestions, questionHosts, rank, settlePending, revokeWay, setFloor, strictest, takenOverWaiting, windowShown, WAYS, } from "../core/terms.js";
|
|
23
23
|
/** The images (or pages) of an archive wait for the person's decision: nothing was asked of the archive. */
|
|
24
24
|
export class NeedsPermissionError extends StromError {
|
|
25
25
|
hosts;
|
|
@@ -466,7 +466,10 @@ export function syncTrees(ctx) {
|
|
|
466
466
|
}
|
|
467
467
|
return synced;
|
|
468
468
|
}
|
|
469
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* The hosts a name stands for: a connector's (its name), else the connector hosts on that host or site — never wider,
|
|
471
|
+
* but for the other hosts of one archive whose question waits with it (one archive, one decision).
|
|
472
|
+
*/
|
|
470
473
|
function targetHosts(ctx, target) {
|
|
471
474
|
const all = listConnectors(shared(ctx), ctx.env);
|
|
472
475
|
const byName = all.find((c) => c.name === target);
|
|
@@ -477,6 +480,14 @@ function targetHosts(ctx, target) {
|
|
|
477
480
|
const hosts = [...new Set(connectors.flatMap((c) => c.manifest.hosts.map(bareHost).filter((h) => hostAllowed(h, [given]) || hostAllowed(given, [h]))))];
|
|
478
481
|
if (!hosts.length)
|
|
479
482
|
throw new UsageError(`no connector reaches ${given}`, { hint: all.length ? `connectors: ${all.map((c) => `${c.name} (${c.manifest.hosts.join(", ")})`).join(", ")}` : "none yet: strom connector list" });
|
|
483
|
+
const reached = new Set(connectors.flatMap((c) => c.manifest.hosts.map(bareHost)));
|
|
484
|
+
for (const q of pendingQuestions(ctx.env)) {
|
|
485
|
+
const its = questionHosts(q);
|
|
486
|
+
if (q.kind === "permission" && its.some((h) => hosts.includes(h)))
|
|
487
|
+
for (const h of its)
|
|
488
|
+
if (reached.has(h) && !hosts.includes(h))
|
|
489
|
+
hosts.push(h);
|
|
490
|
+
}
|
|
480
491
|
return { hosts, connectors };
|
|
481
492
|
}
|
|
482
493
|
const WAY_TEXT = { slow: "images slowly, at a person's pace", browser: "images through the browser on this computer", manual: "images by hand only" };
|
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/commands/meta.js
CHANGED
|
@@ -113,7 +113,7 @@ function orientation(ctx) {
|
|
|
113
113
|
// the ways as the menu and the bridge offer them (a site only a browser goes to, a bot check: never slowly)
|
|
114
114
|
const connectors = listConnectors(ctx.settings.shared()?.value, ctx.env);
|
|
115
115
|
const claude = claudeHere(ctx, tree.config);
|
|
116
|
-
base.permissions = asked.map((q) => ({ host: q.host, title: q.title ?? q.host, ...(q.connector ? { connector: q.connector } : {}), since: q.at, tries: q.tries ?? 1, tasks: tasksStanding(tree, q), command: `strom allow archive ${q.host} --way ${waysOffered(connectorFor(connectors, q.host, q.connector), claude, q.way, !!q.browserOnly).join("|")}` }));
|
|
116
|
+
base.permissions = asked.map((q) => ({ host: q.host, ...(q.archiveHosts ? { hosts: q.archiveHosts } : {}), title: q.title ?? q.host, ...(q.connector ? { connector: q.connector } : {}), since: q.at, tries: q.tries ?? 1, tasks: tasksStanding(tree, q), command: `strom allow archive ${q.host} --way ${waysOffered(connectorFor(connectors, q.host, q.connector), claude, q.way, !!q.browserOnly).join("|")}` }));
|
|
117
117
|
}
|
|
118
118
|
const stories = ctx.settings.stories(tree.config);
|
|
119
119
|
base.stories = !stories.on ? "off" : stories.said || told.stories ? "on" : "tell";
|
package/dist/core/archiveasks.js
CHANGED
|
@@ -7,7 +7,7 @@ import { actsAsArchive } from "./state.js";
|
|
|
7
7
|
import { waitsOnHost } from "./archivewait.js";
|
|
8
8
|
import { waitsForDecision } from "./queue.js";
|
|
9
9
|
export { savedInInbox } from "./archivewait.js";
|
|
10
|
-
import { claimOf, pendingQuestions, termsFor } from "./terms.js";
|
|
10
|
+
import { claimOf, pendingQuestions, questionHosts, termsFor } from "./terms.js";
|
|
11
11
|
import { bareHost, listConnectors, routesOf } from "./connector.js";
|
|
12
12
|
import { ui } from "../cli/ui.js";
|
|
13
13
|
import { humanDay } from "../cli/human.js";
|
|
@@ -52,7 +52,7 @@ export function tasksWaitingOn(tree, host) {
|
|
|
52
52
|
* those the queue holds back for it, and those a fetch needed it for (still to be done) — their IDs.
|
|
53
53
|
*/
|
|
54
54
|
export function tasksStanding(tree, q) {
|
|
55
|
-
const ids = new Set(tasksWaitingOn(tree,
|
|
55
|
+
const ids = new Set(questionHosts(q).flatMap((h) => tasksWaitingOn(tree, h).map((t) => t.id)));
|
|
56
56
|
const held = waitsForDecision(tree, [q]);
|
|
57
57
|
for (const t of tree.list("task"))
|
|
58
58
|
if (t.state === "open" && held(t))
|
|
@@ -169,12 +169,13 @@ export function archiveWaitingItems(tree, shared, claude) {
|
|
|
169
169
|
{
|
|
170
170
|
kind: "permission",
|
|
171
171
|
id: q.key,
|
|
172
|
-
what: ui(lang, "ui.archive.item", { title, host: q.
|
|
172
|
+
what: ui(lang, "ui.archive.item", { title, host: questionHosts(q).join(", ") }),
|
|
173
173
|
on: termsText(lang, tree.env, q),
|
|
174
174
|
at: q.at,
|
|
175
175
|
since: q.at,
|
|
176
176
|
tries: q.tries ?? 1,
|
|
177
177
|
host: q.host,
|
|
178
|
+
...(q.archiveHosts ? { hosts: q.archiveHosts } : {}),
|
|
178
179
|
title,
|
|
179
180
|
...(q.terms ? { terms: q.terms } : {}),
|
|
180
181
|
...(q.quote ? { quote: q.quote } : {}),
|
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/terms.js
CHANGED
|
@@ -186,12 +186,75 @@ export function updateArchives(env, change) {
|
|
|
186
186
|
* know (a list grown since covers more) — none left, no question.
|
|
187
187
|
*/
|
|
188
188
|
export function pendingQuestions(env) {
|
|
189
|
-
return loadArchives(env).pending.flatMap((q) => {
|
|
189
|
+
return oneArchive(loadArchives(env).pending.flatMap((q) => {
|
|
190
190
|
if (q.kind !== "takeover" || !q.hosts)
|
|
191
191
|
return [q];
|
|
192
192
|
const left = (q.connectors ?? []).filter((n) => !q.hosts?.[n] || !coveredByTerms(env, q.hosts[n]));
|
|
193
193
|
return left.length ? [{ ...q, connectors: left }] : [];
|
|
194
|
-
});
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
/** The hosts a question is for: those of its archive, else its own. */
|
|
197
|
+
export function questionHosts(q) {
|
|
198
|
+
return q.archiveHosts?.length ? q.archiveHosts : q.host ? [q.host] : [];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The questions about the images of one archive — its hosts (the site's pages, its image servers) asked one by one,
|
|
202
|
+
* the same connector, site and terms — read as one: since the first time asked, the tasks of them all (each once), the
|
|
203
|
+
* ways every host may go. Each host keeps its own question in the file (an older strom reads them as before).
|
|
204
|
+
*/
|
|
205
|
+
function oneArchive(qs) {
|
|
206
|
+
const groups = new Map();
|
|
207
|
+
const out = [];
|
|
208
|
+
for (const q of qs) {
|
|
209
|
+
if (q.kind !== "permission" || !q.host) {
|
|
210
|
+
out.push(q);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const key = JSON.stringify([q.connector ?? "", webDomain(q.host), q.terms ?? ""]);
|
|
214
|
+
const g = groups.get(key);
|
|
215
|
+
if (g)
|
|
216
|
+
g.push(q);
|
|
217
|
+
else {
|
|
218
|
+
const fresh = [q];
|
|
219
|
+
groups.set(key, fresh);
|
|
220
|
+
out.push(fresh);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out.map((x) => (Array.isArray(x) ? (x.length === 1 ? x[0] : merged(x)) : x));
|
|
224
|
+
}
|
|
225
|
+
function merged(qs) {
|
|
226
|
+
const all = [...qs].sort((a, b) => a.at.localeCompare(b.at));
|
|
227
|
+
const first = all[0];
|
|
228
|
+
const latest = (f) => all.map(f).filter((x) => !!x).sort().at(-1);
|
|
229
|
+
const tasks = [];
|
|
230
|
+
for (const q of all)
|
|
231
|
+
for (const t of q.tasks ?? (q.tree && q.task ? [{ tree: q.tree, task: q.task }] : []))
|
|
232
|
+
if (!tasks.some((x) => x.tree === t.tree && x.task === t.task))
|
|
233
|
+
tasks.push(t);
|
|
234
|
+
const lastAsked = [...all].sort((a, b) => (a.last ?? a.at).localeCompare(b.last ?? b.at)).at(-1);
|
|
235
|
+
const browserOnly = all.some((q) => q.browserOnly);
|
|
236
|
+
const choices = all.every((q) => q.choices) ? first.choices.filter((w) => all.every((q) => q.choices.includes(w)) && !(browserOnly && w === "slow")) : undefined;
|
|
237
|
+
const title = all.find((q) => q.title)?.title;
|
|
238
|
+
const level = all.some((q) => q.level) ? strictest(...all.map((q) => q.level)) : undefined;
|
|
239
|
+
const way = browserOnly ? "browser" : first.way;
|
|
240
|
+
const window = latest((q) => q.window);
|
|
241
|
+
const last = latest((q) => q.last);
|
|
242
|
+
return {
|
|
243
|
+
...first,
|
|
244
|
+
archiveHosts: [...new Set(all.map((q) => q.host))],
|
|
245
|
+
...(title ? { title } : {}),
|
|
246
|
+
...(level ? { level } : {}),
|
|
247
|
+
...(way ? { way } : {}),
|
|
248
|
+
...(choices ? { choices: choices.includes("manual") ? choices : [...choices, "manual"] } : {}),
|
|
249
|
+
...(browserOnly ? { browserOnly } : {}),
|
|
250
|
+
...(window ? { window } : {}),
|
|
251
|
+
...(lastAsked.tree ? { tree: lastAsked.tree } : {}),
|
|
252
|
+
...(lastAsked.task ? { task: lastAsked.task } : {}),
|
|
253
|
+
...(tasks.length ? { tasks } : {}),
|
|
254
|
+
// one command asks all the archive's hosts at once: the most any host was needed
|
|
255
|
+
tries: Math.max(...all.map((q) => q.tries ?? 1)),
|
|
256
|
+
...(last && last > first.at ? { last } : {}),
|
|
257
|
+
};
|
|
195
258
|
}
|
|
196
259
|
export function dropPending(s, key) {
|
|
197
260
|
s.pending = s.pending.filter((p) => p.key !== key);
|
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.4";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|