strom-research 1.23.0-beta.3 → 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.3. 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
@@ -28,6 +28,11 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
28
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
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
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.
31
36
  - **`strom repair` jedním jazykem.** Každý jeho řádek („obnoveno“, „není co opravit“ …) je pro člověka v jazyce
32
37
  výzkumu; agent je čte anglicky jako dřív.
33
38
 
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.3. 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
@@ -33,6 +33,12 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
33
33
  der dort schon liegt, wird nie ersetzt.
34
34
  - **Die Erzählung eines Paares, aus der Strom-App freigegeben.** Die Frage im Terminal nennt jetzt die Erzählung des
35
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.
36
42
  - **`strom repair` in einer Sprache.** Jede Zeile („wiederhergestellt“, „nichts zu reparieren“ …) steht für eine
37
43
  Person in der Sprache der Forschung; ein Agent liest sie wie bisher auf Englisch.
38
44
 
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.3. 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>
@@ -28,6 +28,12 @@ The changes of each version in short. The full notes and the install lines are o
28
28
  older version, the research is still sealed on this computer. A key there already is never replaced.
29
29
  - **A couple's story approved from the Strom app.** The question in the terminal now names the story of the couple,
30
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.
31
37
  - **`strom repair` in one language.** Every line it writes ("restored", "nothing to repair", …) is in the research's
32
38
  language for a person; an agent reads them in English as before.
33
39
 
@@ -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í",
@@ -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",
@@ -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) {
@@ -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
@@ -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]));
@@ -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.3";
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.3",
3
+ "version": "1.23.0-beta.4",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {