strom-research 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -164,6 +164,7 @@
164
164
  "ui.why.session": "ve frontě je úkolů: {n}",
165
165
  "ui.why.frontier": "žádné otevřené úkoly – podívej se, co výzkumu ještě chybí",
166
166
  "ui.wait.title": "čeká na vás ({n})",
167
+ "ui.wait.stories": "nové verze vyprávění, která jste schválili – přečíst a rozhodnout: {ids} (menu: Co čeká na vás · strom story show <ID>)",
167
168
  "ui.wait.images": "snímky {images} z {book}",
168
169
  "ui.wait.save": "uložte je do {dir}",
169
170
  "ui.wait.how1": "snímky patří do schránky (inbox), pro každou knihu vlastní složka, každý pojmenovaný číslem snímku, jak ho počítá prohlížeč",
@@ -266,6 +267,10 @@
266
267
  "story.where": "zapsané údaje (strom person show {id})",
267
268
  "story.why": "{count} údaje ze zápisů vyprávějí život (generace {generation} výzkumu {research}); rodina si ho přečte v rodinné knize aplikace Strom",
268
269
  "story.done": "strom story set {id} se všemi údaji, o které se opírá (--fact) – koncept, dokud ho uživatel neschválí",
270
+ "story.sources.what": "Mimo matriky, pro vyprávění: {name} – noviny, adresáře, vojenské, pozemkové a soudní záznamy, hroby, dějiny místa",
271
+ "story.sources.where": "digitalizované noviny, knihy a archivy země a doby; web (rejstříky a rodokmeny jsou jen vodítko)",
272
+ "story.sources.why": "vyprávění má říct víc než matriky – a každé slovo pořád na záznamu; potom strom navrhne vyprávění",
273
+ "story.sources.done": "každý nález zapsaný jako pramen a údaj s citací (osoba doložená víc než jménem), dějiny místa jako pramen, každé hledání zapsané – i marné",
269
274
  "review.research": "Revize: {name}",
270
275
  "review.unproven.research": "Osoby bez vlastního záznamu",
271
276
  "review.more": " (a po nich dalších {count})",
@@ -483,6 +488,9 @@
483
488
  "ui.sync.fact.changed": "{n}. {who}: {kind} změněno — {was} → {now}",
484
489
  "ui.sync.fact.differs": "{n}. {who}: {kind} se liší — výzkum {was}, soubor {now}",
485
490
  "ui.sync.fact.gone": "{n}. {who}: už v něm není — {fact}",
491
+ "ui.sync.fact.detail": "{n}. {who}: {kind} {when} — {was} → {now}",
492
+ "ui.sync.house": "čp. {x}",
493
+ "ui.sync.cause": "příčina {x}",
486
494
  "ui.sync.name.new": "{n}. {who}: další jméno — {text}",
487
495
  "ui.sync.sex.changed": "{n}. {who}: pohlaví {text}",
488
496
  "ui.sync.note.new": "{n}. {who}: poznámka — „{text}“",
@@ -494,6 +502,7 @@
494
502
  "ui.sync.place.located": "{n}. {place}: poloha na mapě {at}",
495
503
  "ui.sync.do.add": "přidá se jako vodítko",
496
504
  "ui.sync.do.correct": "opraví vodítko výzkumu",
505
+ "ui.sync.do.detail": "doplní se k údaji s odkazem na váš strom",
497
506
  "ui.sync.do.conflict": "rozpor k rozhodnutí (výzkum to má ze záznamu)",
498
507
  "ui.sync.do.user": "platí vaše úprava (údaj ze záznamu se stáhne s důvodem)",
499
508
  "ui.sync.do.set": "výzkum převezme vaši polohu (žádný zápis ji nedokládá)",
@@ -666,8 +675,8 @@
666
675
  "ui.link.conflict.by": "{why} (rozhodnutí uživatele)",
667
676
  "ui.link.conflict.done": "✓ Rozpor je rozhodnutý.",
668
677
  "ui.link.conflict.say": "Podívej se na rozpor {id} ({title}): zvaž prameny, rozhodni ho přes strom conflict resolve a pak mi řekni, k čemu jsi došel a proč.",
669
- "ui.link.story.none": "Osoba {person} zatím nemá vyprávění. Nic se nestalo.",
670
- "ui.link.story.final": "Vyprávění osoby {person} už je schválené.",
678
+ "ui.link.story.none": "Vyprávění o {person} zatím není. Nic se nestalo.",
679
+ "ui.link.story.final": "Vyprávění o {person} už je schválené.",
671
680
  "ui.link.what.story": "schválit vyprávění osoby {person}, jak je (už nebude návrh).",
672
681
  "ui.link.story.done": "✓ Vyprávění je schválené. Až výzkum v aplikaci Strom otevřete znovu, bude tam bez štítku Návrh.",
673
682
  "ui.link.sync.none": "{input} není v tomto výzkumu poslání z aplikace Strom. Nic se nestalo.",
@@ -820,6 +829,7 @@
820
829
  "log.repository.edit": "Upravený archiv: {name}",
821
830
  "log.story.set": "Vyprávění: {who}",
822
831
  "log.story.approve": "Vyprávění schváleno: {who}",
832
+ "log.story.discard": "Nová verze vyprávění nechtěná, zůstává schválená: {who}",
823
833
  "log.note.add": "Poznámka: {name}",
824
834
  "log.input.add": "Materiál od rodiny: {name}",
825
835
  "log.input.sync": "Převzatý rodokmen: {name}",
@@ -902,6 +912,17 @@
902
912
  "ui.review.next.human": "Agent se do nich pustí v rozhovoru nebo při samostatné práci.",
903
913
  "ui.review.wake.human": " pokud v něm chcete pokračovat, řekněte to agentovi v rozhovoru",
904
914
  "ui.waiting.gone": "Tohle už na vás nečeká – agent se toho mezitím ujal.",
915
+ "ui.link.story.nonew": "Žádná nová verze vyprávění o {person} nečeká. Nic se nestalo.",
916
+ "ui.link.story.nocouple": "{couple} nejsou ve výzkumu pár. Nic se nestalo.",
917
+ "ui.story.new.title": "Nová verze vyprávění o {who} ({day}) – schválené zůstává, dokud nerozhodnete:",
918
+ "ui.story.new.pick": "Které vyprávění má mít rodinná kniha?",
919
+ "ui.story.new.take": "novou verzi, místo staré",
920
+ "ui.story.new.keep": "nechat staré (nová verze se zahodí)",
921
+ "ui.story.new.taken": "✓ Vyprávěním je teď nová verze, schválená. Otevřete výzkum v aplikaci Strom znovu a je tam.",
922
+ "ui.story.new.kept": "✓ Schválené vyprávění zůstává, nová verze je pryč (historie výzkumu ji drží).",
923
+ "ui.waiting.story": "Nová verze vyprávění o {who} – schválené zůstává, dokud nerozhodnete",
924
+ "ui.waiting.story.read": "Přečíst nové vyprávění o {who} a rozhodnout",
925
+ "ui.waiting.pick.story": "Vyberte číslo:",
905
926
  "ui.trees.pack": "zabalit „{name}“ a poslat někomu (soubor ZIP)",
906
927
  "ui.trees.unpack": "rodokmen, který vám někdo poslal (soubor ZIP)",
907
928
  "ui.pack.which": "Které snímky přibalit?",
@@ -164,6 +164,7 @@
164
164
  "ui.why.session": "Aufgaben in der Warteschlange: {n}",
165
165
  "ui.why.frontier": "keine offenen Aufgaben – sieh nach, was der Forschung noch fehlt",
166
166
  "ui.wait.title": "wartet auf Sie ({n})",
167
+ "ui.wait.stories": "neue Fassungen von Erzählungen, die Sie freigegeben haben – lesen und entscheiden: {ids} (das Menü: Was auf Sie wartet · strom story show <ID>)",
167
168
  "ui.wait.images": "Scans {images} aus {book}",
168
169
  "ui.wait.save": "speichern Sie sie in {dir}",
169
170
  "ui.wait.how1": "Scans kommen in den Eingang (inbox), ein Ordner pro Buch, jeder benannt nach der Bildnummer, wie der Viewer sie zählt",
@@ -266,6 +267,10 @@
266
267
  "story.where": "die erfassten Angaben (strom person show {id})",
267
268
  "story.why": "{count} Angaben aus Einträgen erzählen das Leben (Generation {generation} der Forschung {research}); die Familie liest es im Familienbuch der Strom-App",
268
269
  "story.done": "strom story set {id} mit jeder Angabe, auf der sie beruht (--fact) – ein Entwurf, bis der Benutzer sie freigibt",
270
+ "story.sources.what": "Über die Kirchenbücher hinaus, für die Geschichte: {name} – Zeitungen, Adressbücher, Militär-, Grund- und Gerichtsakten, Gräber, die Geschichte des Ortes",
271
+ "story.sources.where": "die digitalisierten Zeitungen, Bücher und Archive des Landes und der Zeit; das Web (Register und Stammbäume nur als Hinweis)",
272
+ "story.sources.why": "die Geschichte soll mehr erzählen als die Kirchenbücher – jedes Wort weiterhin auf einer Quelle; danach schlägt strom die Geschichte vor",
273
+ "story.sources.done": "jeder Fund als Quelle und Angabe mit Beleg erfasst (die Person durch mehr als den Namen belegt), die Geschichte des Ortes als Quelle, jede Suche erfasst – auch vergebliche",
269
274
  "review.research": "Durchsicht: {name}",
270
275
  "review.unproven.research": "Personen ohne eigenen Eintrag",
271
276
  "review.more": " (und danach {count} weitere)",
@@ -483,6 +488,9 @@
483
488
  "ui.sync.fact.changed": "{n}. {who}: {kind} geändert — {was} → {now}",
484
489
  "ui.sync.fact.differs": "{n}. {who}: {kind} weicht ab — Forschung {was}, Datei {now}",
485
490
  "ui.sync.fact.gone": "{n}. {who}: nicht mehr darin — {fact}",
491
+ "ui.sync.fact.detail": "{n}. {who}: {kind} {when} — {was} → {now}",
492
+ "ui.sync.house": "Haus Nr. {x}",
493
+ "ui.sync.cause": "Ursache {x}",
486
494
  "ui.sync.name.new": "{n}. {who}: ein weiterer Name — {text}",
487
495
  "ui.sync.sex.changed": "{n}. {who}: Geschlecht {text}",
488
496
  "ui.sync.note.new": "{n}. {who}: eine Notiz — „{text}“",
@@ -494,6 +502,7 @@
494
502
  "ui.sync.place.located": "{n}. {place}: Lage auf der Karte {at}",
495
503
  "ui.sync.do.add": "als Hinweis übernommen",
496
504
  "ui.sync.do.correct": "berichtigt den Hinweis der Forschung",
505
+ "ui.sync.do.detail": "wird der Angabe hinzugefügt, mit Verweis auf Ihren Stammbaum",
497
506
  "ui.sync.do.conflict": "ein Widerspruch zum Entscheiden (die Forschung hat es aus einem Eintrag)",
498
507
  "ui.sync.do.user": "Ihre Änderung gilt (die Angabe aus dem Eintrag wird mit Grund zurückgezogen)",
499
508
  "ui.sync.do.set": "die Forschung übernimmt Ihre Lage (kein Eintrag belegt sie)",
@@ -820,6 +829,7 @@
820
829
  "log.repository.edit": "Archiv geändert: {name}",
821
830
  "log.story.set": "Erzählung: {who}",
822
831
  "log.story.approve": "Erzählung freigegeben: {who}",
832
+ "log.story.discard": "Die neue Fassung der Erzählung nicht gewollt, die freigegebene bleibt: {who}",
823
833
  "log.note.add": "Notiz: {name}",
824
834
  "log.input.add": "Material der Familie: {name}",
825
835
  "log.input.sync": "Stammbaum übernommen: {name}",
@@ -902,6 +912,17 @@
902
912
  "ui.review.next.human": "Der Agent nimmt sie im Gespräch oder bei der Arbeit allein auf.",
903
913
  "ui.review.wake.human": " wenn es weitergehen soll, sagen Sie es dem Agenten im Gespräch",
904
914
  "ui.waiting.gone": "Das wartet nicht mehr auf Sie – der Agent hat es inzwischen übernommen.",
915
+ "ui.link.story.nonew": "Keine neue Fassung der Erzählung über {person} wartet. Es wurde nichts getan.",
916
+ "ui.link.story.nocouple": "{couple} sind in der Forschung kein Paar. Es wurde nichts getan.",
917
+ "ui.story.new.title": "Die neue Fassung der Erzählung über {who} ({day}) – die freigegebene bleibt, bis Sie entscheiden:",
918
+ "ui.story.new.pick": "Welche Erzählung soll das Familienbuch haben?",
919
+ "ui.story.new.take": "die neue Fassung, anstelle der alten",
920
+ "ui.story.new.keep": "die alte behalten (die neue Fassung entfällt)",
921
+ "ui.story.new.taken": "✓ Die neue Fassung ist jetzt die Erzählung, freigegeben. Öffnen Sie die Forschung in der Strom-App erneut, dann ist sie da.",
922
+ "ui.story.new.kept": "✓ Die freigegebene Erzählung bleibt; die neue Fassung ist weg (die Geschichte der Forschung bewahrt sie).",
923
+ "ui.waiting.story": "Eine neue Fassung der Erzählung über {who} – die freigegebene bleibt, bis Sie entscheiden",
924
+ "ui.waiting.story.read": "Die neue Erzählung über {who} lesen und entscheiden",
925
+ "ui.waiting.pick.story": "Wählen Sie die Nummer:",
905
926
  "ui.trees.pack": "„{name}“ einpacken und jemandem schicken (eine ZIP-Datei)",
906
927
  "ui.trees.unpack": "ein Stammbaum, den Ihnen jemand geschickt hat (eine ZIP-Datei)",
907
928
  "ui.pack.which": "Welche Bilder kommen mit?",
@@ -37,6 +37,14 @@ conclusion so the next researcher can follow it.
37
37
  - **Lessons belong where they apply.** A quirk of a register (its calibration,
38
38
  two years per page, a hand that writes 7 like 1) goes to `strom lesson add
39
39
  --on B…`, so whoever opens that book next sees it.
40
+ - **The tree first, then above it.** A family found in the records that no
41
+ family link joins to the tree is not researched further — not its parents,
42
+ not its children — until the link is proven, or the user asks for it. First
43
+ the hypothesis that would join it (`strom hypothesis add … --about` a person
44
+ of the tree), then a task that tests it and names it (`--about H…`). The
45
+ queue holds back every other task about people off the tree
46
+ (`strom task list --off-tree`); they come back by themselves once the link
47
+ is recorded.
40
48
  - **Stay within the task.** New questions become new tasks (`strom task add`,
41
49
  with where and done-when), not detours. A new task whose images are not here
42
50
  gets them now: through the archive's connector (built first when it has
@@ -9,3 +9,34 @@ the village. Each detail is a fact with a citation.
9
9
  - Keep the certainty honest: a house number from a baptism of a sibling is
10
10
  evidence about the family at that date, not about the person's whole life.
11
11
  - Record what you learn about the place and the books as lessons.
12
+
13
+ ## Beyond the registers
14
+
15
+ Before a story is written (the task says so), look where the registers do
16
+ not: newspapers (notices, obituaries, court reports, auctions, lists of
17
+ donors, soldiers), directories and address books, school and society reports,
18
+ military, land, court and notarial records, graves and death notices, and the
19
+ history of the place. Which of them exist for that country and time, and
20
+ where they are digitised, you find out first — every search recorded, also in
21
+ vain (`strom search add … --method web|catalog|full-text --result negative`).
22
+
23
+ What you find must hold as evidence:
24
+
25
+ - **Read the source itself** — the page of the newspaper, the scan of the
26
+ book or the record. A search snippet, an OCR line, an index, an online tree
27
+ or someone's summary is a lead: it says where to look, never a fact.
28
+ - **Your person, not a namesake.** A hit is them only when the name, the place
29
+ and the time agree and at least one more detail does: the house, the
30
+ occupation, the spouse, the age, a relative named. Otherwise it is a
31
+ hypothesis (`strom hypothesis add`) with what would decide it.
32
+ - **Cite it exactly**: the source with its kind (newspaper, book, military,
33
+ land, court, web), `--url`, `--locator "<page, column>"` and its words in
34
+ `--transcript`; then the fact with `--cite S… --quote "…"`. A newspaper written at the time is
35
+ a good witness of what it reports, a later book or website a weaker one:
36
+ say so in the fact's certainty.
37
+ - **The history of the place is not a fact of the person.** A war, an
38
+ epidemic, a fire, a change of lords, what the village lived from: record
39
+ the work that says it as a source (`--kind book|web|newspaper`, what it says
40
+ in `--transcript`) — the story tells it as background, citing it.
41
+ - Images of an archive or a library come through a connector or the user, as
42
+ everywhere (never your own download).
@@ -5,6 +5,10 @@ A story is written from the facts, for the family.
5
5
  - Every statement rests on a recorded fact; say which ones.
6
6
  - What is inferred is marked as inference; what is unknown is said to be
7
7
  unknown. Never fill gaps with plausible fiction.
8
+ - The background of place and time (a war, an epidemic, a fire, what the
9
+ village lived from) only from a source recorded in the research, named with
10
+ `--source S…`, and told as background: "in 1866 the war passed through the
11
+ region" — never as the cause of what the person did, unless a record says so.
8
12
  - Use the research language and plain words; explain old occupations and
9
13
  terms the reader will not know.
10
14
  - Format: paragraphs separated by a blank line; within them only "## "
@@ -12,7 +16,11 @@ A story is written from the facts, for the family.
12
16
  anything else (links, tables, quotes, numbered lists) as plain text. The
13
17
  title goes in --title, not as a "# " line in the text.
14
18
  - Write it to a file in notes/, then `strom story set P… --text @notes/story-P….md
15
- --title "…" --fact E… --fact E… --note "what is inferred"`: every fact it leans on goes
16
- in --fact. A couple's story goes on the family (F…).
19
+ --title "…" --fact E… --fact E… --source S… --note "what is inferred"`: every fact it leans on goes
20
+ in --fact, every source of its background in --source. A couple's story goes on the family (F…).
17
21
  - It stays a draft; when the user says it is right: `strom story approve P…`
18
- (the Strom app asks them too).
22
+ (the Strom app asks them too). An approved story is locked — the user liked
23
+ it: written again (more facts, a better reading), the new version waits
24
+ beside it and the approved one stays until the user decides. Tell them what
25
+ is new in it; they approve it (`strom story approve P…`) or keep the old one
26
+ (`strom story discard P…`) — only on their word.
@@ -143,12 +143,21 @@ export async function decideConflict(ctx, run, lang, root, id) {
143
143
  out(ctx, t("ui.link.conflict.done"));
144
144
  return true;
145
145
  }
146
- /** A story the person read and approves: no longer a draft. */
147
- export async function approveStory(ctx, run, lang, root, person, who) {
146
+ /**
147
+ * A story the person read and approves: no longer a draft, locked. Beside an approved one, its new version: read and
148
+ * approved in its place, or not wanted (keep: the approved one stays) — as the menu's "What waits for you".
149
+ */
150
+ export async function approveStory(ctx, run, lang, root, person, who, how = "final") {
148
151
  const t = translator(lang);
149
152
  const st = person.story;
150
153
  if (!st)
151
154
  return stop(ctx, t("ui.link.story.none", { person: who }));
155
+ if (st.draft) {
156
+ const { decideStory } = await import("./menu-waiting.js");
157
+ return decideStory(ctx, run, lang, root, person.id, who, how);
158
+ }
159
+ if (how === "keep")
160
+ return stop(ctx, t("ui.link.story.nonew", { person: who }));
152
161
  if (st.status === "final")
153
162
  return stop(ctx, t("ui.link.story.final", { person: who }));
154
163
  out(ctx, lines(st.title ? `„${st.title}“` : undefined, "", st.text.replace(/\*\*/g, ""), ""));
@@ -159,6 +168,16 @@ export async function approveStory(ctx, run, lang, root, person, who) {
159
168
  out(ctx, t("ui.link.story.done"));
160
169
  return true;
161
170
  }
171
+ /**
172
+ * The family whose story a link of the Strom app means by its two partners (the app knows no family IDs): of several,
173
+ * the one with a new version waiting, then one with a story, then the first.
174
+ */
175
+ export function coupleStory(tree, a, b) {
176
+ if (a === b)
177
+ return undefined;
178
+ const theirs = tree.list("family").filter((f) => !f.retracted && f.partners.includes(a) && f.partners.includes(b));
179
+ return theirs.find((f) => f.story?.draft) ?? theirs.find((f) => f.story) ?? theirs[0];
180
+ }
162
181
  /** What the research took from the Strom app, taken back on the person's word. */
163
182
  export async function undoSending(ctx, run, lang, root, id) {
164
183
  const t = translator(lang);
@@ -7,6 +7,10 @@ import { subMenu, translator } from "./menu-parts.js";
7
7
  import { offerChat } from "./menu-research.js";
8
8
  import { truncate } from "./format.js";
9
9
  import { Tree } from "../core/tree.js";
10
+ import { storiesToApprove } from "../core/stories.js";
11
+ import { displayName, lifespan } from "../core/people.js";
12
+ /** A person for the person reading: "Jan Novák (*1905) [P0001]". */
13
+ const label = (p) => `${displayName(p)}${lifespan(p) ? ` (${lifespan(p)})` : ""} [${p.id}]`;
10
14
  import { openForUser } from "../core/open.js";
11
15
  /** At most this many are listed to answer (the menu's nine); the rest after them. */
12
16
  const SHOWN = 9;
@@ -67,27 +71,81 @@ export async function waitingTask(ctx, run, lang, root, id) {
67
71
  await takeUp(ctx, run, lang, root, x);
68
72
  return true;
69
73
  }
74
+ /** Whose story it is, for the person: "Jan Novák (*1905) [P0001]", a couple by both. */
75
+ function storyOf(tree, id) {
76
+ const r = tree.get(id);
77
+ if (r?.type === "person")
78
+ return label(r);
79
+ if (r?.type === "family")
80
+ return r.partners
81
+ .map((p) => tree.get(p))
82
+ .filter((p) => !!p)
83
+ .map(label)
84
+ .join(" & ");
85
+ return id;
86
+ }
87
+ /**
88
+ * The new version of a story the person approved before (the lock): read, then it takes the old one's place, or the
89
+ * old one stays. `how` (a link of the Strom app): what was asked for there — still asked here, with the text before it.
90
+ */
91
+ export async function decideStory(ctx, run, lang, root, id, who, how) {
92
+ const t = translator(lang);
93
+ const out = (line) => ctx.io.stdout(line + "\n");
94
+ const draft = Tree.open(root, ctx.env).get(id)?.story?.draft;
95
+ if (!draft) {
96
+ out(t("ui.waiting.gone"));
97
+ return false;
98
+ }
99
+ out("");
100
+ out(t("ui.story.new.title", { who, day: draft.at.slice(0, 10) }));
101
+ out("");
102
+ if (draft.title)
103
+ out(`„${draft.title}“`);
104
+ out(draft.text.replace(/\*\*/g, ""));
105
+ if (draft.note)
106
+ out(`\n(${draft.note})`);
107
+ const i = await ctx.choose(t("ui.story.new.pick"), [{ label: t("ui.story.new.take") }, { label: t("ui.story.new.keep") }], how === "keep" ? 1 : 0, { back: t("ui.browse.back") });
108
+ if (i === undefined)
109
+ return false;
110
+ if ((await run(["story", i === 0 ? "approve" : "discard", id], true)) !== 0)
111
+ return false;
112
+ out(t(i === 0 ? "ui.story.new.taken" : "ui.story.new.kept"));
113
+ return true;
114
+ }
70
115
  export async function waitingForYou(ctx, run, lang, root) {
71
116
  const t = translator(lang);
72
117
  await subMenu(ctx, lang, () => {
73
118
  const tree = Tree.open(root, ctx.env);
74
119
  const waiting = tree.list("task").filter((x) => x.state === "waiting");
75
- if (!waiting.length)
120
+ const stories = storiesToApprove(tree);
121
+ if (!waiting.length && !stories.length)
76
122
  return { title: t("ui.waiting.none"), items: [] };
123
+ // the tasks first, then the stories: at most the menu's nine together
77
124
  const shown = waiting.slice(0, SHOWN);
78
- const text = [t("ui.waiting.title", { n: waiting.length })];
125
+ const shownStories = stories.slice(0, SHOWN - shown.length);
126
+ const text = [t("ui.waiting.title", { n: waiting.length + stories.length })];
79
127
  for (const [k, x] of shown.entries()) {
80
128
  text.push("", ` ${k + 1}. ${x.what}`, ...whatToDo(ctx, lang, tree, x));
81
129
  }
130
+ for (const [k, s] of shownStories.entries())
131
+ text.push("", ` ${shown.length + k + 1}. ${t("ui.waiting.story", { who: storyOf(tree, s.id) })}`);
82
132
  if (shown.some((x) => x.awaits))
83
133
  text.push("", t("ui.wait.how1"), t("ui.wait.how2"));
84
- if (waiting.length > SHOWN)
85
- text.push("", t("ui.waiting.more", { n: waiting.length - SHOWN }));
86
- const items = shown.map((x, k) => ({
87
- key: String(k + 1),
88
- label: x.awaits ? t("ui.waiting.folder", { k: k + 1 }) : t("ui.waiting.answer", { k: k + 1, what: truncate(x.what, 60) }),
89
- act: () => takeUp(ctx, run, lang, root, x),
90
- }));
91
- return { title: `${text.join("\n")}\n\n${t("ui.waiting.pick")}`, items };
134
+ const hidden = waiting.length + stories.length - shown.length - shownStories.length;
135
+ if (hidden > 0)
136
+ text.push("", t("ui.waiting.more", { n: hidden }));
137
+ const items = [
138
+ ...shown.map((x, k) => ({
139
+ key: String(k + 1),
140
+ label: x.awaits ? t("ui.waiting.folder", { k: k + 1 }) : t("ui.waiting.answer", { k: k + 1, what: truncate(x.what, 60) }),
141
+ act: () => takeUp(ctx, run, lang, root, x),
142
+ })),
143
+ ...shownStories.map((s, k) => ({
144
+ key: String(shown.length + k + 1),
145
+ label: t("ui.waiting.story.read", { k: shown.length + k + 1, who: storyOf(tree, s.id) }),
146
+ act: async () => void (await decideStory(ctx, run, lang, root, s.id, storyOf(tree, s.id))),
147
+ })),
148
+ ];
149
+ return { title: `${text.join("\n")}\n\n${t(shown.length ? "ui.waiting.pick" : "ui.waiting.pick.story")}`, items };
92
150
  });
93
151
  }
package/dist/cli/menu.js CHANGED
@@ -36,6 +36,7 @@ import { mainPerson } from "../core/kin.js";
36
36
  import { AGENTS, findAgent } from "../core/which.js";
37
37
  import { agentsHere } from "../core/apps.js";
38
38
  import { PROFILES } from "../agents/profiles.js";
39
+ import { forStory, storiesToApprove } from "../core/stories.js";
39
40
  /** Create a family tree and say so in the user's words (init itself talks to agents). */
40
41
  async function createTree(ctx, run, lang, name) {
41
42
  const before = new Set(ctx.knownTrees().map((k) => k.root));
@@ -99,7 +100,8 @@ export async function runMenu(ctx, run) {
99
100
  const newer = await newerVersion(ctx.settings, ctx.env);
100
101
  if (root) {
101
102
  const tree = Tree.open(root, ctx.env);
102
- const waiting = waitingForUser(tree).length;
103
+ // the tasks that wait for the person, and the new versions of the stories they approved
104
+ const waiting = waitingForUser(tree).length + storiesToApprove(tree).length;
103
105
  const started = tree.list("research").length > 0;
104
106
  out(t("ui.menu.tree", { name: tree.config.name, persons: tree.count("person") }));
105
107
  // No agent yet (none installed, or not the one chosen): the research waits for it, and item 1 gets one.
@@ -157,7 +159,7 @@ export async function runMenu(ctx, run) {
157
159
  if (shown.length) {
158
160
  out(t("ui.run.next"));
159
161
  for (const [k, x] of shown.entries())
160
- out(` ${String(k + 1).padStart(2)}. ${truncate(humanTask(fresh, x.what, lang), 100)}${x.level === "narrate" ? t("ui.run.story") : ""}${lacksImages(fresh, x) ? t("ui.plan.images") : ""}`);
162
+ out(` ${String(k + 1).padStart(2)}. ${truncate(humanTask(fresh, x.what, lang), 100)}${forStory(x) ? t("ui.run.story") : ""}${lacksImages(fresh, x) ? t("ui.plan.images") : ""}`);
161
163
  if (queue.length > shown.length)
162
164
  out(` ${t("ui.plan.more", { n: queue.length - shown.length })}`);
163
165
  }
package/dist/cli/ui.js CHANGED
@@ -148,6 +148,7 @@ export const UI = {
148
148
  "ui.why.session": "{n} task(s) in the queue",
149
149
  "ui.why.frontier": "no open tasks — see what the research still lacks",
150
150
  "ui.wait.title": "waiting for you ({n})",
151
+ "ui.wait.stories": "new versions of stories you approved, to read and decide: {ids} (the menu: What waits for you · strom story show <ID>)",
151
152
  "ui.wait.images": "images {images} of {book}",
152
153
  "ui.wait.save": "save them into {dir}",
153
154
  "ui.wait.how1": "scans go into the inbox, a folder per book, each named by its image number as the viewer counts it",
@@ -415,6 +416,9 @@ export const UI = {
415
416
  "ui.sync.fact.changed": "{n}. {who}: {kind} changed — {was} → {now}",
416
417
  "ui.sync.fact.differs": "{n}. {who}: {kind} differs — the research {was}, the file {now}",
417
418
  "ui.sync.fact.gone": "{n}. {who}: no longer in it — {fact}",
419
+ "ui.sync.fact.detail": "{n}. {who}: {kind} {when} — {was} → {now}",
420
+ "ui.sync.house": "house {x}",
421
+ "ui.sync.cause": "cause {x}",
418
422
  "ui.sync.name.new": "{n}. {who}: another name — {text}",
419
423
  "ui.sync.sex.changed": "{n}. {who}: sex {text}",
420
424
  "ui.sync.note.new": "{n}. {who}: a note — “{text}”",
@@ -426,6 +430,7 @@ export const UI = {
426
430
  "ui.sync.place.located": "{n}. {place}: on the map at {at}",
427
431
  "ui.sync.do.add": "added as a lead",
428
432
  "ui.sync.do.correct": "corrects the research's lead",
433
+ "ui.sync.do.detail": "added to the fact, citing your tree",
429
434
  "ui.sync.do.conflict": "a conflict for you to decide (the research has it from a record)",
430
435
  "ui.sync.do.user": "your edit wins (the record's fact withdrawn with the reason)",
431
436
  "ui.sync.do.set": "the research takes your position (no record proves one)",
@@ -774,6 +779,17 @@ export const UI = {
774
779
  "ui.review.next.human": "The agent takes them up in a conversation or when it works alone.",
775
780
  "ui.review.wake.human": " to go on with it, tell the agent in a conversation",
776
781
  "ui.waiting.gone": "This one no longer waits for you — the agent has taken it up meanwhile.",
782
+ "ui.link.story.nonew": "No new version of the story of {person} waits. Nothing was done.",
783
+ "ui.link.story.nocouple": "{couple} are no couple of the research. Nothing was done.",
784
+ "ui.story.new.title": "The new version of the story of {who} ({day}) — the one you approved stays until you decide:",
785
+ "ui.story.new.pick": "Which story should the family book have?",
786
+ "ui.story.new.take": "the new version, in place of the old one",
787
+ "ui.story.new.keep": "keep the old one (the new version goes)",
788
+ "ui.story.new.taken": "✓ The new version is the story now, approved. Open the research in the Strom app again and it is there.",
789
+ "ui.story.new.kept": "✓ The story you approved stays; the new version is gone (the history of the research keeps it).",
790
+ "ui.waiting.story": "A new version of the story of {who} — the one you approved stays until you decide",
791
+ "ui.waiting.story.read": "Read the new story of {who} and decide",
792
+ "ui.waiting.pick.story": "Choose the number:",
777
793
  "ui.trees.pack": "pack \"{name}\" to send to someone (a ZIP file)",
778
794
  "ui.trees.unpack": "a family tree someone sent you (a ZIP file)",
779
795
  "ui.pack.which": "Which images go along?",
@@ -13,6 +13,7 @@ import { displayName } from "../core/people.js";
13
13
  import { humanTask } from "../cli/human.js";
14
14
  export { humanTask };
15
15
  import { lacksImages } from "../core/queue.js";
16
+ import { forStory } from "../core/stories.js";
16
17
  import { rankedQueue, waitingForUser } from "./tasks.js";
17
18
  /** Most rows a section of `recent` lists; the rest is counted. */
18
19
  const RECENT_ROWS = 15;
@@ -169,8 +170,9 @@ register({
169
170
  const count = positive(opts.count, "count", 10, 200);
170
171
  // The order of a conversation (the story's turn is only for working alone).
171
172
  const queue = rankedQueue(tree, { strategy: ctx.settings.strategy(tree.config) }).map((r) => r.task);
172
- const work = queue.filter((t) => t.level !== "narrate");
173
- const stories = queue.filter((t) => t.level === "narrate");
173
+ // the stories, with the searches beyond the registers before them
174
+ const work = queue.filter((t) => !forStory(t));
175
+ const stories = queue.filter(forStory);
174
176
  const waiting = waitingForUser(tree).length;
175
177
  const shown = work.slice(0, count);
176
178
  let n = 0;
@@ -195,8 +195,17 @@ async function inTerminal(ctx, link) {
195
195
  return { text: "", data: { done: (await run(["chat", ...(first ? [`--say=${first}`] : [])])) === 0, action: link.action, tree: root } };
196
196
  }
197
197
  case "story": {
198
- const { approveStory } = await import("../cli/menu-links.js");
199
- return end(await approveStory(ctx, run, treeLang, root, person, who(person)));
198
+ const { approveStory, coupleStory } = await import("../cli/menu-links.js");
199
+ if (!link.partner)
200
+ return end(await approveStory(ctx, run, treeLang, root, person, who(person), link.do));
201
+ const partner = personOf(root, ctx, link.partner);
202
+ if (!partner)
203
+ return said("ui.link.noperson", { person: link.partner });
204
+ const couple = `${who(person)} & ${who(partner)}`;
205
+ const family = coupleStory(Tree.open(root, ctx.env), person.id, partner.id);
206
+ if (!family)
207
+ return said("ui.link.story.nocouple", { couple });
208
+ return end(await approveStory(ctx, run, treeLang, root, family, couple, link.do));
200
209
  }
201
210
  case "review": {
202
211
  const what = ui(treeLang, `ui.link.what.review.${link.scope}`, { person: who(person) });
@@ -290,7 +299,7 @@ register({
290
299
  "strom-research://research?tree=<id>&person=P0012&direction=ancestors|descendants — a new direction (research new).\n" +
291
300
  "strom-research://task?…&task=T0007&do=park|drop|wake — the task put aside, given up, back in the queue (the reason asked).\n" +
292
301
  "strom-research://conflict?tree=<id>&id=X0007&do=decide|agent — decided by the user (which claim, why) or left to the agent.\n" +
293
- "strom-research://story?tree=<id>&person=P0012&do=final — the story approved (strom story approve).\n" +
302
+ "strom-research://story?tree=<id>&person=P0012[&partner=P0013]&do=final|keep — the story approved (strom story approve), of a couple with partner=.\n" +
294
303
  "strom-research://sync-undo?tree=<id>&intake=I0042 — a sending from the app taken back (strom sync undo).\n" +
295
304
  "strom-research://direction?tree=<id>&id=G0002&do=pause|done|resume — a direction paused, ended (the reason asked) or\n" +
296
305
  "taken up again (strom research pause|done|resume).\n" +
@@ -13,7 +13,7 @@ import { UsageError } from "../core/errors.js";
13
13
  import { excerptSettings, planExcerpts } from "../core/excerpt.js";
14
14
  import { runGit, tracked } from "../core/git.js";
15
15
  import { Settings } from "../core/config.js";
16
- import { appOpensLinks } from "../core/stromapp.js";
16
+ import { appOpensLinks, appShowsEdges, appShowsStoryDrafts } from "../core/stromapp.js";
17
17
  import { EXCERPT_QUALITIES, EXCERPT_SCOPES } from "../core/model.js";
18
18
  function findingRows(f) {
19
19
  return table(f.slice(0, 30).map((x) => [x.level === "error" ? "ERROR" : "warn", `line ${x.line}`, x.message]));
@@ -73,7 +73,11 @@ export function writeTreeGed(tree, opts) {
73
73
  // an app that opens links shows what the research knows of a person too (the same version)
74
74
  const research = opts.for === "strom" && appOpensLinks(new Settings(tree.env, {}), opts.stromVersion);
75
75
  const clips = research && !!images;
76
- const result = exportGedcom(tree, { for: opts.for, stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}) });
76
+ // …and where the tree ends, for an app that shows it
77
+ const edges = opts.for === "strom" && appShowsEdges(new Settings(tree.env, {}), opts.stromVersion);
78
+ // …and the new versions of stories the user approved, for an app that shows them
79
+ const storyDrafts = opts.for === "strom" && appShowsStoryDrafts(new Settings(tree.env, {}), opts.stromVersion);
80
+ const result = exportGedcom(tree, { for: opts.for, stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}) });
77
81
  const findings = validateGedcom(result.text, opts.for === "standard" ? { strict: true } : {});
78
82
  const file = opts.out ?? gedFile(tree, opts.for, opts.research);
79
83
  if (!hasGedErrors(findings) && !tree.dryRun) {