strom-research 1.23.0-beta.2 → 1.23.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 CHANGED
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
7
7
 
8
8
  ## 1.23.0
9
9
 
10
- *Připravuje se: bety do 1.23.0-beta.2. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.23.0-beta.4. Oddíl bude úplný s vydáním.*
11
11
 
12
12
  - **Osoba ukázaná zvoleným jménem.** Žena narozená pod jedním jménem, provdaná pod druhým a po rozvodu zase u prvního
13
13
  se dá ukazovat tímto jménem znovu, aniž by se přidávalo podruhé: `strom name primary <osoba> "<jméno>"` (a
@@ -20,8 +20,21 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
20
20
  „nikdy `strom repair`“ a `strom repair` vrátil zpět práci zapsanou klíčem terminálu. `strom repair` teď vrací jen
21
21
  to, co se udělalo mimo strom, práce obou klíčů zůstává; potom klíče sjednotí `strom seal adopt` ve vlastním
22
22
  terminálu – `strom check` říká oba kroky v tomto pořadí. Spuštěný z aplikace agenta balené pro Windows nechá `strom
23
- repair` takový výzkum beze změny a odkáže na terminál. Po commitu gitem, který nezměnil žádný záznam, `strom repair`
24
- řekne, že výzkum znovu zapečetil, už ne „nothing to repair“.
23
+ repair` takový výzkum beze změny a odkáže na terminál; odmítnutý zápis tam odkáže na terminál také, už ne nejdřív
24
+ na `strom repair`. Po commitu gitem, který nezměnil žádný záznam, `strom repair` řekne, že výzkum znovu zapečetil,
25
+ už ne „nothing to repair“.
26
+ - **Pečetní klíč i tam, kde ho hledá starší strom (Windows).** Klíč převzatý z kopie aplikace balené pro Windows se
27
+ teď ukládá i do `%APPDATA%\strom\keys`, kde ho hledá strom 1.22.1 nebo starší v terminálu: po návratu ke starší verzi
28
+ zůstává výzkum na tomto počítači zapečetěný. Klíč, který tam už je, se nikdy nepřepíše.
29
+ - **Vyprávění páru schválené z aplikace Strom.** Dotaz v terminálu teď mluví o vyprávění páru, už ne o vyprávění
30
+ osoby.
31
+ - **Vyprávění páru navrhuje výzkum sám.** Jakmile záznamy vyprávějí společný život páru (svatbu nebo dítě a další
32
+ dva údaje) a jeden z dvojice už má vlastní vyprávění, výzkum navrhne vyprávění páru stejně jako vyprávění osoby –
33
+ jen společný život (svatba, domácnost, bydliště, děti, ovdovění); vyprávění partnerů odkáže, neopakuje je.
34
+ Vyprávění osoby napsané potom shrne manželství jednou větou; žádné už napsané vyprávění se kvůli tomu nezkracuje.
35
+ `strom config set stories no` vypne i tato.
36
+ - **`strom repair` jedním jazykem.** Každý jeho řádek („obnoveno“, „není co opravit“ …) je pro člověka v jazyce
37
+ výzkumu; agent je čte anglicky jako dřív.
25
38
 
26
39
  ## 1.22.2
27
40
 
package/CHANGELOG.de.md CHANGED
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
7
7
 
8
8
  ## 1.23.0
9
9
 
10
- *In Vorbereitung: die Betas bis 1.23.0-beta.2. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.23.0-beta.4. Der Abschnitt ist mit dem Release vollständig.*
11
11
 
12
12
  - **Eine Person unter dem gewählten Namen.** Eine Frau, unter einem Namen geboren, unter einem anderen verheiratet und
13
13
  nach einer Scheidung wieder beim ersten, lässt sich wieder unter diesem Namen zeigen, ohne dass er ein zweites Mal
@@ -25,7 +25,22 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
25
25
  check` nennt beide Schritte in dieser Reihenfolge. Aus der für Windows paketierten Desktop-App eines Agenten
26
26
  gestartet, lässt `strom repair` eine solche Forschung unverändert und verweist auf das Terminal. Nach einem Commit
27
27
  mit git, der keinen Eintrag änderte, sagt `strom repair`, dass es die Forschung neu versiegelt hat, nicht mehr
28
- „nothing to repair“.
28
+ „nothing to repair“. Ein dort abgelehnter Schreibvorgang verweist ebenfalls auf das Terminal, nicht mehr zuerst auf
29
+ `strom repair`.
30
+ - **Der Siegelschlüssel auch dort, wo ein älteres strom sucht (Windows).** Ein aus der Kopie einer für Windows
31
+ paketierten App übernommener Schlüssel liegt jetzt auch in `%APPDATA%\strom\keys`, wo ihn strom 1.22.1 oder älter
32
+ im Terminal sucht: zurück bei einer älteren Version bleibt die Forschung auf diesem Computer versiegelt. Ein Schlüssel,
33
+ der dort schon liegt, wird nie ersetzt.
34
+ - **Die Erzählung eines Paares, aus der Strom-App freigegeben.** Die Frage im Terminal nennt jetzt die Erzählung des
35
+ Paares, nicht mehr die einer Person.
36
+ - **Die Erzählung eines Paares, von der Forschung vorgeschlagen.** Sobald Einträge das gemeinsame Leben eines Paares
37
+ erzählen (die Hochzeit oder ein Kind und zwei weitere Angaben) und einer der beiden schon eine eigene Erzählung hat,
38
+ schlägt die Forschung die Erzählung des Paares vor wie die einer Person – nur das gemeinsame Leben (Hochzeit,
39
+ Haushalt, Wohnorte, Kinder, Witwenschaft); auf die Erzählungen der Partner wird verwiesen, sie werden nicht
40
+ wiederholt. Eine danach geschriebene Erzählung einer Person fasst die Ehe in einem Satz zusammen; keine schon
41
+ geschriebene wird dafür gekürzt. `strom config set stories no` schaltet auch diese aus.
42
+ - **`strom repair` in einer Sprache.** Jede Zeile („wiederhergestellt“, „nichts zu reparieren“ …) steht für eine
43
+ Person in der Sprache der Forschung; ein Agent liest sie wie bisher auf Englisch.
29
44
 
30
45
  ## 1.22.2
31
46
 
package/CHANGELOG.md CHANGED
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
7
7
 
8
8
  ## 1.23.0
9
9
 
10
- *In preparation: the betas up to 1.23.0-beta.2. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.23.0-beta.4. The section is complete with the release.*
11
11
 
12
12
  - **A person shown by the name chosen.** A woman born under one name, married under another and back at the first
13
13
  after a divorce can be shown by that name again without it being added twice: `strom name primary <person>
@@ -20,8 +20,22 @@ The changes of each version in short. The full notes and the install lines are o
20
20
  and never `strom repair` at once, and `strom repair` undid the work written with the terminal's key. `strom repair`
21
21
  now undoes only what was done outside strom, the work of both keys stays; then `strom seal adopt` in a terminal
22
22
  unites the keys — `strom check` says the two steps in this order. Started by an agent's desktop app packaged for
23
- Windows, `strom repair` leaves such a research as it is and points to the terminal. After a commit made with git
24
- that changed no record, `strom repair` says it sealed the research again, no longer "nothing to repair".
23
+ Windows, `strom repair` leaves such a research as it is and points to the terminal; a write refused there points to
24
+ the terminal too, no longer to `strom repair` first. After a commit made with git that changed no record, `strom
25
+ repair` says it sealed the research again, no longer "nothing to repair".
26
+ - **The seal key also where an older strom looks (Windows).** A key taken over from the copy of an app packaged for
27
+ Windows is now kept in `%APPDATA%\strom\keys` too, where a terminal's strom 1.22.1 or older looks for it: back on an
28
+ older version, the research is still sealed on this computer. A key there already is never replaced.
29
+ - **A couple's story approved from the Strom app.** The question in the terminal now names the story of the couple,
30
+ no longer of a person.
31
+ - **A couple's story proposed by the research.** Once records tell a couple's life together (their wedding or a
32
+ child, and two more facts) and one of the two has a story of their own, the research proposes the couple's story
33
+ as it does a person's — their life together alone (the wedding, the household, the homes, the children, the
34
+ widowhood), the partners' stories referred to, never told again. A person's story written after it says the
35
+ marriage in a sentence; no story written already is shortened for it. `strom config set stories no` turns these off
36
+ too.
37
+ - **`strom repair` in one language.** Every line it writes ("restored", "nothing to repair", …) is in the research's
38
+ language for a person; an agent reads them in English as before.
25
39
 
26
40
  ## 1.22.2
27
41
 
@@ -697,6 +697,12 @@
697
697
  "story.where": "zapsané údaje (strom person show {id})",
698
698
  "story.why": "{count, plural, one {# údaj ze zápisů vypráví} few {# údaje ze zápisů vyprávějí} other {# údajů ze zápisů vypráví}} život (generace {generation} výzkumu {research}); rodina si ho přečte v rodinné knize aplikace Strom",
699
699
  "story.done": "strom story set {id} se všemi údaji, o které se opírá (--fact) – koncept, dokud ho uživatel neschválí",
700
+ "story.couple.what": "Napsat vyprávění páru: {a} a {b}",
701
+ "story.couple.more.what": "Doplnit vyprávění páru: {a} a {b} – {count, plural, one {# nový údaj} few {# nové údaje} other {# nových údajů}}",
702
+ "story.couple.where": "zapsané údaje páru (strom family show {id})",
703
+ "story.couple.partner": "vyprávění: {name}, už napsané – odkázat, neopakovat (strom story show {id})",
704
+ "story.couple.why": "{count, plural, one {# údaj ze zápisů vypráví} few {# údaje ze zápisů vyprávějí} other {# údajů ze zápisů vypráví}} jejich společný život (generace {generation} výzkumu {research}); život každého z nich je v jeho vlastním vyprávění",
705
+ "story.couple.done": "strom story set {id} se všemi údaji, o které se opírá (--fact): jen společný život – svatba, domácnost, bydliště, děti, ovdovění – koncept, dokud ho uživatel neschválí",
700
706
  "story.sources.what": "Mimo matriky, pro vyprávění: {name} – noviny, adresáře, vojenské, pozemkové a soudní záznamy, hroby, dějiny místa",
701
707
  "story.sources.where": "digitalizované noviny, knihy a archivy země a doby; web (rejstříky a rodokmeny jsou jen vodítko)",
702
708
  "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í",
@@ -1956,6 +1962,7 @@
1956
1962
  "ui.link.story.none": "Vyprávění o {person} zatím není. Nic se nestalo.",
1957
1963
  "ui.link.story.final": "Vyprávění o {person} už je schválené.",
1958
1964
  "ui.link.what.story": "schválit vyprávění osoby {person}, jak je (už nebude návrh).",
1965
+ "ui.link.what.story.couple": "schválit vyprávění páru {couple}, jak je (už nebude návrh).",
1959
1966
  "ui.link.story.done": "✓ Vyprávění je schválené. Po novém otevření výzkumu v aplikaci Strom tam bude bez štítku Návrh.",
1960
1967
  "ui.link.sync.none": "{input} není v tomto výzkumu poslání z aplikace Strom. Nic se nestalo.",
1961
1968
  "ui.link.sync.was": "Toto poslání už bylo vráceno.",
@@ -3112,6 +3119,11 @@
3112
3119
  "ui.check.otherkey.afterrepair.hint": "po strom repair sjednotit ve vlastním terminálu: strom seal adopt",
3113
3120
  "ui.repair.resealed": "znovu zapečetěno – commit udělaný mimo strom nezměnil žádný záznam",
3114
3121
  "ui.repair.unite": "dál: sjednotit klíče stromu na tomto počítači ve vlastním terminálu: strom seal adopt",
3122
+ "ui.repair.nothing": "není co opravit – každý záznam odpovídá své pečeti",
3123
+ "ui.repair.restored": "obnoveno {file}",
3124
+ "ui.repair.removed": "odebráno {file}",
3125
+ "ui.repair.from": "zapečetěný stav: {commit} · kopie nahrazených souborů: .strom/quarantine/",
3126
+ "ui.repair.still": "{n, plural, one {zbývá # problém} few {zbývají # problémy} other {zbývá # problémů}} → strom verify",
3115
3127
  "ui.error.seal.repair-in-terminal": "výzkum „{name}“ obsahuje práci zapečetěnou klíčem stromu z terminálu, který strom spuštěný z aplikace nevidí: nic se neopravilo",
3116
3128
  "ui.error.seal.repair-in-terminal.hint": "ve vlastním terminálu: strom repair, pak strom seal adopt – ten vidí oba klíče a vrátí jen to, co se udělalo mimo strom",
3117
3129
  "ui.menu.seal": "Zapečetěno na jiném počítači: zapisovat tu jde až po převzetí (strom seal adopt).",
@@ -697,6 +697,12 @@
697
697
  "story.where": "die erfassten Angaben (strom person show {id})",
698
698
  "story.why": "{count, plural, one {# Angabe aus Einträgen erzählt} other {# Angaben aus Einträgen erzählen}} das Leben (Generation {generation} der Forschung {research}); die Familie liest es im Familienbuch der Strom-App",
699
699
  "story.done": "strom story set {id} mit jeder Angabe, auf der sie beruht (--fact) – ein Entwurf, bis der Benutzer sie freigibt",
700
+ "story.couple.what": "Die Geschichte des Paares schreiben: {a} und {b}",
701
+ "story.couple.more.what": "Die Geschichte des Paares ergänzen: {a} und {b} – {count, plural, one {# neue Angabe} other {# neue Angaben}}",
702
+ "story.couple.where": "die erfassten Angaben des Paares (strom family show {id})",
703
+ "story.couple.partner": "die Geschichte: {name}, schon geschrieben – darauf verweisen, nicht wiederholen (strom story show {id})",
704
+ "story.couple.why": "{count, plural, one {# Angabe aus Einträgen erzählt} other {# Angaben aus Einträgen erzählen}} ihr gemeinsames Leben (Generation {generation} der Forschung {research}); das eigene Leben jedes der beiden steht in seiner eigenen Geschichte",
705
+ "story.couple.done": "strom story set {id} mit jeder Angabe, auf der sie beruht (--fact): nur das gemeinsame Leben – die Hochzeit, der Haushalt, die Wohnorte, die Kinder, die Witwenschaft – ein Entwurf, bis der Benutzer sie freigibt",
700
706
  "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",
701
707
  "story.sources.where": "die digitalisierten Zeitungen, Bücher und Archive des Landes und der Zeit; das Web (Register und Stammbäume nur als Hinweis)",
702
708
  "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",
@@ -1956,6 +1962,7 @@
1956
1962
  "ui.link.story.none": "{person} hat noch keine Erzählung. Es ist nichts geschehen.",
1957
1963
  "ui.link.story.final": "Die Erzählung von {person} ist bereits freigegeben.",
1958
1964
  "ui.link.what.story": "die Erzählung von {person} so freigeben, wie sie ist (dann kein Entwurf mehr).",
1965
+ "ui.link.what.story.couple": "die Erzählung des Paares {couple} so freigeben, wie sie ist (dann kein Entwurf mehr).",
1959
1966
  "ui.link.story.done": "✓ Die Erzählung ist freigegeben. Wird die Forschung in der Strom-App erneut geöffnet, steht sie ohne das Etikett Entwurf da.",
1960
1967
  "ui.link.sync.none": "{input} ist in dieser Forschung keine Sendung aus der Strom-App. Es ist nichts geschehen.",
1961
1968
  "ui.link.sync.was": "Diese Sendung wurde bereits zurückgenommen.",
@@ -3112,6 +3119,11 @@
3112
3119
  "ui.check.otherkey.afterrepair.hint": "nach strom repair vereinen in einem eigenen Terminal: strom seal adopt",
3113
3120
  "ui.repair.resealed": "neu versiegelt – der Commit außerhalb von strom änderte keinen Eintrag",
3114
3121
  "ui.repair.unite": "weiter: die Schlüssel von strom auf diesem Computer vereinen in einem eigenen Terminal: strom seal adopt",
3122
+ "ui.repair.nothing": "nichts zu reparieren – jeder Eintrag stimmt mit seiner Versiegelung überein",
3123
+ "ui.repair.restored": "wiederhergestellt {file}",
3124
+ "ui.repair.removed": "entfernt {file}",
3125
+ "ui.repair.from": "versiegelter Stand: {commit} · Kopien ersetzter Dateien: .strom/quarantine/",
3126
+ "ui.repair.still": "noch {n, plural, one {# Problem} other {# Probleme}} → strom verify",
3115
3127
  "ui.error.seal.repair-in-terminal": "die Forschung „{name}“ enthält Arbeit, versiegelt mit dem Schlüssel des strom aus dem Terminal, den ein aus einer App gestartetes strom nicht sieht: nichts wurde repariert",
3116
3128
  "ui.error.seal.repair-in-terminal.hint": "in einem eigenen Terminal: strom repair, dann strom seal adopt – es sieht beide Schlüssel und macht nur rückgängig, was außerhalb von strom geschah",
3117
3129
  "ui.menu.seal": "Auf einem anderen Computer versiegelt: schreiben hier erst nach der Übernahme (strom seal adopt).",
@@ -18,6 +18,11 @@ A story is written from the facts, for the family.
18
18
  - Write it to a file in notes/, then `strom story set P… --text @notes/story-P….md
19
19
  --title "…" --fact E… --fact E… --source S… --note "what is inferred"`: every fact it leans on goes
20
20
  in --fact, every source of its background in --source. A couple's story goes on the family (F…).
21
+ - A couple's story tells their life together: the wedding, the household, the
22
+ homes, the children, the widowhood. Each one's life before and after it is
23
+ their own story's — referred to in a sentence, never told again.
24
+ - A person whose couple has a story: the marriage in one sentence pointing to
25
+ it. What a story told already stays; it is never shortened for a couple's.
21
26
  - It stays a draft; when the user says it is right: `strom story approve P…`
22
27
  (the Strom app asks them too). An approved story is locked — the user liked
23
28
  it: written again (more facts, a better reading), the new version waits
@@ -294,6 +294,46 @@ function familyBlock(tree, f, short = false) {
294
294
  out.push(` note: ${noteStart(n.text, short ? NOTE_LINE : NOTE_SHOWN)}`);
295
295
  return out;
296
296
  }
297
+ /**
298
+ * What the other stories tell already, for a story to write (spec 027 K6.3–K6.4): a couple's — the partners' own
299
+ * stories, to be referred to and not told again; a person's — the couples' stories of theirs, the marriage in a
300
+ * sentence pointing to it, and what the person's story told already never shortened for it.
301
+ */
302
+ function storiesTold(tree, task) {
303
+ const subject = task.subject.map((id) => tree.get(id)).find((r) => r?.type === "family" || r?.type === "person");
304
+ if (!subject)
305
+ return undefined;
306
+ if (subject.type === "family") {
307
+ const partners = subject.partners.map((x) => tree.get(x)).filter((x) => !!x && !x.retracted);
308
+ const text = [
309
+ `## The couple's story (${subject.id}): their life together alone — the wedding, the household, the homes, the children, the widowhood`,
310
+ "- each one's life before the wedding and after it is their own story's: referred to in a sentence, never told again",
311
+ "- every statement on a recorded fact (--fact), as in a person's story",
312
+ ];
313
+ // who has no story comes before the stories: a line under a partner's story would read as part of it
314
+ for (const p of partners)
315
+ if (!(p.story?.draft ?? p.story))
316
+ text.push(`- ${label(p)}: no story of their own yet — their life outside the marriage stays out of this one too`);
317
+ for (const p of partners) {
318
+ const st = p.story?.draft ?? p.story;
319
+ if (!st)
320
+ continue;
321
+ text.push(`### The story of ${label(p)}${p.story?.draft ? " (its new version, waiting for the user)" : ` (${p.story.status})`}${st.title ? `: ${st.title}` : ""}`, st.text.trim());
322
+ }
323
+ return { name: "stories told", first: true, pointer: `strom story show ${partners.map((p) => p.id).join(" · strom story show ")}`, text: text.join("\n") };
324
+ }
325
+ const couples = familiesAsPartner(tree, subject.id).filter((f) => !f.retracted && f.story);
326
+ if (!couples.length)
327
+ return undefined;
328
+ const text = [`## The couples' stories of ${label(subject)}: told already`];
329
+ for (const f of couples) {
330
+ const others = f.partners.filter((x) => x !== subject.id).map((x) => tree.get(x)).filter((x) => !!x).map(label);
331
+ text.push(`- ${f.id}${others.length ? ` with ${others.join(", ")}` : ""} has a story of its own (strom story show ${f.id}): the marriage in one sentence pointing to it — their life together not told again`);
332
+ }
333
+ if (subject.story)
334
+ text.push("- what the story told already stays — never shortened because of a couple's story");
335
+ return { name: "stories told", first: true, pointer: `strom story show ${couples.map((f) => f.id).join(" · strom story show ")}`, text: text.join("\n") };
336
+ }
297
337
  /** A small text file of an input, as strom input show gives it; unreadable: nothing. */
298
338
  function readSmall(file) {
299
339
  try {
@@ -1299,6 +1339,9 @@ export function buildBrief(tree, opts) {
1299
1339
  const ofTime = storyHistoryOn && task.level === "narrate" ? storyHistorySection(tree, task, opts.shared) : undefined;
1300
1340
  if (ofTime)
1301
1341
  sections.push(ofTime);
1342
+ const told = task?.level === "narrate" ? storiesTold(tree, task) : undefined;
1343
+ if (told)
1344
+ sections.push(told);
1302
1345
  // 8 bis. the search beyond the registers: its budget of pages, printed pages for the user's model for print where the
1303
1346
  // agent hands work to another model, and what the research has written of the person's places already
1304
1347
  if (task?.origin === STORY_SOURCES_ORIGIN) {
@@ -442,7 +442,9 @@ export async function approveStory(ctx, run, lang, root, person, who, how = "fin
442
442
  if (st.status === "final")
443
443
  return stop(ctx, t("ui.link.story.final", { person: who }));
444
444
  out(ctx, lines(st.title ? `„${st.title}“` : undefined, "", st.text.replace(/\*\*/g, ""), ""));
445
- if (!(await asks(ctx, lang, root, t("ui.link.what.story", { person: who }))))
445
+ // a couple's story said as the couple's, never as a person's
446
+ const what = person.type === "family" ? t("ui.link.what.story.couple", { couple: who }) : t("ui.link.what.story", { person: who });
447
+ if (!(await asks(ctx, lang, root, what)))
446
448
  return false;
447
449
  if ((await run(["story", "approve", person.id], true)) !== 0)
448
450
  return false;
package/dist/cli/ui.js CHANGED
@@ -1930,6 +1930,7 @@ export const UI = {
1930
1930
  "ui.link.story.none": "{person} has no story yet. Nothing was done.",
1931
1931
  "ui.link.story.final": "The story of {person} is approved already.",
1932
1932
  "ui.link.what.story": "approve the story of {person} as it is (it will no longer be a draft).",
1933
+ "ui.link.what.story.couple": "approve the story of the couple {couple} as it is (it will no longer be a draft).",
1933
1934
  "ui.link.story.done": "✓ The story is approved. Open the research in the Strom app again and it is there without the draft label.",
1934
1935
  "ui.link.sync.none": "{input} is not a sending from the Strom app in this research. Nothing was done.",
1935
1936
  "ui.link.sync.was": "This sending was taken back already.",
@@ -2804,6 +2805,11 @@ export const UI = {
2804
2805
  "ui.check.otherkey.afterrepair.hint": "after strom repair, united in a terminal of its own: strom seal adopt",
2805
2806
  "ui.repair.resealed": "sealed again — the commit made outside strom changed no record",
2806
2807
  "ui.repair.unite": "next: the keys of strom on this computer united in a terminal of its own: strom seal adopt",
2808
+ "ui.repair.nothing": "nothing to repair — every record matches its seal",
2809
+ "ui.repair.restored": "restored {file}",
2810
+ "ui.repair.removed": "removed {file}",
2811
+ "ui.repair.from": "sealed state: {commit} · copies of replaced files: .strom/quarantine/",
2812
+ "ui.repair.still": "still {n, plural, one {# problem} other {# problems}} → strom verify",
2807
2813
  "ui.error.seal.repair-in-terminal": "\"{name}\" holds work sealed with the key of the terminal's strom, which a strom started by an app cannot see: nothing repaired",
2808
2814
  "ui.error.seal.repair-in-terminal.hint": "in a terminal of its own: strom repair, then strom seal adopt — it sees both keys and undoes only what was done outside strom",
2809
2815
  "ui.menu.seal": "Sealed on another computer: writing here only once taken over (strom seal adopt).",
@@ -8,9 +8,8 @@ import { counted, lines, table } from "../cli/format.js";
8
8
  import { check, SHARED_MISSING, sharedMissing } from "../core/check.js";
9
9
  import { guard } from "../core/guard.js";
10
10
  import { unprovenPeople } from "../core/review.js";
11
- import { AFTER_REPAIR_HINT, NOT_DOWNLOADED, OTHER_KEY, SEALED_PATHS, notDownloaded, notDownloadedError, otherKeyError, verifyFast, verifyFull } from "../core/integrity.js";
12
- import { otherKeys, readCommitSeal, verifyCommitSeal } from "../core/seal.js";
13
- import { settingsHidden } from "../core/oldsettings.js";
11
+ import { AFTER_REPAIR_HINT, NOT_DOWNLOADED, OTHER_KEY, REPAIR_IN_TERMINAL_HINT, SEALED_PATHS, lastSealed, notDownloaded, notDownloadedError, otherKeyError, verifyFast, verifyFull } from "../core/integrity.js";
12
+ import { otherKeys } from "../core/seal.js";
14
13
  import { adoptSeal, keysToUnite } from "../cli/seal.js";
15
14
  import { NeedsConsentError, StromError } from "../core/errors.js";
16
15
  import * as git from "../core/git.js";
@@ -157,18 +156,14 @@ function repair(tree) {
157
156
  const key = tree.requireKey();
158
157
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
159
158
  const head = git.headInfo(tree.root);
160
- // the newest commit any key of this computer sealed (strom of an app packaged for Windows and of a terminal each had
161
- // their own): only what was done outside strom undone, the other key's work kept — strom seal adopt unites them after
162
- const keys = [key, ...otherKeys(tree.env, tree.config.id, key).map((o) => o.key)];
163
- const above = [];
164
- const sealed = git.findCommit(tree.root, (t, m) => keys.some((k) => verifyCommitSeal(k, t, m)) || (above.push(m), false));
159
+ // the newest commit any key of this computer sealed: only what was done outside strom undone, the other key's work
160
+ // kept — strom seal adopt unites them after
161
+ const { sealed, inTerminal } = lastSealed(tree, key);
165
162
  if (!sealed)
166
163
  throw new StromError("no sealed commit found — nothing to restore from", { hint: "ask the user; the history may have been rewritten" });
167
- // started by an app packaged for Windows: a commit above it with strom's seal may be the terminal's key out of its
168
- // sight — never undone from here, the terminal's strom sees both keys
169
- if (above.some((m) => readCommitSeal(m) !== undefined) && settingsHidden(tree.env))
164
+ if (inTerminal)
170
165
  throw new StromError(`"${tree.config.name}" holds work sealed with the key of the terminal's strom, which this strom (started by an app packaged for Windows) does not see — nothing repaired`, {
171
- hint: "the user runs in their terminal: strom repair, then strom seal adopt (it sees both keys and undoes only what was done outside strom)",
166
+ hint: REPAIR_IN_TERMINAL_HINT,
172
167
  code: "seal.repair-in-terminal",
173
168
  params: { name: tree.config.name },
174
169
  });
@@ -230,8 +225,8 @@ register({
230
225
  const text = r.restored.length + r.quarantined.length === 0
231
226
  ? r.resealed
232
227
  ? lines(ui(lang, "ui.repair.resealed"), next)
233
- : lines("nothing to repair — every record matches its seal", next)
234
- : lines(...r.restored.map((f) => `restored ${f}`), ...r.quarantined.map((f) => `removed ${f}`), `sealed state: ${r.from.slice(0, 8)} · copies of replaced files: .strom/quarantine/`, after.length ? `still ${counted(after.length, "problem")} → strom verify` : next);
228
+ : lines(ui(lang, "ui.repair.nothing"), next)
229
+ : lines(...r.restored.map((file) => ui(lang, "ui.repair.restored", { file })), ...r.quarantined.map((file) => ui(lang, "ui.repair.removed", { file })), ui(lang, "ui.repair.from", { commit: r.from.slice(0, 8) }), after.length ? ui(lang, "ui.repair.still", { n: after.length }) : next);
235
230
  return { text, data: { ...r, ok: after.length === 0, ...(unite ? { unite: "strom seal adopt" } : {}) }, exitCode: after.length ? 1 : 0 };
236
231
  },
237
232
  }, {
@@ -253,7 +253,8 @@ STORIES OF THE ANCESTORS (the setting stories — on by default; strom shows it)
253
253
  task: write the story for the family book — plain words, the research language, every statement on a
254
254
  recorded fact (strom story set P… --text @notes/story-P….md --title "…" --fact E… …; paragraphs,
255
255
  "## " subheadings, "- " bullets, **bold**, *italic* — nothing else shows in the app). More facts later: it
256
- proposes adding to it. Stories wait behind all the research; working alone
256
+ proposes adding to it. Once one of a couple has a story, the couple's (strom story set F…): their life
257
+ together alone, the partners' stories referred to, not retold. Stories wait behind all the research; working alone
257
258
  (strom run), every few sessions one comes first. In a conversation the user leads: write one when they
258
259
  want it, or offer it when the research has a pause. A story is a draft until the user approves it (strom story approve P…; the Strom app asks it too). When strom says to tell
259
260
  the user (once, not in every conversation): in a sentence, and that they may say no (strom config set
@@ -484,6 +484,19 @@ export function writableUnasked(tree) {
484
484
  return false;
485
485
  }
486
486
  }
487
+ /**
488
+ * The newest commit any key of this computer sealed (strom of an app packaged for Windows and of a terminal each had
489
+ * their own), and whether strom repair is the terminal's: started by such an app, a commit above it with strom's seal may
490
+ * be the terminal's key out of its sight — never undone from here, the terminal's strom sees both keys.
491
+ */
492
+ export function lastSealed(tree, key) {
493
+ const keys = [key, ...otherKeys(tree.env, tree.config.id, key).map((o) => o.key)];
494
+ const above = [];
495
+ const sealed = git.findCommit(tree.root, (t, m) => keys.some((k) => verifyCommitSeal(k, t, m)) || (above.push(m), false));
496
+ return { sealed, inTerminal: !!sealed && above.some((m) => readCommitSeal(m) !== undefined) && settingsHidden(tree.env) };
497
+ }
498
+ /** What strom repair started by an app packaged for Windows says instead (lastSealed inTerminal). */
499
+ export const REPAIR_IN_TERMINAL_HINT = "the user runs in their terminal: strom repair, then strom seal adopt (it sees both keys and undoes only what was done outside strom)";
487
500
  /** Refuse to write on top of data that was changed outside strom. */
488
501
  export function assertIntact(tree) {
489
502
  const { findings } = verifyFast(tree);
@@ -498,8 +511,15 @@ export function assertIntact(tree) {
498
511
  // files only in iCloud: downloaded first — what else is wrong is judged then
499
512
  if (errors.some((f) => f.code === NOT_DOWNLOADED))
500
513
  throw notDownloadedError(notDownloaded(tree.root));
514
+ // with another key of this computer: the one way strom check says (oneWay); started by an app packaged for Windows
515
+ // above the terminal's work, strom repair in the terminal — never first the app's own, which refuses (found 2026-10-30)
516
+ const way = errors.some((f) => f.code === OTHER_KEY)
517
+ ? errors.find((f) => f.code !== OTHER_KEY && f.hint?.endsWith("then strom seal adopt"))?.hint
518
+ : tree.key && errors.some((f) => f.code === "foreign-commit") && lastSealed(tree, tree.key).inTerminal
519
+ ? REPAIR_IN_TERMINAL_HINT
520
+ : undefined;
501
521
  throw new StromError(`data was changed outside strom (${counted(errors.length, "problem")}) — refusing to write`, {
502
- hint: "strom verify (details)\nstrom repair (restore the last sealed state)",
522
+ hint: `strom verify (details)\n${way ?? "strom repair (restore the last sealed state)"}`,
503
523
  details: errors,
504
524
  });
505
525
  }
@@ -12,6 +12,18 @@ import path from "node:path";
12
12
  import { configDir, userHome } from "./paths.js";
13
13
  import { writesPrivately } from "./packaged.js";
14
14
  import { installation } from "./self.js";
15
+ /**
16
+ * %APPDATA%\strom alone, as this process sees it (oldSettingsDirs' first), without looking through the packaged apps:
17
+ * where an older strom looks for its keys.
18
+ */
19
+ export function oldSettingsDir(env, platform = process.platform, inst = installation()) {
20
+ if (inst.kind === "source" && env.STROM_TEST_OLD_SETTINGS !== undefined)
21
+ return env.STROM_TEST_OLD_SETTINGS.split(path.delimiter).filter(Boolean)[0];
22
+ if (env.STROM_CONFIG_DIR || platform !== "win32")
23
+ return undefined;
24
+ const home = env.USERPROFILE || userHome(env);
25
+ return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "strom");
26
+ }
15
27
  /**
16
28
  * The folders strom kept its settings in before, as this process sees them: %APPDATA%\strom first (in a packaged app
17
29
  * its private copy where there is one, else the real one), then each packaged app's copy, read by its path. None with
@@ -21,10 +33,10 @@ import { installation } from "./self.js";
21
33
  export function oldSettingsDirs(env, platform = process.platform, inst = installation()) {
22
34
  if (inst.kind === "source" && env.STROM_TEST_OLD_SETTINGS !== undefined)
23
35
  return env.STROM_TEST_OLD_SETTINGS.split(path.delimiter).filter(Boolean);
24
- if (env.STROM_CONFIG_DIR || platform !== "win32")
36
+ const roaming = oldSettingsDir(env, platform, inst);
37
+ if (!roaming)
25
38
  return [];
26
39
  const home = env.USERPROFILE || userHome(env);
27
- const roaming = path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "strom");
28
40
  const packages = path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Packages");
29
41
  let apps = [];
30
42
  try {
@@ -35,6 +35,12 @@ export const PHRASES = {
35
35
  "story.where": "the facts recorded (strom person show {id})",
36
36
  "story.why": "{count, plural, one {# fact from records tells} other {# facts from records tell}} the life (generation {generation} of {research}); the family reads it in the Strom app's family book",
37
37
  "story.done": "strom story set {id} with every fact it rests on (--fact) — a draft until the user approves it",
38
+ "story.couple.what": "Write the story of the couple {a} and {b}",
39
+ "story.couple.more.what": "Add to the story of the couple {a} and {b}: {count, plural, one {# new fact} other {# new facts}}",
40
+ "story.couple.where": "the couple's facts recorded (strom family show {id})",
41
+ "story.couple.partner": "the story of {name}, told already: referred to, not retold (strom story show {id})",
42
+ "story.couple.why": "{count, plural, one {# fact from records tells} other {# facts from records tell}} their life together (generation {generation} of {research}); each one's own life is in their own story",
43
+ "story.couple.done": "strom story set {id} with every fact it rests on (--fact): their life together alone — the wedding, the household, the homes, the children, the widowhood — a draft until the user approves it",
38
44
  "story.sources.what": "Beyond the registers, for the story of {name}: newspapers, directories, military, land and court records, graves, the history of the place",
39
45
  "story.sources.where": "the digitised newspapers, books and archives of the country and time; the web (indexes and trees are leads only)",
40
46
  "story.sources.why": "the story is to tell more than the registers do — every word still on a record; then strom proposes the story",
@@ -24,8 +24,7 @@
24
24
  import { parentsOf } from "./people.js";
25
25
  import { taskRecordsets } from "./frontier.js";
26
26
  import { now } from "./tree.js";
27
- import { subjectPeople } from "./records.js";
28
- import { directionOf, scopes } from "./directions.js";
27
+ import { aboutPeople, directionOf, scopes } from "./directions.js";
29
28
  import { offTree } from "./kin.js";
30
29
  import { forStory } from "./stories.js";
31
30
  import { HYPOTHESIS_LINKS_TRIES, linksFirst } from "./hypolinks.js";
@@ -153,7 +152,8 @@ export function rankTasks(tree, tasks, strategy = "balanced", opts = {}) {
153
152
  const place = (t) => {
154
153
  // a task of no research of its own: the direction its people are in (directions.ts), as the menu and the app say
155
154
  const map = linesOf(t.research ?? directionOf(tree, t, all));
156
- const found = [...t.subject, ...subjectPeople(tree, t.subject)].map((s) => map?.get(s)).filter((x) => !!x);
155
+ // a family's task: its partners' (a couple's story)
156
+ const found = [...t.subject, ...aboutPeople(tree, t.subject)].map((s) => map?.get(s)).filter((x) => !!x);
157
157
  return found.sort((a, b) => a.gen - b.gen)[0];
158
158
  };
159
159
  const tasksById = new Map(tree.list("task").map((t) => [t.id, t]));
package/dist/core/seal.js CHANGED
@@ -16,7 +16,7 @@ import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { configDir } from "./paths.js";
18
18
  import { canonicalize } from "./json.js";
19
- import { oldKeys, oldSettingsDirs } from "./oldsettings.js";
19
+ import { oldKeys, oldSettingsDir } from "./oldsettings.js";
20
20
  export function sha256(content) {
21
21
  return crypto.createHash("sha256").update(content).digest("hex");
22
22
  }
@@ -51,8 +51,10 @@ export function loadKey(env, treeId) {
51
51
  */
52
52
  export function settleKey(env, treeId, prefer) {
53
53
  const own = readKey(keyFile(env, treeId));
54
- if (own)
54
+ if (own) {
55
+ mirrorKey(env, treeId, own);
55
56
  return own;
57
+ }
56
58
  const old = oldKeys(env, treeId);
57
59
  if (!old.length)
58
60
  return undefined;
@@ -65,8 +67,29 @@ export function settleKey(env, treeId, prefer) {
65
67
  catch {
66
68
  // taken over next time
67
69
  }
70
+ mirrorKey(env, treeId, key);
68
71
  return key;
69
72
  }
73
+ /**
74
+ * The key also where an older strom looks (Windows: %APPDATA%\strom\keys), made if need be — back on an older version,
75
+ * the research is still sealed here. Only added: a key of its own there already stays (found 2026-10-30: a key taken
76
+ * from a packaged app's copy, never in the folder the terminal's older strom reads).
77
+ */
78
+ function mirrorKey(env, treeId, key) {
79
+ const old = oldSettingsDir(env);
80
+ if (!old)
81
+ return;
82
+ const file = path.join(old, "keys", `${treeId}.key`);
83
+ if (fs.existsSync(file))
84
+ return;
85
+ try {
86
+ fs.mkdirSync(path.dirname(file), { recursive: true });
87
+ fs.writeFileSync(file, key.toString("hex") + "\n", { mode: 0o600, flag: "wx" });
88
+ }
89
+ catch {
90
+ // a key of its own there already: the older strom keeps it
91
+ }
92
+ }
70
93
  /** The tree's other keys on this computer: those of the old settings folders that are not `key` (strom seal adopt unites them). */
71
94
  export function otherKeys(env, treeId, key) {
72
95
  return oldKeys(env, treeId).filter((o) => !o.key.equals(key));
@@ -80,18 +103,7 @@ export function createKey(env, treeId) {
80
103
  fs.mkdirSync(path.dirname(file), { recursive: true });
81
104
  const key = crypto.randomBytes(32);
82
105
  fs.writeFileSync(file, key.toString("hex") + "\n", { mode: 0o600, flag: "wx" });
83
- // also where an older strom looks (Windows: %APPDATA%\strom), made if need be — back on an older version, the research
84
- // is still sealed here
85
- const old = oldSettingsDirs(env)[0];
86
- if (old) {
87
- try {
88
- fs.mkdirSync(path.join(old, "keys"), { recursive: true });
89
- fs.writeFileSync(path.join(old, "keys", `${treeId}.key`), key.toString("hex") + "\n", { mode: 0o600, flag: "wx" });
90
- }
91
- catch {
92
- // a key of its own there already: the older strom keeps it
93
- }
94
- }
106
+ mirrorKey(env, treeId, key);
95
107
  return key;
96
108
  }
97
109
  /** Short public fingerprint of a key, stored on each operation. */
@@ -6,6 +6,9 @@
6
6
  // beyond the registers — newspapers, directories, military, land and court
7
7
  // records, graves, the history of the place — so the story can tell more, every
8
8
  // word still on a record; the story is proposed when that search is finished.
9
+ // A couple's story too, once one of the two has a story of their own: their life
10
+ // together alone (the wedding, the household, the homes, the children, the
11
+ // widowhood); what the partners' stories tell is referred to, never told again.
9
12
  import { researchPeople } from "./frontier.js";
10
13
  import { displayName, familiesAsPartner, lifespan } from "./people.js";
11
14
  import { phrase } from "./phrases.js";
@@ -24,6 +27,7 @@ export function forStory(t) {
24
27
  }
25
28
  const FROM_RECORDS = new Set(["proven", "probable"]);
26
29
  const BIRTHS = new Set(["BIRT", "CHR", "BAPM"]);
30
+ const MARRIAGES = new Set(["MARR"]);
27
31
  const OPEN = new Set(["open", "doing", "parked", "waiting"]);
28
32
  const FINISHED = new Set(["done", "dropped"]);
29
33
  /** Enough to tell a life: a birth or baptism from a record, and more facts from records. */
@@ -36,6 +40,23 @@ export function factsFromRecords(tree, p) {
36
40
  const marriages = familiesAsPartner(tree, p.id).flatMap((f) => f.events.filter((e) => !e.retracted && FROM_RECORDS.has(e.status)));
37
41
  return [...own, ...marriages];
38
42
  }
43
+ /**
44
+ * The facts of a couple's life together that records support: the family's own (the wedding, the homes, …) and the
45
+ * birth or baptism of each child — and whether they show the couple (a wedding or a child from a record).
46
+ */
47
+ export function coupleFacts(tree, f) {
48
+ const own = f.events.filter((e) => !e.retracted && FROM_RECORDS.has(e.status));
49
+ const births = f.children.flatMap((c) => {
50
+ const child = tree.get(c.person);
51
+ const birth = child && !child.retracted ? child.events.find((e) => !e.retracted && FROM_RECORDS.has(e.status) && BIRTHS.has(e.kind)) : undefined;
52
+ return birth ? [birth] : [];
53
+ });
54
+ return { facts: [...own, ...births], shown: births.length > 0 || own.some((e) => MARRIAGES.has(e.kind)) };
55
+ }
56
+ /** Has the person a story of their own — written, or the task of it done? */
57
+ function toldAlone(p, narrate) {
58
+ return !!p.story || narrate.some((t) => t.state === "done" && t.subject.includes(p.id));
59
+ }
39
60
  /** The stories strom proposes to write (or add to) for the ancestors of a research. */
40
61
  export function storyProposals(tree, research) {
41
62
  const lang = tree.lang;
@@ -43,29 +64,18 @@ export function storyProposals(tree, research) {
43
64
  const narrate = tasks.filter((t) => t.level === "narrate");
44
65
  const sources = new Settings(tree.env, {}).storySources(tree.config);
45
66
  const out = [];
46
- for (const [id, generation] of researchPeople(tree, research)) {
67
+ const people = researchPeople(tree, research);
68
+ for (const [id, generation] of people) {
47
69
  const p = tree.get(id);
48
70
  if (!p || p.retracted)
49
71
  continue;
50
72
  const facts = factsFromRecords(tree, p);
51
73
  if (facts.length < STORY_MIN_FACTS || !facts.some((e) => BIRTHS.has(e.kind)))
52
74
  continue;
53
- const mine = narrate.filter((t) => t.subject.includes(id));
54
- if (mine.some((t) => OPEN.has(t.state)))
55
- continue;
56
- // The latest version written: a new one waiting beside an approved story, else the story.
57
- const latest = p.story?.draft ?? p.story;
58
- // Dropped since the story was written (or with none): that was the answer — not again until the story changes.
59
- const since = latest?.at ?? "";
60
- if (mine.some((t) => t.state === "dropped" && t.updated >= since))
61
- continue;
62
- // Done, yet no story: somebody decided so; the user can add the task by hand.
63
- if (!p.story && mine.some((t) => FINISHED.has(t.state)))
75
+ const fresh = due(p, facts, narrate);
76
+ if (!fresh)
64
77
  continue;
65
78
  const name = `${displayName(p)}${lifespan(p) ? ` (${lifespan(p)})` : ""}`;
66
- const fresh = latest ? facts.filter((e) => !latest.facts.includes(e.id)) : facts;
67
- if (p.story && fresh.length < STORY_NEW_FACTS)
68
- continue;
69
79
  // First, once, the search beyond the registers; the story waits while it is open, and comes when it is finished
70
80
  // (done, or dropped: the user's answer about the search, not about the story).
71
81
  if (sources) {
@@ -98,8 +108,60 @@ export function storyProposals(tree, research) {
98
108
  research: research.id,
99
109
  });
100
110
  }
111
+ // The couples of the research, once one of the two has a story of their own: their life together, the partners'
112
+ // stories given to the agent as what is told already. The search beyond the registers was each partner's.
113
+ for (const f of tree.list("family")) {
114
+ if (f.retracted || f.noCouple || f.partners.length !== 2 || !f.partners.some((x) => people.has(x)))
115
+ continue;
116
+ const partners = f.partners.map((x) => tree.get(x));
117
+ if (partners.some((x) => !x || x.retracted))
118
+ continue;
119
+ const [a, b] = partners;
120
+ if (!toldAlone(a, narrate) && !toldAlone(b, narrate))
121
+ continue;
122
+ const { facts, shown } = coupleFacts(tree, f);
123
+ if (!shown || facts.length < STORY_MIN_FACTS)
124
+ continue;
125
+ const fresh = due(f, facts, narrate);
126
+ if (!fresh)
127
+ continue;
128
+ const generation = Math.min(...f.partners.map((x) => people.get(x) ?? Infinity));
129
+ const names = { a: displayName(a), b: displayName(b) };
130
+ out.push({
131
+ level: "narrate",
132
+ priority: 1, // like a person's: after the research
133
+ what: phrase(lang, f.story ? "story.couple.more.what" : "story.couple.what", { ...names, count: fresh.length }),
134
+ where: [phrase(lang, "story.couple.where", { id: f.id }), ...[a, b].filter((x) => x.story).map((x) => phrase(lang, "story.couple.partner", { name: displayName(x), id: x.id }))],
135
+ why: phrase(lang, "story.couple.why", { count: facts.length, generation, research: research.name }),
136
+ doneWhen: phrase(lang, "story.couple.done", { id: f.id }),
137
+ subject: [f.id],
138
+ research: research.id,
139
+ });
140
+ }
101
141
  return out;
102
142
  }
143
+ /**
144
+ * The new facts a story of the person or couple is due for — all of them before it is written — or undefined: its task
145
+ * open, dropped since the story was written (that was the answer), done with no story, or too few new since.
146
+ */
147
+ function due(rec, facts, narrate) {
148
+ const mine = narrate.filter((t) => t.subject.includes(rec.id));
149
+ if (mine.some((t) => OPEN.has(t.state)))
150
+ return undefined;
151
+ // The latest version written: a new one waiting beside an approved story, else the story.
152
+ const latest = rec.story?.draft ?? rec.story;
153
+ // Dropped since the story was written (or with none): that was the answer — not again until the story changes.
154
+ const since = latest?.at ?? "";
155
+ if (mine.some((t) => t.state === "dropped" && t.updated >= since))
156
+ return undefined;
157
+ // Done, yet no story: somebody decided so; the user can add the task by hand.
158
+ if (!rec.story && mine.some((t) => FINISHED.has(t.state)))
159
+ return undefined;
160
+ const fresh = latest ? facts.filter((e) => !latest.facts.includes(e.id)) : facts;
161
+ if (rec.story && fresh.length < STORY_NEW_FACTS)
162
+ return undefined;
163
+ return fresh;
164
+ }
103
165
  /** The new versions of approved stories that wait for the user (the lock): whose, and since when. */
104
166
  export function storiesToApprove(tree) {
105
167
  return [...tree.list("person"), ...tree.list("family")]
package/dist/core/tree.js CHANGED
@@ -29,7 +29,7 @@ export const TREE_FILE = "strom.json";
29
29
  export const SEAL_FOREIGN = "seal.foreign";
30
30
  // between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
31
31
  // at a release the release's own (package.json)
32
- export const VERSION = "1.23.0-beta.2";
32
+ export const VERSION = "1.23.0-beta.4";
33
33
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
34
34
  .strom/
35
35
  /media/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.23.0-beta.2",
3
+ "version": "1.23.0-beta.4",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {