strom-research 1.22.0-beta.5 → 1.22.0-beta.6

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.22.0
9
9
 
10
- *Připravuje se: bety do 1.22.0-beta.5. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.22.0-beta.6. Oddíl bude úplný s vydáním.*
11
11
 
12
12
  - **Jméno jednou v tom, co čeká.** Úkol k materiálu poslanému z aplikace Strom jmenoval ve widgetu a na stránce
13
13
  telefonu každého člověka dvakrát (podruhé v hranatých závorkách) a jeho text bez jmen nechal prázdné závorky; menu,
@@ -32,6 +32,8 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
32
32
  prohledávají průběžně mezi sezeními rodinného výzkumu: zhruba každé 5. sezení (výchozí), každé 10., nebo jen když
33
33
  není co jiného (`strom config set experiments.historyPace`, úvodní nastavení, Nastavení); kdy, řekne `strom plan`.
34
34
  Nejdřív regiony s nejvíc osobami, země před státem nad ní.
35
+ Když jsou události regionu sebrané, roky, kdy v něm lidé stromu žili víc než 15 let po sobě bez jediné události,
36
+ se prohledají ještě jednou; prohledané roky se v žádném výzkumu znovu neotevřou, dokud se o region nepožádá znovu.
35
37
 
36
38
  ## 1.21.0
37
39
 
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.22.0
9
9
 
10
- *In Vorbereitung: die Betas bis 1.22.0-beta.5. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.22.0-beta.6. Der Abschnitt ist mit dem Release vollständig.*
11
11
 
12
12
  - **Jeder Name einmal in dem, was wartet.** Eine Aufgabe zu Material, das aus der Strom-App gesendet wurde, nannte im
13
13
  Widget und auf der Seite des Telefons jede Person zweimal (das zweite Mal in eckigen Klammern), und ihr Text ohne
@@ -37,6 +37,9 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
37
37
  Sitzungen der Familienforschung durchsucht: etwa jede 5. Sitzung (Standard), jede 10. oder nur, wenn nichts anderes
38
38
  ansteht (`strom config set experiments.historyPace`, die Ersteinrichtung, die Einstellungen); wann, sagt `strom plan`.
39
39
  Zuerst die Regionen mit den meisten Personen, ein Land vor dem Staat darüber.
40
+ Sind die Ereignisse einer Region gesammelt, werden die Jahre, in denen die Personen des Stammbaums dort mehr als
41
+ 15 Jahre in Folge ohne ein Ereignis lebten, noch einmal durchsucht; durchsuchte Jahre werden in keiner Forschung
42
+ erneut aufgegriffen, bis die Region wieder angefragt wird.
40
43
 
41
44
  ## 1.21.0
42
45
 
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.22.0
9
9
 
10
- *In preparation: the betas up to 1.22.0-beta.5. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.22.0-beta.6. The section is complete with the release.*
11
11
 
12
12
  - **Each name once in what waits.** A task for material sent from the Strom app named each person twice in the
13
13
  widget and on the phone's page (the second time in brackets), and its text without names kept empty brackets; the
@@ -36,6 +36,9 @@ The changes of each version in short. The full notes and the install lines are o
36
36
  sessions: about every 5th (the default), every 10th, or only when nothing else is left
37
37
  (`strom config set experiments.historyPace`, the setup, the settings); `strom plan` says when. The regions
38
38
  of the most people come first, a land before the state above it.
39
+ Once a region's events are gathered, the years the tree's people lived there for more than 15 in a row with no
40
+ event are looked through once more; years looked through are not raised again, in any research, until the region
41
+ is asked for again.
39
42
 
40
43
  ## 1.21.0
41
44
 
@@ -731,6 +731,13 @@
731
731
  "history.region.why": "co předkové zažili, ukázané u jejich životů",
732
732
  "history.region.done": "důležité události období nalezené, každá ze stránky, kterou strom sám přečetl (nanejvýš asi 60); katalog regionu označený jako hotový",
733
733
  "history.region.finished": "události regionu {region} jsou hotové",
734
+ "history.gaps.what": "Události dějin v regionu {region} pro roky, kdy katalog žádné nemá: {years}",
735
+ "history.gaps.why": "osoby stromu tam v těch letech žily a u jejich životů se z tehdejších dějin nic neukazuje",
736
+ "history.gaps.done": "zhruba jedna událost na 10–15 let každé mezery, každá ze stránky, kterou strom sám přečetl – nebo nic, když žádná událost hodná zápisu doložená není",
737
+ "history.gaps.filled": "v událostech regionu {region} už žádná mezera není",
738
+ "history.gaps.looked.none": "roky {years} v událostech regionu {region} už prohledal jiný výzkum: doložená událost v nich žádná",
739
+ "history.gaps.looked.some": "roky {years} v událostech regionu {region} už prohledal jiný výzkum: zapsaných událostí v nich: {n}",
740
+ "history.gaps.gone": "roky {years} už pro tento strom nejsou mezerou v událostech regionu {region}: údaje jeho osob v tom regionu se změnily",
734
741
  "review.reread.what": "Přečíst znovu modelem {model}, o co se opírají údaje osoby {name}: {list}{more}",
735
742
  "review.reread.why": "tyto zápisy četl jen jiný model; {model} je ten, kterým uživatel čte teď ({research})",
736
743
  "review.reread.done": "zápis každého údaje přečtený naslepo modelem {model} (strom read M… --blind): shoda → poznámka u údaje; rozdíl → rozpor s oběma čteními",
@@ -2692,6 +2699,7 @@
2692
2699
  "ui.plan.history.asked": "; přednost: {region}, na přání",
2693
2700
  "ui.plan.history.order": "Regiony na řadě, po jednom: {list}",
2694
2701
  "ui.plan.history.weight": "{region} (osob: {n})",
2702
+ "ui.plan.history.gaps": "{region}, roky bez událostí {years} (osob: {n})",
2695
2703
  "ui.plan.history.prefer": "; přednost: místo {place} z nedávného sezení",
2696
2704
  "ui.link.experiment.all": "Zapíná se v nastavení tohoto počítače: platí pro všechny výzkumy na něm (každý jde vypnout zvlášť).",
2697
2705
  "ui.link.experiment.on.done": "Hledání událostí dějin je pro tento výzkum zapnuté.",
@@ -731,6 +731,13 @@
731
731
  "history.region.why": "was die Vorfahren erlebten, bei ihren Lebensläufen gezeigt",
732
732
  "history.region.done": "die wichtigen Ereignisse des Zeitraums gefunden, jedes von einer Seite, die strom selbst gelesen hat (höchstens etwa 60); der Katalog der Region als fertig vermerkt",
733
733
  "history.region.finished": "die Ereignisse von {region} sind fertig",
734
+ "history.gaps.what": "Ereignisse der Geschichte in {region} für die Jahre, in denen der Katalog keines hat: {years}",
735
+ "history.gaps.why": "Personen des Stammbaums lebten in diesen Jahren dort, und bei ihren Lebensläufen erscheint nichts von der damaligen Geschichte",
736
+ "history.gaps.done": "etwa ein Ereignis alle 10–15 Jahre jeder Lücke, jedes von einer Seite, die strom selbst gelesen hat – oder keines, wenn kein Ereignis, das einen Eintrag wert ist, belegt ist",
737
+ "history.gaps.filled": "in den Ereignissen von {region} ist keine Lücke mehr",
738
+ "history.gaps.looked.none": "die Jahre {years} in den Ereignissen von {region} hat schon eine andere Forschung durchsucht: kein Ereignis darin belegt",
739
+ "history.gaps.looked.some": "die Jahre {years} in den Ereignissen von {region} hat schon eine andere Forschung durchsucht: darin geschriebene Ereignisse: {n}",
740
+ "history.gaps.gone": "die Jahre {years} sind für diesen Stammbaum keine Lücke in den Ereignissen von {region} mehr: die Angaben seiner Personen dort haben sich geändert",
734
741
  "review.reread.what": "Mit {model} neu lesen, worauf die Angaben von {name} beruhen: {list}{more}",
735
742
  "review.reread.why": "diese Einträge hat nur ein anderes Modell gelesen; mit {model} liest der Nutzer jetzt ({research})",
736
743
  "review.reread.done": "der Eintrag jeder Angabe blind mit {model} gelesen (strom read M… --blind): übereinstimmend → Notiz an der Angabe; abweichend → ein Widerspruch mit beiden Lesungen",
@@ -2692,6 +2699,7 @@
2692
2699
  "ui.plan.history.asked": "; zuerst: {region}, auf Bitte",
2693
2700
  "ui.plan.history.order": "Regionen der Reihe nach, eine nach der anderen: {list}",
2694
2701
  "ui.plan.history.weight": "{region} (Personen: {n})",
2702
+ "ui.plan.history.gaps": "{region}, die Jahre ohne Ereignisse {years} (Personen: {n})",
2695
2703
  "ui.plan.history.prefer": "; zuerst: der Ort {place} aus einer kürzlichen Sitzung",
2696
2704
  "ui.link.experiment.all": "Es wird in den Einstellungen dieses Computers eingeschaltet: für alle Forschungen darauf (jede lässt sich einzeln ausschalten).",
2697
2705
  "ui.link.experiment.on.done": "Die Suche nach den Ereignissen der Geschichte ist für diese Forschung eingeschaltet.",
@@ -6,7 +6,7 @@ import { langName } from "../core/lang.js";
6
6
  import { foldText } from "../core/text.js";
7
7
  import { match } from "../cli/registry.js";
8
8
  import { synopsis } from "./sheet.js";
9
- import { catalogIds, EVENT_KINDS, HISTORY_PLACES_ORIGIN, HOLD_DAYS, readCatalog, readHidden, readPlaces, coveredBy, regionName, regionOfTask, regionPeriod, REGION_LANGS, unmappedPlaces, } from "../core/pastevents.js";
9
+ import { catalogIds, EVENT_KINDS, forGaps, GAP_DENSITY, gapsOfTask, gapText, HISTORY_PLACES_ORIGIN, HOLD_DAYS, readCatalog, readHidden, readPlaces, coveredBy, regionName, regionOfTask, regionPeriod, REGION_LANGS, unmappedPlaces, } from "../core/pastevents.js";
10
10
  import { factPlaces } from "../gedcom/export.js";
11
11
  /** About so many events a region at most: rather fewer. */
12
12
  export const REGION_DENSITY = 60;
@@ -44,7 +44,7 @@ const ONLY_FROM_PAGES = [
44
44
  export function historySections(tree, task, shared, alone) {
45
45
  const region = regionOfTask(task);
46
46
  if (region)
47
- return regionSections(tree, region, shared, alone);
47
+ return regionSections(tree, region, shared, alone, forGaps(task) ? gapsOfTask(task) : undefined);
48
48
  if (task.origin === HISTORY_PLACES_ORIGIN)
49
49
  return placesSections(tree, shared, alone);
50
50
  return undefined;
@@ -63,7 +63,10 @@ function closing(alone, lang, lines) {
63
63
  ].join("\n"),
64
64
  };
65
65
  }
66
- function regionSections(tree, region, shared, alone) {
66
+ /** What events a region's catalog keeps: those that shaped the lives of ordinary people there. */
67
+ const SHAPED = "those that shaped the lives of ordinary people there: wars and their levies, epidemics, famines, censuses, changes of the state and its borders, waves of emigration, laws on serfdom, land, schools, religion, great disasters; no lists of rulers, no battles far away, no happening of one village";
68
+ /** A region's session — or, with its gaps, a session filling the gaps of its finished catalog (K2.6). */
69
+ function regionSections(tree, region, shared, alone, gaps) {
67
70
  const lang = tree.lang;
68
71
  const c = shared ? readCatalog(shared, region) : undefined;
69
72
  const name = regionName(c, lang, region);
@@ -80,19 +83,26 @@ function regionSections(tree, region, shared, alone) {
80
83
  if (hiddenOf.has(e.id))
81
84
  byKind.set(e.kind, (byKind.get(e.kind) ?? 0) + hiddenOf.get(e.id));
82
85
  const room = Math.max(0, REGION_DENSITY - live.length);
86
+ const span = (g) => g.to - g.from + 1;
87
+ const wanted = gaps?.reduce((n, g) => n + Math.max(1, Math.round(span(g) / GAP_DENSITY)), 0) ?? 0;
83
88
  const out = [];
84
89
  out.push({
85
90
  name: "history",
86
91
  required: true,
87
92
  pointer: "strom history status",
88
93
  text: [
89
- `## The history of ${name} (${region}) — for the Strom app`,
94
+ gaps ? `## The gaps in the history of ${name} (${region}) — for the Strom app` : `## The history of ${name} (${region}) — for the Strom app`,
90
95
  "What the ancestors lived through: the events of this region's history, shown in the Strom app with the lives of the people of its places. " + NOTHING_ELSE,
91
- `- period: ${period.from}–${period.to} — this tree's places only (the oldest year of their facts in the region, never before 1600, to this year); another tree sharing the region adds its own years when it takes it`,
96
+ gaps
97
+ ? `- the region's catalog is finished, yet these years of this tree's people there have no event: ${gaps.map((g) => `${gapText(g)} (${span(g)} years)`).join(", ")} — look for events of these years alone (strom history add refuses others in this session); what the catalog has (below) is never looked for again`
98
+ : `- period: ${period.from}–${period.to} — this tree's places only (the oldest year of their facts in the region, never before 1600, to this year); another tree sharing the region adds its own years when it takes it`,
92
99
  period.places.length
93
100
  ? `- this tree's places in it: ${period.places.map((p) => `${p.place}${p.from !== undefined || p.to !== undefined ? ` (${p.from ?? "…"}–${p.to ?? "…"})` : ""}${p.years.length ? ` facts ${years(Math.min(...p.years), Math.max(...p.years))}` : ""}`).join(" · ")}`
94
101
  : "",
95
- `- how many: at most about ${REGION_DENSITY} events for the region, rather fewer (${live.length} written${room ? `, room for about ${room} more` : ": enough — only what is missing badly"}) — those that shaped the lives of ordinary people there: wars and their levies, epidemics, famines, censuses, changes of the state and its borders, waves of emigration, laws on serfdom, land, schools, religion, great disasters; no lists of rulers, no battles far away, no happening of one village`,
102
+ gaps
103
+ ? `- how many: about one event every 10–15 years of each gap (about ${wanted} in all) — ${SHAPED}; none documented worth writing: nothing — a year is never filled for its own sake`
104
+ : `- how many: at most about ${REGION_DENSITY} events for the region, rather fewer (${live.length} written${room ? `, room for about ${room} more` : ": enough — only what is missing badly"}) — ${SHAPED}`,
105
+ gaps ? "" : "- spread over the period: the people of every part of it lived through something — rather an event each 10–15 years of it than many of a few decades",
96
106
  `- kinds: ${EVENT_KINDS.join(", ")}`,
97
107
  `- each event: its years (the day only when sure: --date); its texts in ${REGION_LANGS.join(", ")}, each ≤ 300 characters — factual and impersonal: what happened, where, how many; never addressed to anyone, never what to do, never speaking as yourself; --why-*: why it mattered for the people of the place; --tip-*: where a research may look because of it (a kind of record, an archive's series), impersonal too`,
98
108
  "- a hard event (persecution, deportation, expulsion, famine, a pogrom): --sensitive; sober and factual, no judgement, no words of horror, and never a guess about the people of this tree — whom it touched is the reader's to see",
@@ -129,20 +139,25 @@ function regionSections(tree, region, shared, alone) {
129
139
  text: [
130
140
  `## Found already for the same places and years: ${cover.join(", ")} (finished, ${theirs.length} events) — the Strom app shows them at these places`,
131
141
  "- write only what they lack: an event of the whole state they do not have; one of them again is never written (not even with --same-as)",
132
- `- they lack nothing worth writing: strom history done ${region} at once, no page read`,
142
+ gaps ? "- they lack nothing worth writing: strom session close --done at once, no page read" : `- they lack nothing worth writing: strom history done ${region} at once, no page read`,
133
143
  ...theirs.slice(0, EVENTS_SHOWN).map(({ e }) => ` ${e.id} ${years(e.from, e.to)} ${e.kind} "${short(e.text[lang] ?? e.text.en ?? Object.values(e.text)[0] ?? "")}"`),
134
144
  theirs.length > EVENTS_SHOWN ? ` … ${theirs.length - EVENTS_SHOWN} more: strom history status --app --json` : "",
135
145
  ].filter(Boolean).join("\n"),
136
146
  });
137
147
  }
138
- const commands = [cmd(["history", "page"]), cmd(["history", "add"]), cmd(["history", "retract"]), cmd(["history", "status"]), cmd(["history", "done"])].filter(Boolean);
148
+ const commands = [cmd(["history", "page"]), cmd(["history", "add"]), cmd(["history", "retract"]), cmd(["history", "status"]), ...(gaps ? [] : [cmd(["history", "done"])])].filter(Boolean);
139
149
  if (commands.length)
140
150
  out.push({ name: "commands", first: true, pointer: "strom help history add", text: ["## Commands (each option as it is)", ...commands].join("\n") });
141
- out.push(closing(alone, lang, [
142
- `- the events of the period found (or nothing more worth writing): strom history done ${region}, then strom session close --done "how many events, of which kinds, from which sources" --next "…"`,
143
- `- time up before: strom session close --continue --summary "…" --next "the kinds or years not looked at yet" (the region stays this tree's for ${HOLD_DAYS} days)`,
144
- `- nothing could be found or read at all: strom session close --continue --summary "what was tried" --next "…"`,
145
- ]));
151
+ out.push(closing(alone, lang, gaps
152
+ ? [
153
+ `- the gaps looked at — events found, or none documented: strom session close --done "how many events, of which years and kinds, from which sources — or what was looked at" --next "…" (these years are never looked for again)`,
154
+ `- time up before: strom session close --continue --summary "…" --next "the years not looked at yet" (the region stays this tree's for ${HOLD_DAYS} days)`,
155
+ ]
156
+ : [
157
+ `- the events of the period found (or nothing more worth writing): strom history done ${region}, then strom session close --done "how many events, of which kinds, from which sources" --next "…"`,
158
+ `- time up before: strom session close --continue --summary "…" --next "the kinds or years not looked at yet" (the region stays this tree's for ${HOLD_DAYS} days)`,
159
+ `- nothing could be found or read at all: strom session close --continue --summary "what was tried" --next "…"`,
160
+ ]));
146
161
  return out;
147
162
  }
148
163
  function placesSections(tree, shared, alone) {
package/dist/cli/ui.js CHANGED
@@ -2543,6 +2543,7 @@ export const UI = {
2543
2543
  "ui.plan.history.asked": "; first: {region}, as asked",
2544
2544
  "ui.plan.history.order": "Regions in turn, one at a time: {list}",
2545
2545
  "ui.plan.history.weight": "{region} (people: {n})",
2546
+ "ui.plan.history.gaps": "{region}, the years without events {years} (people: {n})",
2546
2547
  "ui.plan.history.prefer": "; first: the place {place} from a recent session",
2547
2548
  "ui.link.experiment.all": "It is turned on in this computer's settings: for every research on it (each can be turned off on its own).",
2548
2549
  "ui.link.experiment.on.done": "The search for the events of history is on for this research.",
@@ -16,7 +16,7 @@ export { humanTask };
16
16
  import { lacksImages, waitsForArchive } from "../core/queue.js";
17
17
  import { clockUp } from "../core/net.js";
18
18
  import { forStory } from "../core/stories.js";
19
- import { forHistory, HISTORY_PLACES_ORIGIN, historyOn, historyTurn, readCatalog, regionName, regionOfTask } from "../core/pastevents.js";
19
+ import { forGaps, forHistory, gapsOfTask, gapText, HISTORY_PLACES_ORIGIN, historyOn, historyTurn, readCatalog, regionName, regionOfTask } from "../core/pastevents.js";
20
20
  import { isArchive } from "../core/mode.js";
21
21
  import { actsAsArchive, stateOf } from "../core/state.js";
22
22
  import { rankedQueue } from "./tasks.js";
@@ -244,7 +244,9 @@ register({
244
244
  if (history?.regions.length)
245
245
  out.push(ui(lang, "ui.plan.history.order", {
246
246
  list: [
247
- ...history.regions.slice(0, HISTORY_REGIONS).map((r) => ui(lang, "ui.plan.history.weight", { region: r.name, n: r.persons })),
247
+ ...history.regions
248
+ .slice(0, HISTORY_REGIONS)
249
+ .map((r) => (r.gaps ? ui(lang, "ui.plan.history.gaps", { region: r.name, years: r.gaps.join(", "), n: r.persons }) : ui(lang, "ui.plan.history.weight", { region: r.name, n: r.persons }))),
248
250
  ...(history.regions.length > HISTORY_REGIONS ? [`… (+${history.regions.length - HISTORY_REGIONS})`] : []),
249
251
  ].join(" · "),
250
252
  }));
@@ -321,11 +323,14 @@ function historyPlan(ctx, tree) {
321
323
  const prefer = !asked && turn.prefer?.task === task.id ? turn.prefer.place : undefined;
322
324
  const when = task.origin === HISTORY_PLACES_ORIGIN ? ui(lang, "ui.plan.history.places") : turn.now ? ui(lang, "ui.plan.history.now") : turn.in === undefined ? ui(lang, "ui.plan.history.idle") : ui(lang, "ui.plan.history.in", { n: Math.max(1, turn.in) });
323
325
  const weight = new Map((turn.order ?? []).map((w) => [w.id, w]));
324
- // the regions as the queue ranks their tasks: asked for, a recent session's place, then by weight
325
- const regions = ranked
326
- .map((r) => regionOfTask(r.task))
327
- .filter((id) => !!id)
328
- .map((id) => ({ id, name: name(id), persons: weight.get(id)?.persons ?? 0 }));
326
+ // the regions as the queue ranks their tasks: asked for, a recent session's place, then by weight (the gaps of a
327
+ // finished one with their years)
328
+ const regions = ranked.flatMap((r) => {
329
+ const id = regionOfTask(r.task);
330
+ if (!id)
331
+ return [];
332
+ return [{ id, name: name(id), persons: weight.get(id)?.persons ?? 0, ...(forGaps(r.task) ? { gaps: gapsOfTask(r.task).map(gapText) } : {}) }];
333
+ });
329
334
  return {
330
335
  task,
331
336
  turn,
@@ -13,7 +13,7 @@ import { clipMark } from "../core/excerpt.js";
13
13
  import { resolvePerson } from "../core/people.js";
14
14
  import { checkQuotes, cleanText, findPassages, mainQuote, pageParts, PageError, quotesShape, readPage, PART_CHARS } from "../core/pagecheck.js";
15
15
  import { currentSession } from "../core/session.js";
16
- import { addLocalEvent, addRegionEvent, askFirst, checkKind, checkTexts, checkYears, ensureRegion, factPlaceKey, hideEvent, historyAnswer, historyStatus, forHistory, historyOrder, historyTurn, mapPlace, markDone, noteTried, readCatalog, redoRegion, refuseBeforePage, REGION_LANGS, regionIdOk, regionOfTask, requireHistoryOn, requireRegionId, retractEvent, EVENT_KINDS, } from "../core/pastevents.js";
16
+ import { addLocalEvent, addRegionEvent, askFirst, checkKind, checkTexts, checkYears, ensureRegion, factPlaceKey, hideEvent, historyAnswer, historyStatus, forGaps, forHistory, gapsOfTask, gapText, historyOrder, historyTurn, mapPlace, markDone, noteTried, readCatalog, redoRegion, refuseBeforePage, REGION_LANGS, regionIdOk, regionOfTask, requireHistoryOn, requireRegionId, retractEvent, EVENT_KINDS, } from "../core/pastevents.js";
17
17
  function shared(ctx) {
18
18
  const s = ctx.settings.shared()?.value;
19
19
  if (!s)
@@ -124,12 +124,21 @@ register({
124
124
  return "asked first";
125
125
  if (turn?.prefer?.task === t.id)
126
126
  return `first: ${turn.prefer.place}`;
127
- return w ? `${w.persons} people, ${w.facts} facts` : "";
127
+ const gaps = forGaps(t) ? `gaps ${gapsOfTask(t).map(gapText).join(", ")}` : "";
128
+ return [gaps, w ? `${w.persons} people, ${w.facts} facts` : ""].filter(Boolean).join(" · ");
128
129
  };
129
130
  const when = !turn ? undefined : turn.places ? `the next session, the places to map first (${turn.places})` : turn.pace === "idle" ? "when the family research has nothing to do" : turn.now ? "the next session" : `in ~${Math.max(1, turn.in ?? 0)} sessions`;
130
131
  return {
131
132
  text: lines(`history of the places: ${s.on ? "on" : "off (the user alone turns it on: strom config set experiments.history on)"} · search: ${state.search}${state.why ? ` (${state.why})` : ""}`, `places of the facts: ${s.places.total} · mapped ${s.places.mapped} · not located ${s.places.unlocated} · waiting ${s.places.pending.length}`, shown.length ? `waiting: ${shown.join(" · ")}${s.places.pending.length > shown.length ? ` … (+${s.places.pending.length - shown.length}, --limit)` : ""}` : undefined, s.regions.length
132
- ? table(s.regions.map((r) => [r.id, r.name?.en ?? "(no catalog)", `${r.events} events`, r.retracted ? `${r.retracted} taken back` : "", r.done ? `done ${r.done.slice(0, 10)}` : r.heldBy ? `held by ${r.heldBy}` : "open", r.tried ? `${r.tried} pages not read` : ""]))
133
+ ? table(s.regions.map((r) => [
134
+ r.id,
135
+ r.name?.en ?? "(no catalog)",
136
+ `${r.events} events`,
137
+ r.retracted ? `${r.retracted} taken back` : "",
138
+ r.done ? `done ${r.done.slice(0, 10)}` : r.heldBy ? `held by ${r.heldBy}` : "open",
139
+ r.gaps?.length ? `gaps: ${r.gaps.map(gapText).join(", ")}` : "",
140
+ r.tried ? `${r.tried} pages not read` : "",
141
+ ]))
133
142
  : "no regions yet", `local events: ${s.local.events}${s.local.retracted ? ` (${s.local.retracted} taken back)` : ""} · hidden by the person: ${s.hidden.total}`, turn ? `pace: ${turn.pace} (strom config set experiments.historyPace sometimes|rare|idle) · ${turn.since} sessions since the last of history · strom run takes the next: ${when}${turn.asked ? ` · first: ${turn.asked.task} (asked for ${turn.asked.region})` : turn.prefer ? ` · first: ${turn.prefer.task} (a recent session worked at ${turn.prefer.place})` : ""}` : undefined, tasks.length
134
143
  ? lines("tasks of history in the order strom run takes them, one a turn (strom run --task <T…> starts one now):", table(tasks.map((t, i) => [` ${i + 1}.`, t.id, t.origin, t.state, why(t)])))
135
144
  : s.on
@@ -265,6 +274,17 @@ register({
265
274
  if (shape)
266
275
  throw new UsageError(`--quote: ${shape}`, { code: "history.quote", params: { why: shape } });
267
276
  const s = sameness(opts);
277
+ // a session filling a finished catalog's gaps: an event of those years alone, before any request
278
+ const own = currentSession(tree, ctx.env, ctx.refs)?.task;
279
+ const ownTask = own ? tree.get(own) : undefined;
280
+ if (ownTask && forGaps(ownTask) && regionOfTask(ownTask) === region) {
281
+ const gaps = gapsOfTask(ownTask);
282
+ if (gaps.length && !gaps.some((g) => from <= g.to && g.from <= to))
283
+ throw new UsageError(`this session fills the gaps of ${region} only (${gaps.map(gapText).join(", ")}): ${from === to ? from : `${from}–${to}`} is outside them`, {
284
+ code: "history.gaps.outside",
285
+ params: { region },
286
+ });
287
+ }
268
288
  refuseBeforePage(sh, region, { kind, from, to }, s);
269
289
  let checked;
270
290
  try {
@@ -30,13 +30,23 @@ import { lineValue } from "../gedcom/lines.js";
30
30
  export const HISTORY_PLACES_ORIGIN = "history:places";
31
31
  /** The prefix of a region's task (`history:region:<id>`, one a region). */
32
32
  export const HISTORY_REGION_ORIGIN = "history:region:";
33
+ /** The prefix of a task that fills the gaps of a region's finished catalog (`history:gaps:<id>`, one a region). */
34
+ export const HISTORY_GAPS_ORIGIN = "history:gaps:";
33
35
  /** The level the tasks of history take (an existing one: a new value of the list would raise SCHEMA_VERSION). */
34
36
  export const HISTORY_LEVEL = "enrich";
35
37
  /** A region's task origin. */
36
38
  export function regionOrigin(region) {
37
39
  return `${HISTORY_REGION_ORIGIN}${region}`;
38
40
  }
39
- /** Is it a task of the history (history:places, history:region:<id>)? */
41
+ /** A region's task to fill the gaps of its finished catalog. */
42
+ export function gapsOrigin(region) {
43
+ return `${HISTORY_GAPS_ORIGIN}${region}`;
44
+ }
45
+ /** Is it a task that fills the gaps of a region's finished catalog (history:gaps:<id>)? */
46
+ export function forGaps(t) {
47
+ return typeof t.origin === "string" && t.origin.startsWith(HISTORY_GAPS_ORIGIN);
48
+ }
49
+ /** Is it a task of the history (history:places, history:region:<id>, history:gaps:<id>)? */
40
50
  export function forHistory(t) {
41
51
  return typeof t.origin === "string" && t.origin.startsWith("history:");
42
52
  }
@@ -454,6 +464,7 @@ export function redoRegion(shared, region) {
454
464
  throw new UsageError(`there is no region ${region}`, { code: "history.region.missing", params: { region } });
455
465
  delete c.done;
456
466
  delete c.holder;
467
+ delete c.gaps;
457
468
  c.redo = new Date().toISOString();
458
469
  writeCatalog(shared, c);
459
470
  return c;
@@ -900,6 +911,7 @@ export function historyStatus(tree, shared, settings) {
900
911
  ...(c?.done ? { done: c.done.at } : {}),
901
912
  ...(c && heldElsewhere(c, tree.config.id) ? { heldBy: c.holder.tree } : {}),
902
913
  tried: c?.tried.length ?? 0,
914
+ ...(c?.done ? { gaps: regionGaps(tree, shared, id) } : {}),
903
915
  };
904
916
  });
905
917
  const kindOf = new Map();
@@ -946,8 +958,13 @@ export function mergeCatalog(shared, given) {
946
958
  }
947
959
  const ids = new Set(had.events.map((e) => e.id));
948
960
  const more = given.events.filter((e) => typeof e?.id === "string" && !ids.has(e.id));
949
- if (!more.length)
961
+ // the gaps another computer's tree looked at: not looked at here again
962
+ const seen = new Set((had.gaps ?? []).map((g) => `${g.from}-${g.to}-${g.tree}`));
963
+ const gaps = (Array.isArray(given.gaps) ? given.gaps : []).filter((g) => Number.isInteger(g?.from) && Number.isInteger(g?.to) && !seen.has(`${g.from}-${g.to}-${g.tree}`));
964
+ if (!more.length && !gaps.length)
950
965
  return { added: 0 };
966
+ if (gaps.length)
967
+ had.gaps = [...(had.gaps ?? []), ...gaps];
951
968
  had.events.push(...more);
952
969
  had.events.sort((a, b) => a.id.localeCompare(b.id));
953
970
  const top = Math.max(0, ...had.events.map((e) => Number(/-(\d+)$/.exec(e.id)?.[1] ?? 0)));
@@ -970,9 +987,14 @@ export function unmappedPlaces(tree) {
970
987
  export function regionName(c, lang, id = c?.region.id ?? "") {
971
988
  return c?.region.name[lang] ?? c?.region.name.en ?? id;
972
989
  }
973
- /** The region of a task of history (history:region:<id>), else undefined. */
990
+ /** The region of a task of history (history:region:<id>, its gaps history:gaps:<id>), else undefined. */
974
991
  export function regionOfTask(t) {
975
- return typeof t.origin === "string" && t.origin.startsWith(HISTORY_REGION_ORIGIN) ? t.origin.slice(HISTORY_REGION_ORIGIN.length) || undefined : undefined;
992
+ if (typeof t.origin !== "string")
993
+ return undefined;
994
+ for (const prefix of [HISTORY_REGION_ORIGIN, HISTORY_GAPS_ORIGIN])
995
+ if (t.origin.startsWith(prefix))
996
+ return t.origin.slice(prefix.length) || undefined;
997
+ return undefined;
976
998
  }
977
999
  /** History is looked for from this year at the earliest. */
978
1000
  const EARLIEST_YEAR = 1600;
@@ -998,16 +1020,208 @@ export function regionPeriod(tree, region) {
998
1020
  const from = Number.isFinite(oldest) ? Math.min(to, Math.max(EARLIEST_YEAR, oldest)) : EARLIEST_YEAR;
999
1021
  return { from, to, places: places.sort((a, b) => a.place.localeCompare(b.place)) };
1000
1022
  }
1023
+ // ── the gaps of a finished catalog (K2.6) ──────────────────────────────────
1024
+ /** A gap: more years than so many in a row the tree's people lived in a region with no event of its catalog. */
1025
+ export const GAP_YEARS = 15;
1026
+ /** A person lived in a region so many years before the first fact there and after the last. */
1027
+ export const LIVED_MARGIN = 5;
1028
+ /** An event of so many years or more (a state, a period of rule) marks only its beginning and its end, not the years between. */
1029
+ const LONG_EVENT = GAP_YEARS;
1030
+ /** About one event in so many years of a gap: what a session filling it looks for. */
1031
+ export const GAP_DENSITY = 12;
1032
+ /** A gap's years as a task and the brief write them: 1870–1913. */
1033
+ export function gapText(g) {
1034
+ return g.from === g.to ? String(g.from) : `${g.from}–${g.to}`;
1035
+ }
1036
+ /** The gaps a task of them was raised for (its `where`: 1870–1913 each). */
1037
+ export function gapsOfTask(t) {
1038
+ const out = [];
1039
+ for (const w of t.where ?? []) {
1040
+ const m = /^(\d{3,4})(?:\s*[–-]\s*(\d{3,4}))?$/u.exec(w.trim());
1041
+ if (m)
1042
+ out.push({ from: Number(m[1]), to: Number(m[2] ?? m[1]) });
1043
+ }
1044
+ return out;
1045
+ }
1046
+ /** Merged spans, both ends in: touching or overlapping ones joined. */
1047
+ function union(spans) {
1048
+ const out = [];
1049
+ for (const s of [...spans].sort((a, b) => a.from - b.from)) {
1050
+ const last = out.at(-1);
1051
+ if (last && s.from <= last.to + 1)
1052
+ last.to = Math.max(last.to, s.to);
1053
+ else
1054
+ out.push({ ...s });
1055
+ }
1056
+ return out;
1057
+ }
1058
+ /**
1059
+ * The years the tree's people lived in a region: each person from the first year of a fact at its places (in the years
1060
+ * each place belonged to it) to the last, LIVED_MARGIN before and after, within the years of those places' links and
1061
+ * never before 1600 or after this year — the spans of all of them joined. A family's fact counts for its partners;
1062
+ * a fact without a year counts for nothing.
1063
+ */
1064
+ export function livedSpans(tree, region) {
1065
+ const r = reading(tree, undefined);
1066
+ const now = new Date().getFullYear();
1067
+ const of = new Map();
1068
+ const count = (who, e) => {
1069
+ if (!factWritten(e) || !e.place || !e.date)
1070
+ return;
1071
+ const key = lineValue(e.place);
1072
+ const m = key ? r.mapOf(key) : undefined;
1073
+ if (!m)
1074
+ return;
1075
+ const years = dateYears(e.date);
1076
+ for (const g of m.regions) {
1077
+ if (g.id !== region)
1078
+ continue;
1079
+ const inside = years.filter((y) => inRegion({ from: y, to: y }, g));
1080
+ if (!inside.length)
1081
+ continue;
1082
+ for (const p of who) {
1083
+ const w = of.get(p) ?? { years: [], lo: Infinity, hi: -Infinity };
1084
+ of.set(p, w);
1085
+ w.years.push(...inside);
1086
+ w.lo = Math.min(w.lo, g.from ?? -Infinity);
1087
+ w.hi = Math.max(w.hi, g.to ?? Infinity);
1088
+ }
1089
+ }
1090
+ };
1091
+ const persons = tree.list("person").filter((p) => !p.retracted);
1092
+ const alive = new Set(persons.map((p) => p.id));
1093
+ for (const p of persons)
1094
+ for (const e of p.events)
1095
+ count([p.id], e);
1096
+ for (const f of tree.list("family")) {
1097
+ const who = f.partners.filter((p) => alive.has(p));
1098
+ if (who.length)
1099
+ for (const e of f.events)
1100
+ count(who, e);
1101
+ }
1102
+ const spans = [];
1103
+ for (const w of of.values()) {
1104
+ const from = Math.max(EARLIEST_YEAR, w.lo, Math.min(...w.years) - LIVED_MARGIN);
1105
+ const to = Math.min(now, w.hi, Math.max(...w.years) + LIVED_MARGIN);
1106
+ if (from <= to)
1107
+ spans.push({ from, to });
1108
+ }
1109
+ return union(spans);
1110
+ }
1001
1111
  /**
1002
- * A region taken by this tree for a session on its task: the holder written into the shared catalog (refreshed when it
1112
+ * The gaps of a region's finished catalog for this tree (K2.6, Milan 28 Oct 2026 — a land's catalog with nothing
1113
+ * between 1869 and 1914 while the tree's people lived there): the runs of more than GAP_YEARS years the tree's people
1114
+ * lived there (livedSpans) with no event of the catalog — nor of the finished catalogs that hold its places and years
1115
+ * (coverOf: a state after its lands) — and not looked at before: the gaps a session of any tree looked at (the
1116
+ * catalog's `gaps`, noteGaps: a task of them done writes them there), this tree's tasks of them dropped (since the
1117
+ * person last asked for the region again). A task strom closed itself (raiseHistoryTasks: filled, looked at elsewhere)
1118
+ * is no look: events taken back later, its years are a gap again. An event of LONG_EVENT years or more marks its two
1119
+ * ends only. None for a catalog not finished (its own task looks for all), nor without people there. Nothing written.
1120
+ */
1121
+ export function regionGaps(tree, shared, region) {
1122
+ if (!shared)
1123
+ return [];
1124
+ const r = reading(tree, shared);
1125
+ const c = r.catalog(region);
1126
+ if (!c?.done)
1127
+ return [];
1128
+ const lived = livedSpans(tree, region);
1129
+ if (!lived.length)
1130
+ return [];
1131
+ const covered = eventYears(r, region);
1132
+ const mark = (g) => {
1133
+ for (let y = g.from; y <= g.to; y++)
1134
+ covered.add(y);
1135
+ };
1136
+ for (const g of c.gaps ?? [])
1137
+ mark(g);
1138
+ for (const t of tree.list("task"))
1139
+ if (t.origin === gapsOrigin(region) && t.state === "dropped" && t.updated >= (c.redo ?? ""))
1140
+ for (const g of gapsOfTask(t))
1141
+ mark(g);
1142
+ return openRuns(lived, covered);
1143
+ }
1144
+ /** The years the events of a region's catalog (and of the finished catalogs that hold it) mark: a long one its ends. */
1145
+ function eventYears(r, region) {
1146
+ const covered = new Set();
1147
+ for (const id of [region, ...coverOf(r, region)])
1148
+ for (const e of r.catalog(id)?.events ?? []) {
1149
+ if (e.retracted)
1150
+ continue;
1151
+ if (e.to - e.from + 1 >= LONG_EVENT) {
1152
+ covered.add(e.from);
1153
+ covered.add(e.to);
1154
+ }
1155
+ else
1156
+ for (let y = e.from; y <= e.to; y++)
1157
+ covered.add(y);
1158
+ }
1159
+ return covered;
1160
+ }
1161
+ /** The runs of more than GAP_YEARS years of these spans no year of `covered` falls in. */
1162
+ function openRuns(spans, covered) {
1163
+ const out = [];
1164
+ for (const span of spans) {
1165
+ let start;
1166
+ for (let y = span.from; y <= span.to + 1; y++) {
1167
+ const open = y <= span.to && !covered.has(y);
1168
+ if (open && start === undefined)
1169
+ start = y;
1170
+ if (!open && start !== undefined) {
1171
+ if (y - start > GAP_YEARS)
1172
+ out.push({ from: start, to: y - 1 });
1173
+ start = undefined;
1174
+ }
1175
+ }
1176
+ }
1177
+ return out;
1178
+ }
1179
+ /**
1180
+ * Why a gaps' task has nothing left to do (regionGaps none of its years): its years filled by the events of the
1181
+ * catalog ("filled"); else looked at by another tree's session (the catalog's `gaps`: "looked", with the events it
1182
+ * wrote in them); else no gap for this tree any more — its people's facts there changed ("gone").
1183
+ */
1184
+ export function gapsClosed(tree, shared, region, t) {
1185
+ const r = reading(tree, shared);
1186
+ const mine = gapsOfTask(t);
1187
+ if (!openRuns(mine, eventYears(r, region)).length)
1188
+ return { why: "filled", added: 0 };
1189
+ const looked = (r.catalog(region)?.gaps ?? []).filter((g) => g.tree !== tree.config.id && mine.some((m) => g.from <= m.to && m.from <= g.to));
1190
+ if (looked.length)
1191
+ return { why: "looked", added: looked.reduce((n, g) => n + (g.added || 0), 0) };
1192
+ return { why: "gone", added: 0 };
1193
+ }
1194
+ /**
1195
+ * A gaps' task finished (strom session close --done): its gaps written into the shared catalog as looked at — with the
1196
+ * events this tree wrote in them since the task was made, none too — and the region let go when this tree held it.
1197
+ */
1198
+ export function noteGaps(shared, region, t, treeId) {
1199
+ const gaps = gapsOfTask(t);
1200
+ if (!regionIdOk(region) || !gaps.length)
1201
+ return;
1202
+ withCatalogs(shared, () => {
1203
+ const c = readCatalog(shared, region);
1204
+ if (!c)
1205
+ return;
1206
+ const at = new Date().toISOString();
1207
+ const mine = c.events.filter((e) => !e.retracted && e.by === treeId && e.at >= t.created);
1208
+ c.gaps = [...(c.gaps ?? []), ...gaps.map((g) => ({ ...g, at, tree: treeId, added: mine.filter((e) => overlap(e, g)).length }))];
1209
+ if (c.holder?.tree === treeId)
1210
+ delete c.holder;
1211
+ writeCatalog(shared, c);
1212
+ });
1213
+ }
1214
+ /**
1215
+ * A region taken by this tree for a session on its task (or on its gaps): the holder written into the shared catalog (refreshed when it
1003
1216
  * is this tree's already). Another tree's hold within HOLD_DAYS stays: its ID is returned (nothing written).
1004
1217
  */
1005
- export function takeRegion(shared, region, treeId) {
1218
+ export function takeRegion(shared, region, treeId, gaps = false) {
1006
1219
  if (!regionIdOk(region))
1007
1220
  return undefined;
1008
1221
  return withCatalogs(shared, () => {
1009
1222
  const c = readCatalog(shared, region);
1010
- if (!c || c.done)
1223
+ // a finished catalog is held only while its gaps are filled
1224
+ if (!c || (c.done && !gaps))
1011
1225
  return undefined;
1012
1226
  if (heldElsewhere(c, treeId))
1013
1227
  return c.holder.tree;
@@ -1275,7 +1489,8 @@ export function historyHeld(tree, shared, settings = new Settings(tree.env, {}))
1275
1489
  /**
1276
1490
  * The tasks of history raised while the experiment is on in a research (never in an archive, nor one waiting for its
1277
1491
  * agent, never in a dry run): one to map the places of the facts while any is not mapped, one for each region of the
1278
- * tree's places whose catalog is not finished — level enrich, priority 1, after all the research in the queue. Never a
1492
+ * tree's places whose catalog is not finished, one for the gaps of a finished one (regionGaps) — level enrich,
1493
+ * priority 1, after all the research in the queue. Never a
1279
1494
  * second one of the same origin open; a places' task again only for a place the last one did not name; a region's task
1280
1495
  * dropped is the answer until the person asks for the region again (redo); one whose catalog was finished meanwhile
1281
1496
  * (another tree, strom history done) is done. Inside the tree's lock, committed by the caller. Returns what it made.
@@ -1288,6 +1503,36 @@ export function raiseHistoryTasks(tree, shared, settings = new Settings(tree.env
1288
1503
  const tasks = tree.list("task").filter(forHistory);
1289
1504
  const made = [];
1290
1505
  const add = (fields) => made.push(create(tree, "task", { level: HISTORY_LEVEL, priority: 1, subject: [], state: "open", ...fields }, (id) => `+${id} task "${fields.what.slice(0, 60)}" (history)`).id);
1506
+ // a finished catalog with gaps in the years of the tree's people there: one task to fill them (K2.6), paced as a
1507
+ // region's; none left (filled meanwhile, by another tree too): its waiting task is done — one at work closes itself
1508
+ const raiseGaps = (id, c) => {
1509
+ const origin = gapsOrigin(id);
1510
+ const live = tasks.filter((t) => t.origin === origin && LIVE_STATES.has(t.state));
1511
+ const gaps = regionGaps(tree, shared, id);
1512
+ if (!gaps.length) {
1513
+ // its result says why, truly: filled, looked at by another research (found or not), or no gap here any more
1514
+ for (const t of live.filter((x) => x.state === "open" || x.state === "parked")) {
1515
+ const { why, added } = gapsClosed(tree, shared, id, t);
1516
+ const key = why === "looked" ? (added ? "history.gaps.looked.some" : "history.gaps.looked.none") : why === "gone" ? "history.gaps.gone" : "history.gaps.filled";
1517
+ const result = phrase(lang, key, { region: regionName(c, lang, id), years: t.where.join(", "), n: added });
1518
+ update(tree, t.id, "task", (x) => ({ ...x, state: "done", result }), {
1519
+ op: "task.done",
1520
+ summary: `${t.id} done: no gap left in the catalog of ${id} (${why})`,
1521
+ });
1522
+ }
1523
+ return;
1524
+ }
1525
+ if (live.length)
1526
+ return;
1527
+ const years = gaps.map(gapText);
1528
+ add({
1529
+ what: phrase(lang, "history.gaps.what", { region: regionName(c, lang, id), years: years.join(", ") }),
1530
+ where: years,
1531
+ why: phrase(lang, "history.gaps.why"),
1532
+ doneWhen: phrase(lang, "history.gaps.done"),
1533
+ origin,
1534
+ });
1535
+ };
1291
1536
  // the places of the facts
1292
1537
  const unmapped = unmappedPlaces(tree);
1293
1538
  const ofPlaces = tasks.filter((t) => t.origin === HISTORY_PLACES_ORIGIN);
@@ -1317,6 +1562,7 @@ export function raiseHistoryTasks(tree, shared, settings = new Settings(tree.env
1317
1562
  op: "task.done",
1318
1563
  summary: `${t.id} done: the catalog of ${id} is finished`,
1319
1564
  });
1565
+ raiseGaps(id, c);
1320
1566
  continue;
1321
1567
  }
1322
1568
  if (mine.some((t) => LIVE_STATES.has(t.state)))
@@ -1336,11 +1582,16 @@ export function raiseHistoryTasks(tree, shared, settings = new Settings(tree.env
1336
1582
  return made;
1337
1583
  });
1338
1584
  }
1339
- /** A region's task finished (strom session close --done): its catalog finished too, when it is not yet. */
1585
+ /**
1586
+ * A region's task finished (strom session close --done): its catalog finished too, when it is not yet; a task of its
1587
+ * gaps: those gaps looked at (noteGaps).
1588
+ */
1340
1589
  export function finishRegionOf(tree, shared, t) {
1341
1590
  const region = regionOfTask(t);
1342
1591
  if (!region || !shared || tree.dryRun)
1343
1592
  return;
1593
+ if (forGaps(t))
1594
+ return noteGaps(shared, region, t, tree.config.id);
1344
1595
  const c = readCatalog(shared, region);
1345
1596
  if (c && !c.done)
1346
1597
  markDone(shared, region, tree.config.id);
@@ -83,6 +83,13 @@ export const PHRASES = {
83
83
  "history.region.why": "what the ancestors lived through, shown with their lives",
84
84
  "history.region.done": "the important events of the period found, each from a page strom read itself (about 60 at most); the region's catalog marked finished",
85
85
  "history.region.finished": "the events of {region} are finished",
86
+ "history.gaps.what": "Events of history in {region} for the years its catalog has none: {years}",
87
+ "history.gaps.why": "the people of the tree lived there in those years, and their lives show nothing of the history then",
88
+ "history.gaps.done": "about one event every 10–15 years of each gap, each from a page strom read itself — or nothing, when no event worth writing is documented",
89
+ "history.gaps.filled": "no gap left in the events of {region}",
90
+ "history.gaps.looked.none": "the years {years} of the events of {region} were looked through by another research: no event documented in them",
91
+ "history.gaps.looked.some": "the years {years} of the events of {region} were looked through by another research: events written in them: {n}",
92
+ "history.gaps.gone": "the years {years} are no gap in the events of {region} for this tree any more: the facts of its people there changed",
86
93
  "review.reread.what": "Read again with {model} what the facts of {name} rest on: {list}{more}",
87
94
  "review.reread.why": "only another model read these entries; {model} is the one the user reads with now ({research})",
88
95
  "review.reread.done": "each fact's entry read blind with {model} (strom read M… --blind): agreed → noted on the fact; differs → a conflict with both readings",
@@ -20,7 +20,7 @@ import { readCodexConfig } from "../agents/effort.js";
20
20
  import { clearSessionWait, permissionWait, waitingForDecision } from "./archivewait.js";
21
21
  import { forgetLeftAlone } from "./leftalone.js";
22
22
  import { Settings } from "./config.js";
23
- import { regionOfTask, takeRegion } from "./pastevents.js";
23
+ import { forGaps, regionOfTask, takeRegion } from "./pastevents.js";
24
24
  function currentFile(tree, env = tree.env) {
25
25
  const w = env.STROM_WORKER;
26
26
  return path.join(tree.root, ".strom", isWorkerId(w) ? `session-${w}.json` : "session.json");
@@ -190,11 +190,12 @@ export function startSession(tree, opts) {
190
190
  const doing = { ...rest, state: "doing", updated: now() };
191
191
  tree.put(doing, { op: "task.start", targets: [doing.id, s.id], summary: `${doing.id} start` });
192
192
  }
193
- // a region's task of history: this tree holds the region while it builds its catalog (another tree's task waits)
193
+ // a region's task of history: this tree holds the region while it builds its catalog, or fills its gaps (another
194
+ // tree's task waits)
194
195
  const region = task ? regionOfTask(task) : undefined;
195
196
  const shared = region && !tree.dryRun ? new Settings(tree.env, {}).shared()?.value : undefined;
196
197
  if (region && shared)
197
- takeRegion(shared, region, tree.config.id);
198
+ takeRegion(shared, region, tree.config.id, forGaps(task));
198
199
  setCurrent(tree, s.id);
199
200
  return s;
200
201
  });
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.22.0-beta.5";
32
+ export const VERSION = "1.22.0-beta.6";
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.22.0-beta.5",
3
+ "version": "1.22.0-beta.6",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {