strom-research 1.24.0-beta.1 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.cs.md CHANGED
@@ -7,31 +7,37 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
7
7
 
8
8
  ## 1.24.0
9
9
 
10
- *Připravuje se: bety do 1.24.0-beta.1. Oddíl bude úplný s vydáním.*
10
+ *31. 10. 2026 · patří k aplikaci Strom 3.19.0.*
11
11
 
12
12
  - **Místa zmapovaná jednou pro všechny výzkumy na tomto počítači.** Místo, které jiný výzkum tady už přiřadil k regionům
13
13
  dějin, se převezme, když jde jistě o totéž místo (stejný název a stejné souřadnice, farnost nebo okres) a všechny
14
14
  výzkumy ho přiřazují stejně; přechází jen přiřazení místa, nic z rodiny. Místo stejného jména, které jistě totéž
15
- není, dostane agent jako návrh; sám přiřazuje jen zbytek. `strom history adopt --dry-run` řekne, co by se převzalo.
15
+ není, dostane agent jako návrh; sám přiřazuje jen zbytek. `strom history adopt --dry-run` řekne, co by se převzalo. Otázka na zapnutí nebo vypnutí událostí
16
+ dějin říká, že platí pro všechny výzkumy na tomto počítači, i jak to změnit jen pro jeden výzkum.
16
17
  - **Opravy výzkumu pro aplikaci Strom.** Most vypíše, co `strom fixes` ve výzkumu nabízí (název, co dělá, kolik
17
18
  položek, stav, jestli jde vrátit), zdarma a bez zápisu; odkaz `strom-research://fixes` otevře opravu v terminálu
18
19
  výzkumu, kde se oprava i její vrácení vždy nejdřív potvrzuje.
19
20
  - **Stránka každého skenu v aplikaci Strom.** Soubor pro Strom dává teď každému prameni stránku jeho skenu v online
20
21
  archivu: stránku jeho prvního obrázku, jinak vlastní adresu pramene. Aplikace Strom ji nabídne u výřezů, které soubor
21
22
  vynechává, a u pramenů bez výřezu. Jen veřejná webová stránka, nikdy adresa fondu; standardní soubor GEDCOM nechává
22
- adresu v poznámce.
23
- - **Číslo domu a věk tak, jak byly zapsány.** Slovo pro číslo domu („No.“, „čp.“) se dává jen před samotné číslo;
23
+ adresu v poznámce. Datum verze výzkumu v souboru pro Strom nese i čas, takže aplikace ukáže den, kdy vznikla, i kolem
24
+ půlnoci.
25
+ - **Vlastní rod osoby z aplikace Strom.** Když aplikace Strom u osoby označí, že zakládá vlastní rod nebo rod
26
+ rodiny nenese, `strom sync` to uloží do výzkumu a soubor pro Strom to vrací, takže další odeslání i jiné zařízení
27
+ ukážou stejné rody; sloučení dvou osob značku zachová. Standardní soubor GEDCOM o tom nic neříká. Potřebuje aplikaci
28
+ Strom 3.19.0.
29
+ - **Číslo domu, věk a období tak, jak byly zapsány.** Slovo pro číslo domu („No.“, „čp.“) se dává jen před samotné číslo;
24
30
  ulice s číslem jde do souboru tak, jak je. Věk „05y“ se čte jako 5 let. Obojí se z aplikace Strom vrací stejné,
25
- není to úprava uživatele.
26
- - **Otázka na dějiny říká, kde platí.** Zapnutí nebo vypnutí událostí dějin platí pro všechny výzkumy na tomto
27
- počítači; otázka to říká, i jak to změnit jen pro jeden výzkum.
31
+ není to úprava uživatele. Období, které vede aplikace Strom („1871/1882“, nebo otevřené na jednom konci), přijde do
32
+ výzkumu jako od … do …, nikdy jako datum někde mezi nimi.
28
33
  - **Slabý nebo odmítnutý model agenta hlásí doctor.** Když výzkum nemá zvolený model a vlastní výchozí model agenta je
29
34
  ten, který výzkum shledal pro svou práci slabým nebo odmítnutým, `strom doctor` varuje a dá příkaz k volbě; sám
30
35
  nikdy nevolí.
31
36
  - **strom z npm drží svůj kanál.** strom nainstalovaný přes npm zůstává na kanálu, ze kterého byl nainstalován, i po
32
37
  vydání; kanál zvolený přes `strom update --channel` se pro něj také pamatuje.
33
38
  - **Příkaz, který selže, vrátí jen své.** Příkaz, který cestou uložil a pak selhal, vrátí jen to, co přišlo po jeho
34
- uložení, takže výzkum nikdy nevypadá změněný mimo strom.
39
+ uložení, takže výzkum nikdy nevypadá změněný mimo strom. Když se zápis odmítne, protože se výzkum změnil mimo strom,
40
+ člověk to čte v jazyce výzkumu i s cestou, která sedí; agent to dál čte anglicky.
35
41
 
36
42
  ## 1.23.0
37
43
 
package/CHANGELOG.de.md CHANGED
@@ -7,13 +7,14 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
7
7
 
8
8
  ## 1.24.0
9
9
 
10
- *In Vorbereitung: die Betas bis 1.24.0-beta.1. Der Abschnitt ist mit dem Release vollständig.*
10
+ *31.10.2026 · gehört zur Strom-App 3.19.0.*
11
11
 
12
12
  - **Orte einmal zugeordnet für jede Forschung auf diesem Computer.** Ein Ort, den eine andere Forschung hier schon den
13
13
  Regionen der Geschichte zugeordnet hat, wird übernommen, wenn es sicher derselbe Ort ist (derselbe Name und dieselben
14
14
  Koordinaten, Pfarrei oder derselbe Bezirk) und jede Forschung ihn gleich zuordnet; nur die Zuordnung des Orts geht
15
15
  über, nichts von der Familie. Ein Ort gleichen Namens, der nicht sicher derselbe ist, geht als Vorschlag an den
16
- Agenten; er ordnet nur den Rest zu. `strom history adopt --dry-run` sagt, was übernommen würde.
16
+ Agenten; er ordnet nur den Rest zu. `strom history adopt --dry-run` sagt, was übernommen würde. Die Frage, die Ereignisse der Geschichte ein- oder
17
+ auszuschalten, sagt, dass es für jede Forschung auf diesem Computer gilt, und wie es sich für eine allein ändern lässt.
17
18
  - **Die Korrekturen einer Forschung für die Strom-App.** Die Brücke listet, was `strom fixes` in der Forschung anbietet
18
19
  (Titel, was sie tut, wie viele Einträge, ihr Stand, ob sie sich zurücknehmen lässt), kostenlos und ohne etwas zu
19
20
  schreiben; der Link `strom-research://fixes` öffnet eine Korrektur im Terminal der Forschung, wo sie und ihre
@@ -21,12 +22,16 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
21
22
  - **Die Seite jedes Scans in der Strom-App.** Die Strom-Datei gibt jetzt jeder Quelle die Seite ihres Scans im
22
23
  Online-Archiv: die Seite ihres ersten Bildes, sonst die eigene Adresse der Quelle. Die Strom-App verlinkt sie bei den
23
24
  Ausschnitten, die die Datei auslässt, und bei Quellen ohne Ausschnitt. Nur eine öffentliche Webseite, nie die Adresse
24
- eines Bestands; die Standard-GEDCOM-Datei behält die Adresse in der Notiz.
25
- - **Hausnummer und Alter, wie sie geschrieben wurden.** Das Wort für eine Hausnummer („No.“, „čp.“) steht nur vor
25
+ eines Bestands; die Standard-GEDCOM-Datei behält die Adresse in der Notiz. Das Datum der Version der
26
+ Forschung in der Strom-Datei trägt auch die Uhrzeit, so zeigt die App den Tag ihrer Entstehung auch um Mitternacht.
27
+ - **Die eigene Linie einer Person aus der Strom-App.** Markiert die Strom-App, dass eine Person eine eigene Linie
28
+ gründet oder die der Familie nicht trägt, behält `strom sync` das in der Forschung, und die Strom-Datei gibt es
29
+ zurück, so zeigen die nächste Sendung und ein anderes Gerät dieselben Linien; das Zusammenführen zweier Personen
30
+ behält die Markierung. Die Standard-GEDCOM-Datei sagt nichts davon. Es braucht die Strom-App 3.19.0.
31
+ - **Hausnummer, Alter und Zeitraum, wie sie geschrieben wurden.** Das Wort für eine Hausnummer („No.“, „čp.“) steht nur vor
26
32
  einer Zahl allein; eine Straße mit Nummer geht so in die Datei, wie sie ist. Ein Alter „05y“ gilt als 5 Jahre.
27
- Beides kommt aus der Strom-App gleich zurück, keine Änderung des Benutzers.
28
- - **Die Frage nach der Geschichte sagt, wo sie gilt.** Die Ereignisse der Geschichte ein- oder auszuschalten gilt für
29
- jede Forschung auf diesem Computer; die Frage sagt es, und wie es sich für eine Forschung allein ändern lässt.
33
+ Beides kommt aus der Strom-App gleich zurück, keine Änderung des Benutzers. Ein Zeitraum der Strom-App
34
+ („1871/1882“ oder an einem Ende offen) kommt als von … bis … in die Forschung, nie als ein Datum irgendwo dazwischen.
30
35
  - **Ein schwaches oder abgelehntes Modell des Agenten meldet der Doctor.** Ist für die Forschung kein Modell gewählt und
31
36
  das eigene Standardmodell des Agenten eines, das die Forschung für ihre Arbeit schwach oder abgelehnt fand, warnt
32
37
  `strom doctor` und nennt den Befehl zur Wahl; selbst wählt er nie.
@@ -34,7 +39,9 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
34
39
  wurde, auch nach einer Veröffentlichung; ein mit `strom update --channel` gewählter Kanal bleibt dafür ebenfalls
35
40
  gemerkt.
36
41
  - **Ein gescheiterter Befehl nimmt nur das Seine zurück.** Ein Befehl, der unterwegs gespeichert hat und dann scheitert,
37
- nimmt nur zurück, was nach dieser Speicherung kam, so wirkt die Forschung nie außerhalb von strom geändert.
42
+ nimmt nur zurück, was nach dieser Speicherung kam, so wirkt die Forschung nie außerhalb von strom geändert. Wird das
43
+ Schreiben abgelehnt, weil die Forschung außerhalb von strom geändert wurde, liest die Person das in der Sprache der
44
+ Forschung, mit dem passenden Weg; ein Agent liest es weiter auf Englisch.
38
45
 
39
46
  ## 1.23.0
40
47
 
package/CHANGELOG.md CHANGED
@@ -7,13 +7,14 @@ The changes of each version in short. The full notes and the install lines are o
7
7
 
8
8
  ## 1.24.0
9
9
 
10
- *In preparation: the betas up to 1.24.0-beta.1. The section is complete with the release.*
10
+ *2026-10-31 · goes with the Strom app 3.19.0.*
11
11
 
12
12
  - **Places mapped once for every research on this computer.** A place another research here has mapped to its
13
13
  regions of history is taken over when it is surely the same place (the same name and the same coordinates, parish
14
14
  or district) and every research maps it alike; only the place's map crosses, nothing of the family. A place of the
15
15
  same name that is not surely the same is offered to the agent as a suggestion; the agent maps only the rest.
16
- `strom history adopt --dry-run` says what would be taken.
16
+ `strom history adopt --dry-run` says what would be taken. The question about turning the events of history on or off
17
+ says it holds for every research on this computer, and how to change it for one research alone.
17
18
  - **The fixes of a research for the Strom app.** The bridge lists what `strom fixes` offers in the research (title,
18
19
  what it does, how many items, its state, whether it can be put back), free and without writing anything; the link
19
20
  `strom-research://fixes` opens a fix in the research's terminal, where setting it right or putting it back is
@@ -21,19 +22,25 @@ The changes of each version in short. The full notes and the install lines are o
21
22
  - **The page of each scan in the Strom app.** The Strom file now gives each source the page of its scan in the online
22
23
  archive: the page of its first image, else the source's own address. The Strom app links it for the excerpts the
23
24
  file leaves out and for the sources without an excerpt. Only a public web page, never a recordset's address; the
24
- standard GEDCOM file keeps the address in the note.
25
- - **A house number and an age as they were written.** The word for a house number ("No.", "čp.") is put only before
25
+ standard GEDCOM file keeps the address in the note. The date of the research's version in the Strom file carries its
26
+ time too, so the app shows the day it was made also around midnight.
27
+ - **A person's own lineage from the Strom app.** Where the Strom app marks that a person founds a lineage of their
28
+ own or carries none of the family's, `strom sync` keeps it in the research and the Strom file gives it back, so the
29
+ next send and another device show the same lineages; a merge of two people keeps the mark. The standard GEDCOM file
30
+ says nothing of it. It needs the Strom app 3.19.0.
31
+ - **A house number, an age and a period as they were written.** The word for a house number ("No.", "čp.") is put only before
26
32
  a number standing alone; a street with its number goes into the file as it is. An age of "05y" is read as 5 years.
27
- Both come back from the Strom app the same, no edit of the user's.
28
- - **The question about history says where it holds.** Turning the events of history on or off holds for every
29
- research on this computer; the question says so, and how to change it for one research alone.
33
+ Both come back from the Strom app the same, no edit of the user's. A period the Strom app keeps ("1871/1882", or
34
+ open at one end) comes into the research as from … to …, never as a date somewhere between.
30
35
  - **A weak or refused model of the agent said by the doctor.** When no model is chosen for the research and the
31
36
  agent's own default is one the research found weak or refused for its work, `strom doctor` warns and gives the
32
37
  command to choose one; it never chooses itself.
33
38
  - **strom from npm keeps its channel.** strom installed with npm stays on the channel it was installed from, also
34
39
  after a release; a channel chosen with `strom update --channel` is kept for it too.
35
40
  - **A failed command takes back only its own.** A command that saved on the way and then failed takes back only what
36
- came after its save, so the research never looks changed outside strom.
41
+ came after its save, so the research never looks changed outside strom. Where writing is refused because the
42
+ research was changed outside strom, the person reads it in the research's language, with the way out that fits; an
43
+ agent still reads it in English.
37
44
 
38
45
  ## 1.23.0
39
46
 
@@ -756,6 +756,10 @@
756
756
  "log.surname.reject": "Tvary příjmení nejsou jedné rodiny: {forms}",
757
757
  "log.surname.unreject": "Tvary příjmení se smějí znovu navrhnout: {forms}",
758
758
  "log.surname.undo": "Tvary příjmení vráceny, jak byly",
759
+ "log.lineage.founder": "Zakládá vlastní rod: {who}",
760
+ "log.lineage.nocarry": "Rod nenese: {who}",
761
+ "log.lineage.none": "Rod zase podle výpočtu: {who}",
762
+ "log.lineage.undo": "Rod vrácen, jak byl: {who}",
759
763
  "log.participant.fix": "Jména kmotrů a svědků oddělena od poznámek: {n}",
760
764
  "log.participant.unfix": "Jména kmotrů a svědků vrácena, jak byla: {n}",
761
765
  "fix.appshape.resolution": "Platí údaj výzkumu: {value}",
@@ -1398,6 +1402,10 @@
1398
1402
  "ui.sync.do.surname": "zapíše se, jak bylo rozhodnuto v aplikaci",
1399
1403
  "ui.sync.do.surname.kept": "zůstává, jak je ve výzkumu",
1400
1404
  "ui.sync.do.surname.report": "nic se nemění",
1405
+ "ui.sync.lineage.founder": "{n}. Rod: {name} zakládá vlastní rod",
1406
+ "ui.sync.lineage.nocarry": "{n}. Rod: {name} rod nenese",
1407
+ "ui.sync.lineage.none": "{n}. Rod: {name} zase podle výpočtu",
1408
+ "ui.sync.do.lineage": "zapíše se, jak bylo nastaveno v aplikaci",
1401
1409
  "ui.sync.do.part": "doplní se k údaji s odkazem na strom z aplikace",
1402
1410
  "ui.sync.do.part.user": "platí úprava z aplikace (verze výzkumu zůstane v historii)",
1403
1411
  "ui.sync.do.person.cite": "bude ho citovat jméno osoby (vodítko, dokud výzkum záznam nepřečte)",
@@ -1757,6 +1765,10 @@
1757
1765
  "ui.check.streetplaces": "{n, plural, one {# údaj má} few {# údaje mají} other {# údajů má}} v místě ulici nebo číslo domu",
1758
1766
  "ui.error.tree.not-downloaded": "z iCloudu ještě nestaženo: {n, plural, one {# soubor} few {# soubory} other {# souborů}} výzkumu (první: {first}) – nic se nezapsalo",
1759
1767
  "ui.error.tree.not-downloaded.hint": "otevřít složku výzkumu ve Finderu a soubory stáhnout (nebo složku z iCloud Drive vyjmout), pak spustit znovu – ne strom repair",
1768
+ "ui.error.tree.changed-outside": "data výzkumu se změnila mimo strom ({n, plural, one {# problém} few {# problémy} other {# problémů}}): nic se nezapsalo",
1769
+ "ui.error.tree.changed-outside.hint": "strom verify (podrobnosti)\nstrom repair (vrátí poslední zapečetěný stav)",
1770
+ "ui.error.tree.changed-outside.keys.hint": "strom verify (podrobnosti)\nstrom repair, pak strom seal adopt (vrátí jen to, co se udělalo mimo strom – práce obou klíčů tohoto počítače zůstane)",
1771
+ "ui.error.tree.changed-outside.terminal.hint": "strom verify (podrobnosti)\nve 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",
1760
1772
  "ui.export.file": "{file} ({for}) — {persons, plural, one {# osoba} few {# osoby} other {# osob}} · {families, plural, one {# rodina} few {# rodiny} other {# rodin}} · {facts, plural, one {# údaj} few {# údaje} other {# údajů}} · {sources, plural, one {# pramen} few {# prameny} other {# pramenů}}{skipped}",
1761
1773
  "ui.export.for.strom": "pro aplikaci Strom",
1762
1774
  "ui.export.for.strom.images": "pro aplikaci Strom, s obrázky",
@@ -756,6 +756,10 @@
756
756
  "log.surname.reject": "Namensformen keiner gemeinsamen Familie: {forms}",
757
757
  "log.surname.unreject": "Namensformen dürfen wieder vorgeschlagen werden: {forms}",
758
758
  "log.surname.undo": "Namensformen wiederhergestellt, wie sie waren",
759
+ "log.lineage.founder": "Begründet einen eigenen Stamm: {who}",
760
+ "log.lineage.nocarry": "Trägt keinen Stamm: {who}",
761
+ "log.lineage.none": "Stamm wieder wie berechnet: {who}",
762
+ "log.lineage.undo": "Stamm zurückgesetzt, wie er war: {who}",
759
763
  "log.participant.fix": "Namen von Paten und Zeugen von ihren Notizen getrennt: {n}",
760
764
  "log.participant.unfix": "Namen von Paten und Zeugen wiederhergestellt, wie sie waren: {n}",
761
765
  "fix.appshape.resolution": "Der Wert der Forschung bleibt: {value}",
@@ -1398,6 +1402,10 @@
1398
1402
  "ui.sync.do.surname": "übernommen, wie in der App entschieden",
1399
1403
  "ui.sync.do.surname.kept": "bleibt, wie die Forschung es hat",
1400
1404
  "ui.sync.do.surname.report": "nichts ändert sich",
1405
+ "ui.sync.lineage.founder": "{n}. Stamm: {name} begründet einen eigenen Stamm",
1406
+ "ui.sync.lineage.nocarry": "{n}. Stamm: {name} trägt keinen Stamm",
1407
+ "ui.sync.lineage.none": "{n}. Stamm: {name} wieder wie berechnet",
1408
+ "ui.sync.do.lineage": "übernommen, wie in der App festgelegt",
1401
1409
  "ui.sync.do.part": "wird der Angabe hinzugefügt, mit Verweis auf den eigenen Stammbaum",
1402
1410
  "ui.sync.do.part.user": "Die eigene Änderung gilt (die der Forschung bleibt in ihrer Geschichte)",
1403
1411
  "ui.sync.do.person.cite": "der Name der Person zitiert sie (ein Hinweis, bis die Forschung den Eintrag liest)",
@@ -1757,6 +1765,10 @@
1757
1765
  "ui.check.streetplaces": "{n, plural, one {# Angabe hat} other {# Angaben haben}} eine Straße oder eine Nummer im Ort",
1758
1766
  "ui.error.tree.not-downloaded": "noch nicht aus iCloud geladen: {n, plural, one {# Datei} other {# Dateien}} der Forschung (die erste: {first}) – nichts wurde geschrieben",
1759
1767
  "ui.error.tree.not-downloaded.hint": "den Ordner der Forschung im Finder öffnen und die Dateien laden (oder den Ordner aus iCloud Drive herausnehmen), dann erneut ausführen – nicht strom repair",
1768
+ "ui.error.tree.changed-outside": "die Daten der Forschung wurden außerhalb von strom geändert ({n, plural, one {# Problem} other {# Probleme}}): nichts wurde geschrieben",
1769
+ "ui.error.tree.changed-outside.hint": "strom verify (Einzelheiten)\nstrom repair (stellt den letzten versiegelten Stand wieder her)",
1770
+ "ui.error.tree.changed-outside.keys.hint": "strom verify (Einzelheiten)\nstrom repair, dann strom seal adopt (macht nur rückgängig, was außerhalb von strom geschah – die Arbeit beider Schlüssel dieses Computers bleibt)",
1771
+ "ui.error.tree.changed-outside.terminal.hint": "strom verify (Einzelheiten)\nin 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",
1760
1772
  "ui.export.file": "{file} ({for}) — {persons, plural, one {# Person} other {# Personen}} · {families, plural, one {# Familie} other {# Familien}} · {facts, plural, one {# Angabe} other {# Angaben}} · {sources, plural, one {# Quelle} other {# Quellen}}{skipped}",
1761
1773
  "ui.export.for.strom": "für die Strom-App",
1762
1774
  "ui.export.for.strom.images": "für die Strom-App, mit Bildern",
package/dist/cli/ui.js CHANGED
@@ -1357,6 +1357,10 @@ export const UI = {
1357
1357
  "ui.sync.do.surname": "written as decided in the app",
1358
1358
  "ui.sync.do.surname.kept": "stays as the research has it",
1359
1359
  "ui.sync.do.surname.report": "nothing changes",
1360
+ "ui.sync.lineage.founder": "{n}. Lineage: {name} begins a lineage of their own",
1361
+ "ui.sync.lineage.nocarry": "{n}. Lineage: {name} carries no lineage",
1362
+ "ui.sync.lineage.none": "{n}. Lineage: {name} as computed again",
1363
+ "ui.sync.do.lineage": "written as set in the app",
1360
1364
  "ui.sync.do.part": "added to the fact, citing the tree sent",
1361
1365
  "ui.sync.do.part.user": "the edit wins (the research's in its history)",
1362
1366
  "ui.sync.do.person.cite": "their name cites it (a lead until the research reads the record)",
@@ -1725,6 +1729,10 @@ export const UI = {
1725
1729
  "ui.check.streetplaces": "{n, plural, one {# fact holds} other {# facts hold}} a street or a house number in the place",
1726
1730
  "ui.error.tree.not-downloaded": "{n, plural, one {# file} other {# files}} of the research not downloaded from iCloud yet (the first: {first}) — nothing written",
1727
1731
  "ui.error.tree.not-downloaded.hint": "open the research's folder in Finder and download them (or take the folder out of iCloud Drive), then run it again — not strom repair",
1732
+ "ui.error.tree.changed-outside": "the research's data was changed outside strom ({n, plural, one {# problem} other {# problems}}): nothing written",
1733
+ "ui.error.tree.changed-outside.hint": "strom verify (details)\nstrom repair (restores the last sealed state)",
1734
+ "ui.error.tree.changed-outside.keys.hint": "strom verify (details)\nstrom repair, then strom seal adopt (undoes only what was done outside strom — the work of both keys of this computer stays)",
1735
+ "ui.error.tree.changed-outside.terminal.hint": "strom verify (details)\nin a terminal of its own: strom repair, then strom seal adopt — it sees both keys and undoes only what was done outside strom",
1728
1736
  "ui.export.file": "{file} ({for}) — {persons, plural, one {# person} other {# persons}} · {families, plural, one {# family} other {# families}} · {facts, plural, one {# fact} other {# facts}} · {sources, plural, one {# source} other {# sources}}{skipped}",
1729
1737
  "ui.export.for.strom": "for the Strom app",
1730
1738
  "ui.export.for.strom.images": "for the Strom app, with images",
@@ -13,8 +13,9 @@ import { UsageError } from "../core/errors.js";
13
13
  import { excerptSettings, planExcerpts } from "../core/excerpt.js";
14
14
  import { runGit, tracked } from "../core/git.js";
15
15
  import { Settings } from "../core/config.js";
16
- import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appShowsReadings, appShowsOmittedImages, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsSourceReads, appShowsFactStatus, appShowsStoryDrafts, appTurnsExcerpts, appReadsSurnameGroups } from "../core/stromapp.js";
16
+ import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appShowsReadings, appShowsOmittedImages, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsSourceReads, appShowsFactStatus, appShowsStoryDrafts, appTurnsExcerpts, appReadsSurnameGroups, appReadsLines } from "../core/stromapp.js";
17
17
  import { surnamesForExport } from "../core/surnames.js";
18
+ import { lineMarks } from "../core/lineages.js";
18
19
  import { EXCERPT_QUALITIES, EXCERPT_SCOPES } from "../core/model.js";
19
20
  import { ui } from "../cli/ui.js";
20
21
  import { isAgent } from "../core/which.js";
@@ -98,7 +99,9 @@ export function writeTreeGed(tree, opts) {
98
99
  const omitted = !!images && appShowsOmittedImages(new Settings(tree.env, {}), opts.stromVersion);
99
100
  // …and the groups of surname forms the user made one family and the suggestions rejected, for an app that reads them
100
101
  const surnameGroups = opts.for === "strom" && appReadsSurnameGroups(new Settings(tree.env, {}), opts.stromVersion) ? surnamesForExport(tree) : undefined;
101
- const result = exportGedcom(tree, { for: opts.for, ...(surnameGroups ? { surnameGroups } : {}), stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(readings ? { readings } : {}), ...(omitted ? { omitted: images.omitted } : {}) });
102
+ // …and the bounds of a lineage the person set, for an app that keeps them
103
+ const lines = opts.for === "strom" && appReadsLines(new Settings(tree.env, {}), opts.stromVersion) ? lineMarks(tree) : undefined;
104
+ const result = exportGedcom(tree, { for: opts.for, ...(surnameGroups ? { surnameGroups } : {}), ...(lines ? { lines } : {}), stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(readings ? { readings } : {}), ...(omitted ? { omitted: images.omitted } : {}) });
102
105
  const findings = validateGedcom(result.text, opts.for === "standard" ? { strict: true } : {});
103
106
  const file = opts.out ?? gedFile(tree, opts.for, opts.research);
104
107
  if (!hasGedErrors(findings) && !tree.dryRun) {
@@ -94,6 +94,12 @@ function changeLine(tree, c, incoming, lang, readings) {
94
94
  const p = tree.get(key);
95
95
  return p ? `${(p.names[0] ? `${p.names[0].given} ${p.names[0].surname}` : key).trim()} [${key}]` : key;
96
96
  };
97
+ // a person's bound of a lineage the user set in the app (core/lineages.ts)
98
+ if (c.line) {
99
+ const key = c.line.mark === "FOUNDER" ? "ui.sync.lineage.founder" : c.line.mark === "NOCARRY" ? "ui.sync.lineage.nocarry" : "ui.sync.lineage.none";
100
+ const does = c.takenBack ? "ui.sync.do.taken" : c.action === "report" ? "ui.sync.do.surname.report" : "ui.sync.do.lineage";
101
+ return `${ui(lang, key, { n: c.n, name: name(c.person) })} → ${ui(lang, does)}`;
102
+ }
97
103
  const who = c.partners ? c.partners.map(name).join(" & ") || c.family || "?" : name(c.person);
98
104
  // a partner in their age: the given name
99
105
  const short = (key) => (key.startsWith("x:") ? name(key) : (tree.get(key)?.names[0]?.given ?? key));
@@ -432,6 +432,19 @@ export function changeLines(tree, ops, subject, lang, was, tasks) {
432
432
  say(kind === "undo" ? phrase(lang, "log.surname.undo") : kind in { group: 1, ungroup: 1, reject: 1, unreject: 1 } ? phrase(lang, k, { forms: short(forms, 120) }) : changed(o));
433
433
  continue;
434
434
  }
435
+ // the bound of a lineage the person set in the Strom app (quality/lineages.json): the person by name, never the summary
436
+ if (o.op === "lineage.set" || o.op === "lineage.undo") {
437
+ const m = /^(\S+) (begins a lineage|carries no lineage|lineage as computed)/u.exec(o.summary);
438
+ const person = who(o.targets[0] ?? m?.[1]);
439
+ say(o.op === "lineage.undo"
440
+ ? phrase(lang, "log.lineage.undo", { who: person })
441
+ : m?.[2] === "begins a lineage"
442
+ ? phrase(lang, "log.lineage.founder", { who: person })
443
+ : m?.[2] === "carries no lineage"
444
+ ? phrase(lang, "log.lineage.nocarry", { who: person })
445
+ : phrase(lang, "log.lineage.none", { who: person }));
446
+ continue;
447
+ }
435
448
  // the names of participants set apart from their notes by strom's fix, or put back (quality/participant-names.json)
436
449
  if (o.op === "participant.fix" || o.op === "participant.unfix") {
437
450
  const n = /: (\d+) in /u.exec(o.summary)?.[1] ?? String(o.targets.length);
@@ -672,17 +672,26 @@ function importLabel(lang) {
672
672
  export function isStromJson(data) {
673
673
  return !!data && typeof data === "object" && "persons" in data && "partnerships" in data;
674
674
  }
675
- /** Strom flexible date ("~1900", "<1900-05", "1900-05-03") → GEDCOM. */
675
+ /** Strom flexible date ("~1900", "<1900-05", "1900-05-03", a range "1785..1804", a period "1871/1882") → GEDCOM. */
676
676
  export function fromFlexDate(flex) {
677
677
  if (!flex)
678
678
  return undefined;
679
+ // a period ("1871/1882", "1871/", "/1882"): from the one to the other, each side a plain date
680
+ const period = /^([^/]*)\/([^/]*)$/.exec(flex.trim());
681
+ if (period) {
682
+ const plain = (s) => (/^\d{3,4}(?:-\d{1,2}(?:-\d{1,2})?)?$/.test(s) ? fromFlexDate(s) : undefined);
683
+ const [a, b] = [period[1] ? plain(period[1]) : "", period[2] ? plain(period[2]) : ""];
684
+ if (a === undefined || b === undefined || (!a && !b))
685
+ return undefined;
686
+ return normalizeDate(a && b ? `FROM ${a} TO ${b}` : a ? `FROM ${a}` : `TO ${b}`);
687
+ }
679
688
  // a range ("1785..1804"): between the two
680
689
  const range = /^(.+?)\.\.(.+)$/.exec(flex.trim());
681
690
  if (range) {
682
691
  const [a, b] = [fromFlexDate(range[1]), fromFlexDate(range[2])];
683
692
  return a && b && !/^(ABT|BEF|AFT|BET|FROM|TO) /.test(a) && !/^(ABT|BEF|AFT|BET|FROM|TO) /.test(b) ? normalizeDate(`BET ${a} AND ${b}`) : undefined;
684
693
  }
685
- const m = /^([~<>])?(\d{3,4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(flex.trim());
694
+ const m = /^([~<>])?(\d{3,4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?$/.exec(flex.trim());
686
695
  if (!m)
687
696
  return importDate(flex);
688
697
  const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
@@ -518,9 +518,14 @@ export function assertIntact(tree) {
518
518
  : tree.key && errors.some((f) => f.code === "foreign-commit") && lastSealed(tree, tree.key).inTerminal
519
519
  ? REPAIR_IN_TERMINAL_HINT
520
520
  : undefined;
521
+ // a person reads it in the research's language (ui.error.tree.changed-outside), the way said as one of three hints
522
+ const inTerminal = way !== undefined && (way === REPAIR_IN_TERMINAL_HINT || way.startsWith("the user runs in their terminal"));
521
523
  throw new StromError(`data was changed outside strom (${counted(errors.length, "problem")}) — refusing to write`, {
522
524
  hint: `strom verify (details)\n${way ?? "strom repair (restore the last sealed state)"}`,
523
525
  details: errors,
526
+ code: "tree.changed-outside",
527
+ params: { n: String(errors.length) },
528
+ ...(way ? { hintCode: inTerminal ? "tree.changed-outside.terminal" : "tree.changed-outside.keys" } : {}),
524
529
  });
525
530
  }
526
531
  /**
@@ -0,0 +1,147 @@
1
+ // The bounds of a lineage the person set in the Strom app (spec 028; the app's ZADANI_VYZKUM_rody.md): the app computes a
2
+ // lineage (the male line from the oldest bearer of the name) and the person may correct it in two ways — a person who
3
+ // begins a lineage of their own (a founder: their tie to their father does not count for it), a person who carries
4
+ // none (their sons begin lineages). <tree>/quality/lineages.json keeps both lists of people (P…), written whole as a
5
+ // sealed operation of the tree like quality/surnames.json (core/integrity.ts): a change outside strom is an error of
6
+ // strom guard; strom sync undo and the history take it back.
7
+ // Only the person sets them, in the Strom app: strom sync takes them from the app's file (1 _STROM_LINE on a person,
8
+ // 1 _STROM_LINES Y in the header), the Strom file gives them back (gate APP_READS_LINES). No evidence and no fact of a
9
+ // person: they change no name, no parents, no status — a locked person may have one.
10
+ // A person merged since is read as the one merged into (a founder before one who carries none); one taken back has none.
11
+ import path from "node:path";
12
+ import { readJsonIfExists, stringifyCanonical } from "./json.js";
13
+ import { personNow } from "./quality.js";
14
+ import * as git from "./git.js";
15
+ export const LINEAGES_FILE = "quality/lineages.json";
16
+ const PERSON_ID = /^P\d{1,9}$/u;
17
+ /** A person's ID as the research writes it (P12 → P0012); undefined when it is none. */
18
+ function idOf(raw) {
19
+ if (typeof raw !== "string")
20
+ return undefined;
21
+ const id = raw.trim().toUpperCase();
22
+ if (!PERSON_ID.test(id))
23
+ return undefined;
24
+ return id.length < 5 ? `P${id.slice(1).padStart(4, "0")}` : id;
25
+ }
26
+ /** A mark as the Strom file writes it; anything else (a value of a later app) is none. */
27
+ export function lineMarkOf(raw) {
28
+ const v = raw?.trim().toUpperCase();
29
+ return v === "FOUNDER" || v === "NOCARRY" ? v : undefined;
30
+ }
31
+ const sorted = (ids) => [...new Set(ids)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
32
+ function listOf(raw) {
33
+ return Array.isArray(raw) ? sorted(raw.map(idOf).filter((x) => !!x)) : [];
34
+ }
35
+ /** The lists as a file (or a commit's copy of it) holds them; empty when there is none. */
36
+ export function lineagesOf(text) {
37
+ if (!text)
38
+ return { lineFounders: [], lineNotCarriers: [] };
39
+ try {
40
+ const f = JSON.parse(text);
41
+ return { lineFounders: listOf(f?.lineFounders), lineNotCarriers: listOf(f?.lineNotCarriers) };
42
+ }
43
+ catch {
44
+ return { lineFounders: [], lineNotCarriers: [] };
45
+ }
46
+ }
47
+ export function readLineages(tree) {
48
+ try {
49
+ const f = readJsonIfExists(path.join(tree.root, ...LINEAGES_FILE.split("/")));
50
+ return { lineFounders: listOf(f?.lineFounders), lineNotCarriers: listOf(f?.lineNotCarriers) };
51
+ }
52
+ catch {
53
+ return { lineFounders: [], lineNotCarriers: [] };
54
+ }
55
+ }
56
+ /** The lists at a commit (what the export at it gave the Strom app); undefined when the history has no such commit. */
57
+ export function lineagesAt(tree, head) {
58
+ if (!/^[0-9a-f]{7,40}$/u.test(head))
59
+ return undefined;
60
+ if (git.runGit(tree.root, ["cat-file", "-e", `${head}^{commit}`]).status !== 0)
61
+ return undefined;
62
+ return lineagesOf(git.showFile(tree.root, head, LINEAGES_FILE));
63
+ }
64
+ /**
65
+ * Each person's mark as the research reads it now: by who they are now (merged: the one merged into), a person taken
66
+ * back (or not in the research) none, a founder before one who carries none.
67
+ */
68
+ export function lineMarks(tree, file = readLineages(tree)) {
69
+ const out = new Map();
70
+ const live = (id) => {
71
+ const now = personNow(tree, id);
72
+ const p = tree.get(now);
73
+ return p && p.type === "person" && !p.retracted ? now : undefined;
74
+ };
75
+ for (const id of file.lineFounders) {
76
+ const p = live(id);
77
+ if (p)
78
+ out.set(p, "FOUNDER");
79
+ }
80
+ for (const id of file.lineNotCarriers) {
81
+ const p = live(id);
82
+ if (p && !out.has(p))
83
+ out.set(p, "NOCARRY");
84
+ }
85
+ return out;
86
+ }
87
+ /** Written whole as a sealed operation of the tree, inside its lock. */
88
+ function writeLineages(tree, next, op) {
89
+ const founders = sorted(next.lineFounders);
90
+ const notCarriers = sorted(next.lineNotCarriers).filter((id) => !founders.includes(id));
91
+ tree.withTreeLock(() => tree.putFile(LINEAGES_FILE, stringifyCanonical({ lineFounders: founders, lineNotCarriers: notCarriers }), { ...op, summary: op.summary.normalize("NFC") }));
92
+ }
93
+ /** A mark as the history says it. */
94
+ export const markSaid = (m) => (m === "FOUNDER" ? "begins a lineage" : m === "NOCARRY" ? "carries no lineage" : "lineage as computed");
95
+ /**
96
+ * One person's mark set (null: none — the lineage as the app computes it): every ID of the file that is this person now
97
+ * (one merged into them) taken out of both lists, the person put in the one of the mark. Undefined when nothing changes;
98
+ * else the mark before, for an undo.
99
+ */
100
+ export function setLineMark(tree, person, mark, why, op = "lineage.set") {
101
+ const file = readLineages(tree);
102
+ const was = lineMarks(tree, file).get(person) ?? null;
103
+ const theirs = (id) => id === person || personNow(tree, id) === person;
104
+ const founders = file.lineFounders.filter((id) => !theirs(id));
105
+ const notCarriers = file.lineNotCarriers.filter((id) => !theirs(id));
106
+ if (mark === "FOUNDER")
107
+ founders.push(person);
108
+ if (mark === "NOCARRY")
109
+ notCarriers.push(person);
110
+ const next = { lineFounders: sorted(founders), lineNotCarriers: sorted(notCarriers) };
111
+ if (JSON.stringify(next) === JSON.stringify({ lineFounders: file.lineFounders, lineNotCarriers: file.lineNotCarriers }))
112
+ return undefined;
113
+ writeLineages(tree, next, { op, targets: [person], summary: `${person} ${markSaid(mark)} (${why})` });
114
+ return { was };
115
+ }
116
+ /**
117
+ * What the Strom app's marks change in the research. `theirs`: the file's marks by the research's ID (or the file's key
118
+ * of a new person), undefined where the file says nothing of them (no header mark: an older app); `present`: the people
119
+ * the file has (a mark is taken away only from one of them); `base`: the marks the research gave the app at
120
+ * _STROM_HEAD, when it gave them — only what the user changed since counts (a mark they kept as given leaves the
121
+ * research's word); without one the file's list is the truth, an empty one too.
122
+ */
123
+ export function planLines(now, theirs, present, base) {
124
+ if (!theirs)
125
+ return [];
126
+ const out = [];
127
+ const people = sorted([...theirs.keys(), ...now.keys(), ...(base?.keys() ?? [])]);
128
+ for (const id of people) {
129
+ const t = theirs.get(id) ?? null;
130
+ const n = now.get(id) ?? null;
131
+ if (t === n)
132
+ continue;
133
+ // taken away only from someone the file has (one it lacks says nothing of them)
134
+ if (!t && !present(id))
135
+ continue;
136
+ if (base && t === (base.get(id) ?? null))
137
+ continue;
138
+ out.push({ person: id, mark: t, was: n });
139
+ }
140
+ return out;
141
+ }
142
+ /** A change of a sync taken back: the person's mark before it again — unless it changed since (the newer word stands). */
143
+ export function undoLineMark(tree, person, mark, was, why) {
144
+ if ((lineMarks(tree).get(person) ?? null) !== mark)
145
+ return false;
146
+ return !!setLineMark(tree, person, was, why, "lineage.undo");
147
+ }
package/dist/core/live.js CHANGED
@@ -9,7 +9,9 @@
9
9
  //
10
10
  // GET <token>/status the tree, its researches, who is at work, what waits; what the research takes from
11
11
  // the app (accepts). ?poll=1: the app asking now and then — the bridge's idle time goes on
12
- // GET <token>/tree.ged the tree for the Strom app, as it is now, each entry with its image (the settings excerpts.*)
12
+ // GET <token>/tree.ged the tree for the Strom app, as it is now, each entry with its image (the settings excerpts.*);
13
+ // for an app that keeps them the bounds of a lineage the person set (_STROM_LINE, _STROM_LINES;
14
+ // feature lineage.read: strom sync takes them back)
13
15
  // GET <token>/images.ged the same (the address an older strom gave)
14
16
  // GET <token>/log what the research saved, newest first (each commit: when, what, for which task;
15
17
  // text: its lines as the user reads them, kinds: what each is about, sources: the S… each
@@ -215,8 +217,9 @@ import { knownNewerVersion, updateChannel } from "./update.js";
215
217
  import { whatsNew } from "./whatsnew.js";
216
218
  import { gitProgram, runGit } from "./git.js";
217
219
  import { RECENT_HOURS, recentChanges, recentNow } from "./recent.js";
218
- import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appShowsOmittedImages, appShowsReadings, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsFactStatus, appShowsSourceReads, appShowsStoryDrafts, appTurnsExcerpts, appReadsSurnameGroups, appUrlSetting, isAppVersion, isStromAppOrigin, stromAppState } from "./stromapp.js";
220
+ import { appKnowsArchive, appKnowsNoCouple, appShowsHypothesisLinks, appShowsOmittedImages, appShowsReadings, appDecidesConflicts, appOpensLinks, appReadsTitles, appShowsCoupleEvents, appShowsEdges, appShowsFactStatus, appShowsSourceReads, appShowsStoryDrafts, appTurnsExcerpts, appReadsSurnameGroups, appReadsLines, appUrlSetting, isAppVersion, isStromAppOrigin, stromAppState } from "./stromapp.js";
219
221
  import { suggestSurnames, surnamesForExport } from "./surnames.js";
222
+ import { lineMarks } from "./lineages.js";
220
223
  import { judgePairs, pairsOf, PAIRS_MAX } from "./duplicates.js";
221
224
  import { askOf, qualityRev, readDismissed } from "./quality.js";
222
225
  import { Settings } from "./config.js";
@@ -925,7 +928,7 @@ function idsOf(applied, known) {
925
928
  return Object.keys(ids.persons).length || Object.keys(ids.sources).length ? { ids } : {};
926
929
  }
927
930
  /** What the bridge does that an app may ask about (each added once, never taken away). */
928
- export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname", "phone", "read.now", "read.sync", "excerpt.get", "log.sources", "material.assign", "material.thumb", "seal.foreign", "tree.state", "disk.full", "tree.notDownloaded", "sync.removed", "tree.kind", "experiments", "experiments.off", "experiments.link", "experiments.split", "history.events", "history.hide", "experiments.pace", "history.first", "history.regions", "history.hide.all", "history.skip", "history.map", "history.delete", "history.delete.restore", "stories.history", "history.place", "surnames.suggest", "surnames.read", "duplicates.judge", "quality.dismiss", "quality.read", "person.primary", "fixes.list"];
931
+ export const BRIDGE_FEATURES = ["sync.again", "sync.undoneSince", "sync.takenBack", "sync.conflictEdit", "sync.since", "sync.ids", "family.noCouple", "family.alone", "adopt.transfer", "adopt.empty", "material.list", "person.titles", "media.codes", "hypothesis.links", "status.recent", "spend.readers", "hypothesis.intree", "archive.permission", "task.check", "waiting.groups", "waiting.sent", "waiting.note", "batch.kept", "live.end", "whats.new", "terminal.choice", "person.marriedSurname", "phone", "read.now", "read.sync", "excerpt.get", "log.sources", "material.assign", "material.thumb", "seal.foreign", "tree.state", "disk.full", "tree.notDownloaded", "sync.removed", "tree.kind", "experiments", "experiments.off", "experiments.link", "experiments.split", "history.events", "history.hide", "experiments.pace", "history.first", "history.regions", "history.hide.all", "history.skip", "history.map", "history.delete", "history.delete.restore", "stories.history", "history.place", "surnames.suggest", "surnames.read", "lineage.read", "duplicates.judge", "quality.dismiss", "quality.read", "person.primary", "fixes.list"];
929
932
  /** /status recent, when it is ready. */
930
933
  function recentOf(root, env, head) {
931
934
  const recent = head ? recentNow(root, env, head) : undefined;
@@ -1664,6 +1667,8 @@ export function serveLive(root, env) {
1664
1667
  const omitted = !!images && appShowsOmittedImages(new Settings(env, {}), version);
1665
1668
  // …and the groups of surname forms and the suggestions rejected, for an app that reads them (strom sync takes them back)
1666
1669
  const surnameGroups = appReadsSurnameGroups(new Settings(env, {}), version) ? surnamesForExport(tree) : undefined;
1670
+ // …and the bounds of a lineage the person set, for an app that keeps them (strom sync takes them back)
1671
+ const lines = appReadsLines(new Settings(env, {}), version) ? lineMarks(tree) : undefined;
1667
1672
  left = {
1668
1673
  head: h,
1669
1674
  level: set?.quality ?? LEVELS[1].name,
@@ -1674,7 +1679,7 @@ export function serveLive(root, env) {
1674
1679
  head: h,
1675
1680
  links: offered.join(" "),
1676
1681
  version: version ?? "",
1677
- text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(surnameGroups ? { surnameGroups } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(readings ? { readings } : {}), ...(omitted ? { omitted: images.omitted } : {}), ...(offered.length ? { links: offered, ...(linkScheme(env) !== LINK_SCHEME ? { linkScheme: linkScheme(env) } : {}) } : {}) }).text,
1682
+ text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(surnameGroups ? { surnameGroups } : {}), ...(lines ? { lines } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(edges ? { edges } : {}), ...(storyDrafts ? { storyDrafts } : {}), ...(coupleResi ? { coupleResi } : {}), ...(sourceReads ? { sourceReads } : {}), ...(factStatus ? { factStatus } : {}), ...(archive ? { archive } : {}), ...(turnsExcerpts ? { turnsExcerpts } : {}), ...(noCouple ? { noCouple } : {}), ...(titles ? { titles } : {}), ...(hypothesisLinks ? { hypothesisLinks } : {}), ...(conflictSides ? { conflictSides } : {}), ...(readings ? { readings } : {}), ...(omitted ? { omitted: images.omitted } : {}), ...(offered.length ? { links: offered, ...(linkScheme(env) !== LINK_SCHEME ? { linkScheme: linkScheme(env) } : {}) } : {}) }).text,
1678
1683
  };
1679
1684
  }
1680
1685
  return h;
@@ -3903,7 +3908,8 @@ export function serveLive(root, env) {
3903
3908
  const tree = Tree.open(root, env);
3904
3909
  const settings = new Settings(env, {});
3905
3910
  const rev = historyShown(tree, settings) ? historyRevNow(tree, settings, h, historyRev(tree, settings.shared()?.value, settings)) : undefined;
3906
- if (rev && historySeen && historySeen.head === h && historySeen.rev !== rev)
3911
+ // (a commit made since the head was read changes the rev too: its change says it, never a history of its own)
3912
+ if (rev && historySeen && historySeen.head === h && historySeen.rev !== rev && tip(root).head === h)
3907
3913
  send("history", { rev });
3908
3914
  historySeen = rev ? { rev, head: h } : undefined;
3909
3915
  }
@@ -318,6 +318,10 @@ export const PHRASES = {
318
318
  "log.surname.reject": "Surname forms of no one family: {forms}",
319
319
  "log.surname.unreject": "Surname forms may be suggested again: {forms}",
320
320
  "log.surname.undo": "Surname forms put back as they were",
321
+ "log.lineage.founder": "Begins a lineage of their own: {who}",
322
+ "log.lineage.nocarry": "Carries no lineage: {who}",
323
+ "log.lineage.none": "Lineage as computed again: {who}",
324
+ "log.lineage.undo": "Lineage put back as it was: {who}",
321
325
  "log.participant.fix": "Names of godparents and witnesses set apart from their notes: {n}",
322
326
  "log.participant.unfix": "Names of godparents and witnesses put back as they were: {n}",
323
327
  "fix.appshape.resolution": "The research's value stays: {value}",
@@ -558,6 +558,26 @@ export function appKnowsPrimaryName(settings, version) {
558
558
  return compareVersions(v, APP_PRIMARY_NAME_BETA) >= 0;
559
559
  return !!APP_KNOWS_PRIMARY_NAME && compareVersions(v, APP_KNOWS_PRIMARY_NAME) >= 0;
560
560
  }
561
+ /**
562
+ * The first Strom app that keeps the person's own bounds of a lineage (spec 028; the app's ZADANI_VYZKUM_rody.md):
563
+ * it reads 1 _STROM_LINE FOUNDER|NOCARRY on a person and 1 _STROM_LINES Y in the header of the Strom file, keeps them
564
+ * as its lineageOverrides and sends them back. An older app skips the tags and sends none back (a send of it says
565
+ * nothing of them: no header mark). Released with the app 3.19.0; an app of unknown version (strom app from a
566
+ * terminal) stays off, as the groups of a surname's forms; another copy of the app (its beta, its development) gets
567
+ * them from APP_READS_LINES_BETA.
568
+ */
569
+ export const APP_READS_LINES = "3.19.0";
570
+ /** The first beta of the app that reads and writes the bounds of a lineage (another copy of the app gets them from it). */
571
+ export const APP_READS_LINES_BETA = "3.19.0-beta.4";
572
+ /** Does the Strom app of this version keep the bounds of a lineage the person set (_STROM_LINE, _STROM_LINES)? */
573
+ export function appReadsLines(settings, version, gate = APP_READS_LINES) {
574
+ const v = version?.trim();
575
+ if (!v || !isAppVersion(v))
576
+ return false;
577
+ if (anotherCopy(settings) && APP_READS_LINES_BETA)
578
+ return compareVersions(v, APP_READS_LINES_BETA) >= 0;
579
+ return !!gate && compareVersions(v, gate) >= 0;
580
+ }
561
581
  /** The address that opens the Strom app to send the user's edited tree to the bridge. */
562
582
  export function sendAppUrl(bridge, settings) {
563
583
  return `${stromAppUrl(settings)}?send=${encodeURIComponent(bridge)}`;
package/dist/core/sync.js CHANGED
@@ -42,7 +42,8 @@ import * as git from "./git.js";
42
42
  import { now, Tree, typeOfId } from "./tree.js";
43
43
  import { actsAsArchive } from "./state.js";
44
44
  import { appSides } from "./conflicts.js";
45
- import { APP_DATA_SEX_UNKNOWN, appKeepsPeriods, appKnowsPrimaryName, appReadsSurnameGroups, isAppVersion } from "./stromapp.js";
45
+ import { APP_DATA_SEX_UNKNOWN, appKeepsPeriods, appKnowsPrimaryName, appReadsLines, appReadsSurnameGroups, isAppVersion } from "./stromapp.js";
46
+ import { lineMarkOf, lineMarks, lineagesAt, planLines, setLineMark, undoLineMark } from "./lineages.js";
46
47
  import { Settings } from "./config.js";
47
48
  import { applySurnameChange, formsSaid, planSurnames, readMarkLines, readSurnames, surnamesAt, undoSurnameChange } from "./surnames.js";
48
49
  import { SURNAME_GROUPS_MARK, SURNAME_REJECTIONS_MARK } from "../gedcom/export.js";
@@ -585,6 +586,8 @@ export function readGedcom(text) {
585
586
  }
586
587
  // the sources of the person: the Strom app's (1 SOUR), the research's of their names
587
588
  const cites = [...children(r, "SOUR"), ...children(r, "NAME").flatMap((n) => children(n, "SOUR"))].map(cite).filter((x) => !!x);
589
+ // their bound of a lineage (a value of a later app: none)
590
+ const line = children(r, "_STROM_LINE").map((n) => lineMarkOf(n.value)).reduce((a, m) => (a === "FOUNDER" || m === "FOUNDER" ? "FOUNDER" : (a ?? m)), undefined);
588
591
  persons.set(key, {
589
592
  key,
590
593
  names,
@@ -596,6 +599,7 @@ export function readGedcom(text) {
596
599
  ...(titles ? { titles } : {}),
597
600
  ...(married.length ? { married } : {}),
598
601
  ...(primary ? { primary } : {}),
602
+ ...(line ? { line } : {}),
599
603
  });
600
604
  }
601
605
  // how a child belongs to a family (the child's FAMC + PEDI: adopted, foster, step): none said is birth
@@ -664,10 +668,12 @@ export function readGedcom(text) {
664
668
  const again = val(head, "_STROM_AGAIN")?.trim();
665
669
  const appVersion = fromApp.app ? val(children(head, "SOUR")[0], "VERS")?.trim() : undefined;
666
670
  const surnames = head ? surnameMarksOf(head) : undefined;
671
+ const lines = /^y(es)?$/i.test(val(head, "_STROM_LINES")?.trim() ?? "");
667
672
  return {
668
673
  format: "gedcom",
669
674
  ...(appVersion ? { appVersion } : {}),
670
675
  ...(surnames ? { surnames } : {}),
676
+ ...(lines ? { lines: true } : {}),
671
677
  ...(treeId ? { treeId } : {}),
672
678
  ...(at ? { head: at } : {}),
673
679
  persons,
@@ -2623,6 +2629,25 @@ export function planSync(tree, incoming, edits, opts = {}) {
2623
2629
  const action = !here || (given && samePoint(given, here)) || mirror ? "set" : "pick";
2624
2630
  push({ kind: "place.coords", action, place: { ...p, ...(mine ? { id: mine.id } : {}), ...(here ? { was: here } : {}) } });
2625
2631
  }
2632
+ // the bounds of a lineage the user set in the app: where the file says them (_STROM_LINES), against what the research
2633
+ // gave the app at _STROM_HEAD when it gave them (an app that keeps them: APP_READS_LINES) — else the file's are the truth
2634
+ if (incoming.lines) {
2635
+ const theirs = new Map();
2636
+ const present = new Set();
2637
+ for (const [key, p] of incoming.persons) {
2638
+ const id = key.startsWith("x:") ? key : resolve(tree, key)?.id;
2639
+ if (!id)
2640
+ continue;
2641
+ present.add(id);
2642
+ if (p.line && theirs.get(id) !== "FOUNDER")
2643
+ theirs.set(id, p.line);
2644
+ }
2645
+ const given = incoming.head && appReadsLines(new Settings(tree.env, {}), incoming.appVersion) ? lineagesAt(tree, incoming.head) : undefined;
2646
+ for (const c of planLines(lineMarks(tree), theirs, (id) => !partial && present.has(id), given ? lineMarks(tree, given) : undefined)) {
2647
+ const p = incoming.persons.get(c.person);
2648
+ push({ kind: "lineage.mark", action: "set", person: c.person, ...(p?.names[0] ? { name: p.names[0] } : {}), text: c.mark ?? "", line: c });
2649
+ }
2650
+ }
2626
2651
  if (incoming.head && opts.takenBack !== false && !sentAgain)
2627
2652
  takenBack(tree, incoming, edits, changes);
2628
2653
  // a send of the app (`sent`: it came through the bridge) with nothing to stand on (no _STROM_HEAD, no _STROM_SINCE the research still has —
@@ -4273,6 +4298,18 @@ export function applySync(tree, plan, incoming, source, only) {
4273
4298
  if (r)
4274
4299
  applied.push({ do: "surname.set", id: r.list, before: JSON.stringify({ added: r.added ?? null, removed: r.removed }) });
4275
4300
  }
4301
+ // the bounds of a lineage the user set in the app (quality/lineages.json): a new person's by the ID they got
4302
+ for (const c of plan.changes.filter((x) => x.line && taken(x))) {
4303
+ const id = who(c.line.person);
4304
+ const p = id ? resolve(tree, id) : undefined;
4305
+ if (!p) {
4306
+ skipped.push({ n: c.n, kind: c.kind, why: `${c.line.person} is not in the research`, code: "person.no-match", params: { person: c.line.person } });
4307
+ continue;
4308
+ }
4309
+ const r = setLineMark(tree, p.id, c.line.mark, reason);
4310
+ if (r)
4311
+ applied.push({ do: "lineage.set", id: p.id, before: JSON.stringify({ was: r.was, mark: c.line.mark }) });
4312
+ }
4276
4313
  return { applied, changes: changes - skipped.length, skipped, ...(asked.length ? { asked } : {}) };
4277
4314
  }
4278
4315
  /** Where a task to check a reading of a document confirmed in the app comes from (one per person and its sources). */
@@ -4971,6 +5008,11 @@ export function undoSync(tree, input) {
4971
5008
  undoSurnameChange(tree, a.id === "rejected" ? "rejected" : "groups", b.added ?? undefined, b.removed ?? [], reason);
4972
5009
  break;
4973
5010
  }
5011
+ case "lineage.set": {
5012
+ const b = JSON.parse(String(a.before ?? "{}"));
5013
+ undoLineMark(tree, a.id, b.mark ?? null, b.was ?? null, reason);
5014
+ break;
5015
+ }
4974
5016
  }
4975
5017
  n++;
4976
5018
  }
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.24.0-beta.1";
32
+ export const VERSION = "1.24.0";
33
33
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
34
34
  .strom/
35
35
  /media/
@@ -226,11 +226,16 @@ export function exportGedcom(tree, opts = {}) {
226
226
  if (opts.linkScheme)
227
227
  w.line(2, "_SCHEME", opts.linkScheme);
228
228
  }
229
+ // when the research wrote it, to the second in UTC (the app shows the local day; DATE stays the day): every app takes
230
+ // its first ten characters as the day before (N23)
229
231
  if (opts.research)
230
- w.line(1, "_STROM_ASOF", new Date().toISOString().slice(0, 10));
232
+ w.line(1, "_STROM_ASOF", asOfNow());
231
233
  // an archive (no agent; the app's data written as they come): for an app that knows it (APP_KNOWS_ARCHIVE)
232
234
  if (opts.archive && tree.config.mode === "archive")
233
235
  w.line(1, "_STROM_MODE", "archive");
236
+ // the file says the bounds of a lineage the person set, all of them (none: none), for an app that keeps them
237
+ if (opts.lines)
238
+ w.line(1, "_STROM_LINES", "Y");
234
239
  }
235
240
  w.line(1, "SUBM", "@U1@");
236
241
  w.line(1, "GEDC");
@@ -301,6 +306,10 @@ export function exportGedcom(tree, opts = {}) {
301
306
  }
302
307
  }
303
308
  w.line(1, "SEX", p.sex);
309
+ // the person's bound of a lineage (the Strom profile, an app that keeps it)
310
+ const line = opts.for === "strom" ? opts.lines?.get(p.id) : undefined;
311
+ if (line)
312
+ w.line(1, "_STROM_LINE", line);
304
313
  w.line(1, "REFN", p.id);
305
314
  w.line(2, "TYPE", REFN_TYPE);
306
315
  const deferred = [];
@@ -953,3 +962,7 @@ function gedDate(d) {
953
962
  const m = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"][d.getUTCMonth()];
954
963
  return `${d.getUTCDate()} ${m} ${d.getUTCFullYear()}`;
955
964
  }
965
+ /** When the research wrote the file (1 _STROM_ASOF): ISO 8601 to the second in UTC ("2026-10-31T06:12:05Z"). */
966
+ export function asOfNow(d = new Date()) {
967
+ return `${d.toISOString().slice(0, 19)}Z`;
968
+ }
@@ -14,12 +14,12 @@ const INDI_EVENTS = [
14
14
  const FAM_EVENTS = ["MARR", "DIV", "MARB", "MARC", "MARL", "MARS", "ANUL", "DIVF", "ENGA", "CENS", "RESI", "EVEN", "NCHI"];
15
15
  /** Allowed child tags by parent context ("INDI", "INDI.BIRT", …). CONC/CONT are allowed under anything with text. */
16
16
  const CHILDREN = {
17
- HEAD: ["SOUR", "DEST", "DATE", "SUBM", "SUBN", "FILE", "COPR", "GEDC", "CHAR", "LANG", "PLAC", "NOTE", "_STROM_TREE", "_STROM_HEAD", "_STROM_LINKS", "_STROM_ASOF", "_STROM_MODE"],
17
+ HEAD: ["SOUR", "DEST", "DATE", "SUBM", "SUBN", "FILE", "COPR", "GEDC", "CHAR", "LANG", "PLAC", "NOTE", "_STROM_TREE", "_STROM_HEAD", "_STROM_LINKS", "_STROM_ASOF", "_STROM_MODE", "_STROM_LINES"],
18
18
  "HEAD.SOUR": ["VERS", "NAME", "CORP", "DATA"],
19
19
  "HEAD._STROM_LINKS": ["_SCHEME"],
20
20
  "HEAD.GEDC": ["VERS", "FORM"],
21
21
  "HEAD.DATE": ["TIME"],
22
- INDI: ["NAME", "SEX", "FAMC", "FAMS", "NOTE", "SOUR", "REFN", "ASSO", "ALIA", "OBJE", "RESN", "RIN", "CHAN", "_STORY", "_STROM_CONFLICT", "_STROM_HYPO", "_STROM_SEARCHED", "_STROM_EDGE", "_STROM_ISLAND", ...INDI_EVENTS],
22
+ INDI: ["NAME", "SEX", "FAMC", "FAMS", "NOTE", "SOUR", "REFN", "ASSO", "ALIA", "OBJE", "RESN", "RIN", "CHAN", "_STORY", "_STROM_CONFLICT", "_STROM_HYPO", "_STROM_SEARCHED", "_STROM_EDGE", "_STROM_ISLAND", "_STROM_LINE", ...INDI_EVENTS],
23
23
  "INDI._STROM_CONFLICT": ["TYPE", "TITL", "STAT", "_STROM_TAKE", "VAL", "DECI"],
24
24
  "INDI._STROM_HYPO": ["TITL", "STAT", "_CHOSEN", "NOTE", "_VAR"],
25
25
  // what a variant of a hypothesis would connect (the Strom profile)
@@ -82,6 +82,8 @@ const EXTENSIONS = new Set([
82
82
  "_STROM_READ", "_STROM_VERIFIED", "_STROM_READING", "_STROM_ENTRY", "_STROM_STATUS",
83
83
  // an excerpt's original by its content, and how its picture lies
84
84
  "_STROM_SHA", "_STROM_ORIENT",
85
+ // the bounds of a lineage the person set (on a person), and the header's mark that the file says them
86
+ "_STROM_LINE", "_STROM_LINES",
85
87
  ]);
86
88
  /** Context of the children of the last tag in `anc` (the ancestors of a line). */
87
89
  function contextOf(anc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.24.0-beta.1",
3
+ "version": "1.24.0",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "runtime": {
22
22
  "name": "node",
23
23
  "version": ">=24",
24
- "onFail": "error"
24
+ "onFail": "warn"
25
25
  }
26
26
  },
27
27
  "scripts": {
@@ -57,8 +57,5 @@
57
57
  "devDependencies": {
58
58
  "@types/node": "^22.20.4",
59
59
  "typescript": "^7.0.2"
60
- },
61
- "publishConfig": {
62
- "tag": "beta"
63
60
  }
64
61
  }