strom-research 1.0.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/LICENSE +373 -0
- package/README.md +142 -0
- package/assets/lang/cs.json +302 -0
- package/assets/lang/de.json +302 -0
- package/assets/method/core.md +43 -0
- package/assets/method/enrich.md +11 -0
- package/assets/method/intake.md +30 -0
- package/assets/method/link.md +28 -0
- package/assets/method/locate.md +28 -0
- package/assets/method/narrate.md +13 -0
- package/assets/method/reading.md +62 -0
- package/assets/method/recording.md +59 -0
- package/assets/method/request.md +10 -0
- package/assets/method/verify.md +17 -0
- package/assets/plugins/README.md +23 -0
- package/assets/plugins/connectors/DISCOVERY.md +159 -0
- package/assets/plugins/connectors/README.md +376 -0
- package/assets/plugins/connectors/sdk.ts +168 -0
- package/assets/plugins/connectors/template.ts +38 -0
- package/assets/plugins/gitignore +4 -0
- package/dist/agents/files.js +313 -0
- package/dist/agents/global.js +257 -0
- package/dist/agents/launch.js +36 -0
- package/dist/agents/profiles.js +95 -0
- package/dist/brief/brief.js +345 -0
- package/dist/cli/commit.js +44 -0
- package/dist/cli/context.js +311 -0
- package/dist/cli/execute.js +154 -0
- package/dist/cli/fixes.js +78 -0
- package/dist/cli/format.js +53 -0
- package/dist/cli/help.js +59 -0
- package/dist/cli/main.js +152 -0
- package/dist/cli/menu.js +212 -0
- package/dist/cli/registry.js +96 -0
- package/dist/cli/ui.js +266 -0
- package/dist/cli/wizard.js +142 -0
- package/dist/cli.js +14 -0
- package/dist/commands/analysis.js +622 -0
- package/dist/commands/batch.js +181 -0
- package/dist/commands/checks.js +153 -0
- package/dist/commands/connectors.js +1377 -0
- package/dist/commands/guide.js +160 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/intake.js +234 -0
- package/dist/commands/media.js +406 -0
- package/dist/commands/meta.js +195 -0
- package/dist/commands/output.js +117 -0
- package/dist/commands/people.js +664 -0
- package/dist/commands/read.js +199 -0
- package/dist/commands/research.js +139 -0
- package/dist/commands/session.js +605 -0
- package/dist/commands/setup.js +465 -0
- package/dist/commands/sources.js +634 -0
- package/dist/commands/start.js +383 -0
- package/dist/commands/story.js +75 -0
- package/dist/commands/tasks.js +436 -0
- package/dist/commands/trees.js +128 -0
- package/dist/core/actions.js +852 -0
- package/dist/core/age.js +95 -0
- package/dist/core/apps.js +74 -0
- package/dist/core/assets.js +34 -0
- package/dist/core/awake.js +33 -0
- package/dist/core/browser.js +281 -0
- package/dist/core/calibration.js +48 -0
- package/dist/core/check.js +112 -0
- package/dist/core/chromium.js +88 -0
- package/dist/core/config.js +348 -0
- package/dist/core/connector.js +811 -0
- package/dist/core/deps.js +73 -0
- package/dist/core/dialog.js +61 -0
- package/dist/core/errors.js +89 -0
- package/dist/core/evidence.js +58 -0
- package/dist/core/frontier.js +219 -0
- package/dist/core/gdate.js +77 -0
- package/dist/core/git.js +300 -0
- package/dist/core/guard.js +124 -0
- package/dist/core/http2.js +76 -0
- package/dist/core/import.js +541 -0
- package/dist/core/install.js +28 -0
- package/dist/core/integrity.js +219 -0
- package/dist/core/json.js +87 -0
- package/dist/core/lang.js +70 -0
- package/dist/core/live.js +244 -0
- package/dist/core/lock.js +112 -0
- package/dist/core/logins.js +67 -0
- package/dist/core/media.js +223 -0
- package/dist/core/model.js +101 -0
- package/dist/core/net.js +366 -0
- package/dist/core/open.js +29 -0
- package/dist/core/paths.js +84 -0
- package/dist/core/people.js +283 -0
- package/dist/core/phrases.js +85 -0
- package/dist/core/queue.js +113 -0
- package/dist/core/reader.js +76 -0
- package/dist/core/records.js +105 -0
- package/dist/core/roles.js +30 -0
- package/dist/core/schema.js +261 -0
- package/dist/core/seal.js +77 -0
- package/dist/core/self.js +40 -0
- package/dist/core/session.js +155 -0
- package/dist/core/shortcut.js +90 -0
- package/dist/core/stories.js +61 -0
- package/dist/core/stromapp.js +138 -0
- package/dist/core/text.js +104 -0
- package/dist/core/tree.js +507 -0
- package/dist/core/uninstall.js +128 -0
- package/dist/core/update.js +193 -0
- package/dist/core/validate.js +260 -0
- package/dist/core/views.js +164 -0
- package/dist/core/which.js +51 -0
- package/dist/core/workers.js +42 -0
- package/dist/gedcom/export.js +454 -0
- package/dist/gedcom/labels.js +103 -0
- package/dist/gedcom/lines.js +91 -0
- package/dist/gedcom/parse.js +53 -0
- package/dist/gedcom/validate.js +183 -0
- package/dist/image/image.js +223 -0
- package/dist/image/index.js +114 -0
- package/dist/image/jpeg-decode.js +552 -0
- package/dist/image/jpeg-encode.js +254 -0
- package/dist/image/png.js +241 -0
- package/dist/runners/antigravity.js +70 -0
- package/dist/runners/claude.js +179 -0
- package/dist/runners/codex.js +45 -0
- package/dist/runners/index.js +13 -0
- package/dist/runners/jsonl.js +86 -0
- package/dist/runners/opencode.js +50 -0
- package/dist/runners/runner.js +63 -0
- package/dist/runners/script.js +58 -0
- package/package.json +44 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Who works in a tree right now. Several agents may: a conversation with
|
|
2
|
+
// Claude Code, another with Codex, a run on its own. Each one strom starts
|
|
3
|
+
// gets a name of its own (STROM_WORKER) and is present while its strom process
|
|
4
|
+
// lives (.strom/workers/<name>.json, a lock that dies with the process). Every
|
|
5
|
+
// worker has its own current session, and never gets a task another one has.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { acquireLock, liveHolder } from "./lock.js";
|
|
9
|
+
function dir(root) {
|
|
10
|
+
return path.join(root, ".strom", "workers");
|
|
11
|
+
}
|
|
12
|
+
/** A worker's name, safe as a file name. */
|
|
13
|
+
export function isWorkerId(id) {
|
|
14
|
+
return !!id && /^[\w.-]{1,80}$/.test(id);
|
|
15
|
+
}
|
|
16
|
+
/** Be present in the tree until the returned function is called (or the process ends). */
|
|
17
|
+
export function enterWorker(root, id, label) {
|
|
18
|
+
return acquireLock(path.join(dir(root), `${id}.json`), { owner: label, waitMs: 0, staleMs: 7 * 24 * 3600_000 });
|
|
19
|
+
}
|
|
20
|
+
/** The workers present now; files of workers that are gone are cleared away. */
|
|
21
|
+
export function liveWorkers(root) {
|
|
22
|
+
const out = [];
|
|
23
|
+
let files = [];
|
|
24
|
+
try {
|
|
25
|
+
files = fs.readdirSync(dir(root)).filter((f) => f.endsWith(".json"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
for (const f of files) {
|
|
31
|
+
const file = path.join(dir(root), f);
|
|
32
|
+
const info = liveHolder(file, 7 * 24 * 3600_000);
|
|
33
|
+
if (info)
|
|
34
|
+
out.push({ id: f.slice(0, -5), label: info.owner, since: info.at });
|
|
35
|
+
else
|
|
36
|
+
fs.rmSync(file, { force: true });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export function isLiveWorker(root, id) {
|
|
41
|
+
return !!liveHolder(path.join(dir(root), `${id}.json`), 7 * 24 * 3600_000);
|
|
42
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// GEDCOM 5.5.1 export — the automated output of a research.
|
|
2
|
+
//
|
|
3
|
+
// Written to the Strom import contract (docs/GEDCOM-IMPORT.md of the Strom app) and
|
|
4
|
+
// generalised from an earlier research exporter:
|
|
5
|
+
// - every fact under its own standard tag where readers look for it; value
|
|
6
|
+
// tags (OCCU, RELI, TITL, NATI) carry the fact on the tag line; family-only
|
|
7
|
+
// tags stay on FAM;
|
|
8
|
+
// - of several facts of one kind the best-supported comes first — readers
|
|
9
|
+
// take the first BIRT as the birth;
|
|
10
|
+
// - QUAY is the quality of each citation's evidence (5.5.1), not the
|
|
11
|
+
// certainty of the conclusion; leads and possible facts say so in a note;
|
|
12
|
+
// - REFN carries our ID so a round trip through the Strom app is matched
|
|
13
|
+
// without guessing.
|
|
14
|
+
//
|
|
15
|
+
// Two files from the same evidence, for two readers:
|
|
16
|
+
// - "standard": plain GEDCOM 5.5.1 for any program — no extension tags,
|
|
17
|
+
// associations on the individual (ASSO), the words of a record as citation
|
|
18
|
+
// DATA/TEXT, a story as a note;
|
|
19
|
+
// - "strom": shaped to what the Strom app reads (agreed with it; its
|
|
20
|
+
// docs/GEDCOM-IMPORT.md): _WITN, _FREL/_MREL, _STORY, the transcript as the
|
|
21
|
+
// source's TEXT, a source per PAGE (Strom keeps one PAGE per source). Until
|
|
22
|
+
// Strom reads AGE, CAUS, ADDR and _FREL/_MREL (STROM_READS_TAGS) they are
|
|
23
|
+
// also said in notes.
|
|
24
|
+
import { GedWriter } from "./lines.js";
|
|
25
|
+
import { labels, RELA } from "./labels.js";
|
|
26
|
+
import { birthEvent, displayName, formatName, gedcomName, preferredOrder, primaryName, relationTo } from "../core/people.js";
|
|
27
|
+
import { foldText } from "../core/text.js";
|
|
28
|
+
import { dateYears } from "../core/gdate.js";
|
|
29
|
+
import { humanAge, isGedcomAge, normalizeAge } from "../core/age.js";
|
|
30
|
+
import { quay } from "../core/evidence.js";
|
|
31
|
+
import { VERSION } from "../core/tree.js";
|
|
32
|
+
export const GED_PROFILES = ["standard", "strom"];
|
|
33
|
+
/** The first Strom version that reads AGE, CAUS, ADDR, _FREL/_MREL and more source notes itself (not released yet). */
|
|
34
|
+
export const STROM_READS_TAGS = undefined;
|
|
35
|
+
/** Does this Strom read the standard tags, so the notes repeating them can go? */
|
|
36
|
+
export function stromReadsTags(version, from = STROM_READS_TAGS) {
|
|
37
|
+
if (!from || !version)
|
|
38
|
+
return false;
|
|
39
|
+
const n = (v) => v.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
40
|
+
const [a, b] = [n(version), n(from)];
|
|
41
|
+
for (let i = 0; i < 3; i++)
|
|
42
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
43
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/** Tags whose value IS the fact and rides on the tag line. */
|
|
47
|
+
const VALUE_TAGS = new Set(["OCCU", "RELI", "TITL", "NATI"]);
|
|
48
|
+
/** Tags the Strom importer reads on INDI (contract, "Events" + note tags). */
|
|
49
|
+
const INDI_TAGS = new Set([
|
|
50
|
+
"BIRT", "DEAT", "BAPM", "CHR", "CHRA", "CONF", "FCOM", "BARM", "BASM", "ORDN", "EDUC", "GRAD", "OCCU", "RESI",
|
|
51
|
+
"EMIG", "IMMI", "NATU", "RELI", "TITL", "NATI", "ADOP", "WILL", "PROB", "BURI", "CREM", "CENS", "EVEN", "RETI",
|
|
52
|
+
]);
|
|
53
|
+
/** Tags read on FAM (RESI is standard on FAM but Strom does not read it yet: see famTag). */
|
|
54
|
+
const FAM_TAGS = new Set(["MARR", "DIV", "MARB", "MARC", "MARL", "MARS", "ANUL", "DIVF", "ENGA", "CENS", "EVEN"]);
|
|
55
|
+
/** Our kinds with no standard tag of their own: exported as EVEN + TYPE. */
|
|
56
|
+
const AS_EVEN = { MILI: "MILI" };
|
|
57
|
+
/** Events that may carry "Y" when nothing else is known about them (5.5.1). */
|
|
58
|
+
const Y_TAGS = new Set(["BIRT", "CHR", "DEAT", "BURI", "CREM", "ADOP", "BAPM", "CHRA", "CONF", "FCOM", "ORDN", "NATU", "EMIG", "IMMI", "CENS", "PROB", "WILL", "GRAD", "RETI", "MARR", "DIV", "ANUL", "ENGA", "MARB", "MARC", "MARL", "MARS", "DIVF"]);
|
|
59
|
+
/** _FREL/_MREL values (Legacy, RootsMagic, FTM). */
|
|
60
|
+
const FREL = { birth: "Natural", adopted: "Adopted", step: "Step", foster: "Foster", unknown: "Unknown" };
|
|
61
|
+
/** GEDCOM NAME TYPE for our kinds of name ("religious" is a user-defined type). */
|
|
62
|
+
const NAME_TYPE = { birth: "birth", married: "married", alias: "aka", religious: "religious" };
|
|
63
|
+
export function exportGedcom(tree, opts = {}) {
|
|
64
|
+
const lang = tree.config.lang;
|
|
65
|
+
const L = labels(lang);
|
|
66
|
+
const w = new GedWriter();
|
|
67
|
+
const stats = { persons: 0, families: 0, sources: 0, repositories: 0, events: 0, skipped: 0 };
|
|
68
|
+
const strict = (opts.for ?? "standard") === "standard";
|
|
69
|
+
// Strom profile: say in notes what the Strom app does not read from the tags yet.
|
|
70
|
+
const repeat = !strict && !stromReadsTags(opts.stromVersion);
|
|
71
|
+
const everyone = tree.list("person");
|
|
72
|
+
const personById = new Map(everyone.map((p) => [p.id, p]));
|
|
73
|
+
const eventById = new Map([...everyone, ...tree.list("family")].flatMap((r) => r.events.map((e) => [e.id, e])));
|
|
74
|
+
const persons = everyone.filter((p) => !p.retracted && (!opts.persons || opts.persons.has(p.id)));
|
|
75
|
+
const personIds = new Set(persons.map((p) => p.id));
|
|
76
|
+
const families = tree
|
|
77
|
+
.list("family")
|
|
78
|
+
.filter((f) => !f.retracted)
|
|
79
|
+
.map((f) => ({ ...f, partners: f.partners.filter((p) => personIds.has(p)), children: f.children.filter((c) => personIds.has(c.person)) }))
|
|
80
|
+
.filter((f) => f.partners.length + f.children.length > (opts.persons ? 1 : 0));
|
|
81
|
+
const sources = tree.list("source").filter((s) => !s.retracted);
|
|
82
|
+
const sourceById = new Map(sources.map((s) => [s.id, s]));
|
|
83
|
+
const repos = tree.list("repository");
|
|
84
|
+
const recordsets = new Map(tree.list("recordset").map((b) => [b.id, b]));
|
|
85
|
+
// xrefs: our IDs are already unique and stable — use them directly.
|
|
86
|
+
const x = (id) => `@${id}@`;
|
|
87
|
+
const citedSources = new Set();
|
|
88
|
+
// Strom profile: each page of a source cited at several pages gets a source of its own (S0002, S0002_2, …).
|
|
89
|
+
const pageXrefs = new Map();
|
|
90
|
+
const pageXref = (id, page) => {
|
|
91
|
+
const list = pageXrefs.get(id) ?? [];
|
|
92
|
+
let hit = list.find((p) => p.page === page);
|
|
93
|
+
if (!hit) {
|
|
94
|
+
hit = { xref: list.length ? `@${id}_${list.length + 1}@` : x(id), page };
|
|
95
|
+
pageXrefs.set(id, [...list, hit]);
|
|
96
|
+
}
|
|
97
|
+
return hit.xref;
|
|
98
|
+
};
|
|
99
|
+
// Places with coordinates, looked up by folded name.
|
|
100
|
+
const coords = new Map();
|
|
101
|
+
for (const pl of tree.list("place"))
|
|
102
|
+
if (pl.coords)
|
|
103
|
+
for (const n of pl.names)
|
|
104
|
+
coords.set(foldText(n.name), pl.coords);
|
|
105
|
+
// ── header ──
|
|
106
|
+
w.line(0, "HEAD");
|
|
107
|
+
w.line(1, "SOUR", "STROM_RESEARCH");
|
|
108
|
+
w.line(2, "VERS", VERSION);
|
|
109
|
+
w.line(2, "NAME", "Strom Research");
|
|
110
|
+
w.line(1, "DATE", gedDate(new Date()));
|
|
111
|
+
// Which research this is: the Strom app updates the same tree when it is opened again.
|
|
112
|
+
if (opts.for === "strom")
|
|
113
|
+
w.line(1, "_STROM_TREE", tree.config.id);
|
|
114
|
+
w.line(1, "SUBM", "@U1@");
|
|
115
|
+
w.line(1, "GEDC");
|
|
116
|
+
w.line(2, "VERS", "5.5.1");
|
|
117
|
+
w.line(2, "FORM", "LINEAGE-LINKED");
|
|
118
|
+
w.line(1, "CHAR", "UTF-8");
|
|
119
|
+
w.text(1, "NOTE", `${tree.config.name}\n${L("header")}\n${L("generated")}`);
|
|
120
|
+
// ── people ──
|
|
121
|
+
const famc = new Map();
|
|
122
|
+
const fams = new Map();
|
|
123
|
+
for (const f of families) {
|
|
124
|
+
for (const p of f.partners)
|
|
125
|
+
fams.set(p, [...(fams.get(p) ?? []), f.id]);
|
|
126
|
+
for (const c of f.children)
|
|
127
|
+
famc.set(c.person, [...(famc.get(c.person) ?? []), { fam: f.id, relation: c.relation }]);
|
|
128
|
+
}
|
|
129
|
+
for (const p of persons) {
|
|
130
|
+
stats.persons++;
|
|
131
|
+
w.record(x(p.id), "INDI");
|
|
132
|
+
const primary = primaryName(p);
|
|
133
|
+
const nameQuotes = [];
|
|
134
|
+
for (const n of [primary, ...p.names.filter((n) => n !== primary)]) {
|
|
135
|
+
w.line(1, "NAME", gedcomName(n));
|
|
136
|
+
// "birth" says something only next to another name
|
|
137
|
+
if (n.kind && (n.kind !== "birth" || p.names.length > 1))
|
|
138
|
+
w.line(2, "TYPE", NAME_TYPE[n.kind]);
|
|
139
|
+
for (const c of n.citations ?? []) {
|
|
140
|
+
const q = citation(2, c);
|
|
141
|
+
if (q)
|
|
142
|
+
nameQuotes.push(`${L("name")} ${formatName(n)} — ${L("quote")}: „${q}“`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
w.line(1, "SEX", p.sex);
|
|
146
|
+
w.line(1, "REFN", p.id);
|
|
147
|
+
const deferred = [];
|
|
148
|
+
const assos = [];
|
|
149
|
+
for (const e of preferredOrder(p.events))
|
|
150
|
+
deferred.push(...event(e, "INDI", {}, assos));
|
|
151
|
+
// 5.5.1 has associations on the individual only (strict); the event's note names the event.
|
|
152
|
+
for (const a of assos) {
|
|
153
|
+
w.line(1, "ASSO", x(a.person));
|
|
154
|
+
w.line(2, "RELA", a.rela);
|
|
155
|
+
}
|
|
156
|
+
for (const n of p.notes)
|
|
157
|
+
w.text(1, "NOTE", n.text);
|
|
158
|
+
for (const d of [...nameQuotes, ...deferred])
|
|
159
|
+
w.text(1, "NOTE", d);
|
|
160
|
+
story(p.story);
|
|
161
|
+
for (const link of famc.get(p.id) ?? []) {
|
|
162
|
+
w.line(1, "FAMC", x(link.fam));
|
|
163
|
+
if (link.relation === "adopted" || link.relation === "foster")
|
|
164
|
+
w.line(2, "PEDI", link.relation);
|
|
165
|
+
else if (link.relation === "step")
|
|
166
|
+
strict ? w.text(2, "NOTE", L("step")) : w.line(2, "PEDI", "step");
|
|
167
|
+
}
|
|
168
|
+
for (const f of fams.get(p.id) ?? [])
|
|
169
|
+
w.line(1, "FAMS", x(f));
|
|
170
|
+
}
|
|
171
|
+
// ── families ──
|
|
172
|
+
for (const f of families) {
|
|
173
|
+
stats.families++;
|
|
174
|
+
w.record(x(f.id), "FAM");
|
|
175
|
+
const [a, b] = f.partners.map((id) => persons.find((p) => p.id === id));
|
|
176
|
+
// HUSB/WIFE by sex; an unknown sex keeps the order given.
|
|
177
|
+
const husb = [a, b].find((p) => p?.sex === "M") ?? (a?.sex !== "F" ? a : undefined);
|
|
178
|
+
const wife = [a, b].find((p) => p && p !== husb);
|
|
179
|
+
if (husb)
|
|
180
|
+
w.line(1, "HUSB", x(husb.id));
|
|
181
|
+
if (wife)
|
|
182
|
+
w.line(1, "WIFE", x(wife.id));
|
|
183
|
+
// Children by birth, the unknown ones last, in the order they were added.
|
|
184
|
+
const born = (id) => dateYears(birthEvent(personById.get(id))?.date ?? "")[0] ?? Infinity;
|
|
185
|
+
// The record naming the family itself (a child's parents), where no fact of it carries one.
|
|
186
|
+
const deferred = [];
|
|
187
|
+
for (const c of [...f.children].sort((m, n) => born(m.person) - born(n.person))) {
|
|
188
|
+
w.line(1, "CHIL", x(c.person));
|
|
189
|
+
if (!c.relations)
|
|
190
|
+
continue;
|
|
191
|
+
// A child related differently to each parent (a stepchild of the husband, the wife's own):
|
|
192
|
+
// _FREL/_MREL as Legacy, RootsMagic and FTM write it; strict GEDCOM has no place for it but a note.
|
|
193
|
+
const rel = (p) => (p ? relationTo(c, p.id) : undefined);
|
|
194
|
+
const [fr, mr] = [rel(husb), rel(wife)];
|
|
195
|
+
if (!strict) {
|
|
196
|
+
if (fr)
|
|
197
|
+
w.line(2, "_FREL", FREL[fr]);
|
|
198
|
+
if (mr)
|
|
199
|
+
w.line(2, "_MREL", FREL[mr]);
|
|
200
|
+
}
|
|
201
|
+
if (strict || repeat) {
|
|
202
|
+
const as = (r) => L(r === "unknown" ? "unknownRelation" : r);
|
|
203
|
+
const who = [husb && fr ? `${displayName(husb)}: ${as(fr)}` : "", wife && mr ? `${displayName(wife)}: ${as(mr)}` : ""].filter(Boolean).join(", ");
|
|
204
|
+
deferred.push(`${displayName(personById.get(c.person))} — ${who}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const c of f.citations ?? []) {
|
|
208
|
+
const q = citation(1, c);
|
|
209
|
+
if (q)
|
|
210
|
+
deferred.push(`${L("family")} — ${L("quote")}: „${q}“`);
|
|
211
|
+
}
|
|
212
|
+
for (const e of preferredOrder(f.events))
|
|
213
|
+
deferred.push(...event(e, "FAM", { husb: husb?.id, wife: wife?.id }, []));
|
|
214
|
+
for (const n of f.notes)
|
|
215
|
+
w.text(1, "NOTE", n.text);
|
|
216
|
+
for (const d of deferred)
|
|
217
|
+
w.text(1, "NOTE", d);
|
|
218
|
+
story(f.story);
|
|
219
|
+
// No REFN on FAM: the Strom importer reads REFN on individuals only.
|
|
220
|
+
}
|
|
221
|
+
// ── sources and repositories ──
|
|
222
|
+
const usedRepos = new Set();
|
|
223
|
+
for (const s of sources) {
|
|
224
|
+
if (!citedSources.has(s.id) && opts.persons)
|
|
225
|
+
continue;
|
|
226
|
+
// Strom keeps one PAGE per source: another page of it is a source of its own there.
|
|
227
|
+
const pages = strict ? [undefined] : [undefined, ...(pageXrefs.get(s.id) ?? []).slice(1)];
|
|
228
|
+
for (const page of pages)
|
|
229
|
+
writeSource(s, page);
|
|
230
|
+
}
|
|
231
|
+
function writeSource(s, page) {
|
|
232
|
+
stats.sources++;
|
|
233
|
+
w.record(page ? page.xref : x(s.id), "SOUR");
|
|
234
|
+
w.text(1, "TITL", page ? `${s.title} (${page.page})` : s.title);
|
|
235
|
+
const set = s.recordset ? recordsets.get(s.recordset) : undefined;
|
|
236
|
+
const repo = s.repository ?? set?.repository;
|
|
237
|
+
if (repo && repos.some((r) => r.id === repo)) {
|
|
238
|
+
w.line(1, "REPO", x(repo));
|
|
239
|
+
if (set?.callNumber)
|
|
240
|
+
w.line(2, "CALN", set.callNumber);
|
|
241
|
+
usedRepos.add(repo);
|
|
242
|
+
}
|
|
243
|
+
// The words of the record are the source's TEXT (in both files: Strom reads it there).
|
|
244
|
+
if (s.transcript)
|
|
245
|
+
w.text(1, "TEXT", s.transcript);
|
|
246
|
+
const url = s.url ?? set?.url;
|
|
247
|
+
const head = [
|
|
248
|
+
set ? `${L("recordset")}: ${set.title}${set.callNumber && (strict || repeat) ? ` (${set.callNumber})` : ""}` : "",
|
|
249
|
+
s.locator ?? "",
|
|
250
|
+
s.date ? `${L("recorded")}: ${s.date}` : "",
|
|
251
|
+
s.language ? `${L("language")}: ${s.language}` : "",
|
|
252
|
+
url ? `${L("url")}: ${url}${s.accessed ? ` (${s.accessed})` : ""}` : "",
|
|
253
|
+
`${L("evidence")}: ${L(s.form)}, ${L(s.information)}`,
|
|
254
|
+
].filter(Boolean);
|
|
255
|
+
const sections = [
|
|
256
|
+
head.join("\n"),
|
|
257
|
+
s.notes.map((n) => n.text).join("\n"),
|
|
258
|
+
repeat && s.transcript ? `${L("transcript")}:\n${s.transcript}` : "",
|
|
259
|
+
s.translation ? `${L("translation")}:\n${s.translation}` : "",
|
|
260
|
+
].filter(Boolean);
|
|
261
|
+
// One note while Strom keeps a single note per source (a second one replaced the first).
|
|
262
|
+
if (repeat) {
|
|
263
|
+
if (sections.length)
|
|
264
|
+
w.text(1, "NOTE", sections.join("\n\n"));
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
for (const section of sections)
|
|
268
|
+
w.text(1, "NOTE", section);
|
|
269
|
+
// The research's number of the source: standard GEDCOM keeps it; Strom reads REFN on people only.
|
|
270
|
+
if (strict)
|
|
271
|
+
w.line(1, "REFN", s.id);
|
|
272
|
+
}
|
|
273
|
+
for (const r of repos) {
|
|
274
|
+
if (!usedRepos.has(r.id))
|
|
275
|
+
continue;
|
|
276
|
+
stats.repositories++;
|
|
277
|
+
w.record(x(r.id), "REPO");
|
|
278
|
+
w.line(1, "NAME", r.name);
|
|
279
|
+
if (r.url)
|
|
280
|
+
w.text(1, "NOTE", `${L("url")}: ${r.url}`);
|
|
281
|
+
}
|
|
282
|
+
w.record("@U1@", "SUBM");
|
|
283
|
+
w.line(1, "NAME", "Strom Research");
|
|
284
|
+
w.line(0, "TRLR");
|
|
285
|
+
return { text: w.toString(), lines: w.lines, stats };
|
|
286
|
+
// ── one fact; returns notes that must go one level up (value tags) ──
|
|
287
|
+
function event(e, on, couple, assos) {
|
|
288
|
+
if (e.retracted || e.status === "retracted" || e.status === "disproven") {
|
|
289
|
+
stats.skipped++;
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
const allowed = on === "INDI" ? INDI_TAGS : FAM_TAGS;
|
|
293
|
+
let tag = e.kind;
|
|
294
|
+
let typeLabel = e.label;
|
|
295
|
+
if (on === "FAM" && tag === "RESI" && strict) {
|
|
296
|
+
// standard on FAM; Strom does not read it yet, so only in strict
|
|
297
|
+
}
|
|
298
|
+
else if (AS_EVEN[tag] || !allowed.has(tag)) {
|
|
299
|
+
typeLabel = e.label ?? (AS_EVEN[tag] ? L(AS_EVEN[tag]) : L(tag) ?? tag);
|
|
300
|
+
tag = "EVEN";
|
|
301
|
+
}
|
|
302
|
+
stats.events++;
|
|
303
|
+
const up = [];
|
|
304
|
+
const empty = !e.date && !e.place;
|
|
305
|
+
if (VALUE_TAGS.has(tag))
|
|
306
|
+
w.line(1, tag, e.value);
|
|
307
|
+
else
|
|
308
|
+
w.line(1, tag, tag === "EVEN" && !typeLabel ? e.value : empty && Y_TAGS.has(tag) ? "Y" : undefined);
|
|
309
|
+
if (tag === "EVEN" && typeLabel)
|
|
310
|
+
w.line(2, "TYPE", typeLabel);
|
|
311
|
+
if (e.date)
|
|
312
|
+
w.line(2, "DATE", e.date);
|
|
313
|
+
if (e.place) {
|
|
314
|
+
w.line(2, "PLAC", e.place);
|
|
315
|
+
const c = coords.get(foldText(e.place));
|
|
316
|
+
if (c) {
|
|
317
|
+
w.line(3, "MAP");
|
|
318
|
+
w.line(4, "LATI", `${c.lat < 0 ? "S" : "N"}${Math.abs(c.lat)}`);
|
|
319
|
+
w.line(4, "LONG", `${c.lon < 0 ? "W" : "E"}${Math.abs(c.lon)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const noteParts = [];
|
|
323
|
+
// A house is an address within the place: "2 ADDR čp. 13" (the value on the line, as Strom reads it).
|
|
324
|
+
if (e.house) {
|
|
325
|
+
w.line(2, "ADDR", `${L("house")} ${e.house}`);
|
|
326
|
+
if (strict && e.place)
|
|
327
|
+
w.line(3, "CITY", e.place.split(",")[0].trim());
|
|
328
|
+
if (repeat)
|
|
329
|
+
noteParts.push(`${L("house")} ${e.house}`);
|
|
330
|
+
}
|
|
331
|
+
if (e.cause) {
|
|
332
|
+
w.line(2, "CAUS", e.cause);
|
|
333
|
+
if (repeat)
|
|
334
|
+
noteParts.push(`${L("cause")}: ${e.cause}`);
|
|
335
|
+
}
|
|
336
|
+
// Ages: a person's under the event, the partners' under HUSB/WIFE.
|
|
337
|
+
const ageNotes = [];
|
|
338
|
+
if (on === "INDI" && e.age) {
|
|
339
|
+
const age = normalizeAge(e.age);
|
|
340
|
+
if (age && isGedcomAge(age))
|
|
341
|
+
w.line(2, "AGE", age);
|
|
342
|
+
ageNotes.push(humanAge(age ?? e.age, lang));
|
|
343
|
+
}
|
|
344
|
+
if (on === "FAM" && e.ages)
|
|
345
|
+
for (const [role, id] of [["HUSB", couple.husb], ["WIFE", couple.wife]]) {
|
|
346
|
+
const raw = id ? e.ages[id] : undefined;
|
|
347
|
+
if (!raw)
|
|
348
|
+
continue;
|
|
349
|
+
const age = normalizeAge(raw);
|
|
350
|
+
if (!age)
|
|
351
|
+
continue;
|
|
352
|
+
w.line(2, role);
|
|
353
|
+
w.line(3, "AGE", age);
|
|
354
|
+
ageNotes.push(`${displayName(personById.get(id))} ${humanAge(age, lang)}`);
|
|
355
|
+
}
|
|
356
|
+
for (const c of e.citations)
|
|
357
|
+
citation(2, c);
|
|
358
|
+
for (const pt of e.participants ?? [])
|
|
359
|
+
participant(pt, on, noteParts, assos);
|
|
360
|
+
if (e.status === "lead" || e.status === "possible")
|
|
361
|
+
noteParts.unshift(L(e.status));
|
|
362
|
+
if (!VALUE_TAGS.has(tag) && e.value && tag !== "EVEN")
|
|
363
|
+
noteParts.push(e.value);
|
|
364
|
+
// Strom does not read AGE and citation DATA yet: say it in the note too.
|
|
365
|
+
if (repeat && ageNotes.length)
|
|
366
|
+
noteParts.push(`${L("age")}: ${ageNotes.join(", ")}`);
|
|
367
|
+
if (!strict)
|
|
368
|
+
for (const c of e.citations)
|
|
369
|
+
if (c.quote)
|
|
370
|
+
noteParts.push(`${L("quote")}: „${c.quote}“`);
|
|
371
|
+
if (e.note)
|
|
372
|
+
noteParts.push(e.note);
|
|
373
|
+
if (noteParts.length) {
|
|
374
|
+
// Under a value tag a NOTE would be glued onto the fact; it goes to the person instead.
|
|
375
|
+
if (VALUE_TAGS.has(tag))
|
|
376
|
+
up.push(`${L(tag)} — ${e.value}${e.date ? ` (${e.date})` : ""}: ${noteParts.join(" ")}`);
|
|
377
|
+
else
|
|
378
|
+
w.text(2, "NOTE", noteParts.join("\n"));
|
|
379
|
+
}
|
|
380
|
+
return up;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The story of a person or a couple: _STORY as Strom reads it (the facts it
|
|
384
|
+
* rests on as DATA lines, the caveat as NOTE); a note in strict GEDCOM.
|
|
385
|
+
*/
|
|
386
|
+
function story(st) {
|
|
387
|
+
if (!st)
|
|
388
|
+
return;
|
|
389
|
+
if (strict) {
|
|
390
|
+
w.text(1, "NOTE", [st.title, st.text, st.note].filter(Boolean).join("\n\n"));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
w.line(1, "_STORY");
|
|
394
|
+
w.line(2, "TYPE", "vypraveni");
|
|
395
|
+
if (st.title)
|
|
396
|
+
w.text(2, "TITL", st.title);
|
|
397
|
+
w.line(2, "STAT", st.status === "final" ? "hotovo" : "navrh");
|
|
398
|
+
w.text(2, "TEXT", st.text);
|
|
399
|
+
for (const id of st.facts) {
|
|
400
|
+
const e = eventById.get(id);
|
|
401
|
+
if (!e || e.retracted)
|
|
402
|
+
continue;
|
|
403
|
+
const cites = [...new Set(e.citations.map((c) => c.source))].join(", ");
|
|
404
|
+
w.line(2, "DATA", [e.kind, e.date, e.place, e.value, cites ? `[${cites}]` : ""].filter(Boolean).join(" ").slice(0, 200));
|
|
405
|
+
}
|
|
406
|
+
if (st.note)
|
|
407
|
+
w.text(2, "NOTE", st.note);
|
|
408
|
+
}
|
|
409
|
+
/** A source citation at `level`; returns the quote when it has to go into a note (Strom does not read DATA). */
|
|
410
|
+
function citation(level, c) {
|
|
411
|
+
const src = sourceById.get(c.source);
|
|
412
|
+
if (!src)
|
|
413
|
+
return undefined;
|
|
414
|
+
citedSources.add(c.source);
|
|
415
|
+
const page = c.locator ?? src.locator;
|
|
416
|
+
w.line(level, "SOUR", strict ? x(c.source) : pageXref(c.source, page ?? ""));
|
|
417
|
+
if (page)
|
|
418
|
+
w.text(level + 1, "PAGE", page);
|
|
419
|
+
w.line(level + 1, "QUAY", quay(src, c));
|
|
420
|
+
if (!c.quote)
|
|
421
|
+
return undefined;
|
|
422
|
+
if (!strict)
|
|
423
|
+
return c.quote;
|
|
424
|
+
w.line(level + 1, "DATA");
|
|
425
|
+
w.text(level + 2, "TEXT", c.quote);
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
function participant(pt, on, noteParts, assos) {
|
|
429
|
+
const rela = RELA[pt.role] ?? "Participant";
|
|
430
|
+
const linked = pt.person && personIds.has(pt.person) ? pt.person : undefined;
|
|
431
|
+
// A person of the tree outside this export is named, never shown as a bare ID.
|
|
432
|
+
const name = pt.name ?? (pt.person ? (personById.get(pt.person) ? displayName(personById.get(pt.person)) : pt.person) : "");
|
|
433
|
+
if (strict) {
|
|
434
|
+
if (linked && on === "INDI")
|
|
435
|
+
assos.push({ person: linked, rela });
|
|
436
|
+
noteParts.push(`${L(pt.role)}: ${name}${pt.note ? ` (${pt.note})` : ""}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (linked) {
|
|
440
|
+
w.line(2, "ASSO", x(linked));
|
|
441
|
+
w.line(3, "RELA", rela);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
w.line(2, "_WITN", name);
|
|
445
|
+
w.line(3, "RELA", rela);
|
|
446
|
+
}
|
|
447
|
+
if (pt.note)
|
|
448
|
+
w.text(3, "NOTE", pt.note);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function gedDate(d) {
|
|
452
|
+
const m = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"][d.getUTCMonth()];
|
|
453
|
+
return `${d.getUTCDate()} ${m} ${d.getUTCFullYear()}`;
|
|
454
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Human-readable words that end up inside the GEDCOM (notes), in the
|
|
2
|
+
// research language of the tree. English is the fallback.
|
|
3
|
+
const LABELS = {
|
|
4
|
+
en: {
|
|
5
|
+
lead: "Lead — not proven by a record.",
|
|
6
|
+
possible: "Possible — weak evidence.",
|
|
7
|
+
header: "Generated by Strom Research from the research evidence. Facts without a source citation are not proven by a record.",
|
|
8
|
+
generated: "This file is generated — do not edit it; changes are made in the research.",
|
|
9
|
+
OCCU: "Occupation",
|
|
10
|
+
RELI: "Religion",
|
|
11
|
+
TITL: "Title",
|
|
12
|
+
NATI: "Nationality",
|
|
13
|
+
godparent: "godparent",
|
|
14
|
+
witness: "witness",
|
|
15
|
+
officiant: "officiant",
|
|
16
|
+
informant: "informant",
|
|
17
|
+
midwife: "midwife",
|
|
18
|
+
other: "participant",
|
|
19
|
+
transcript: "Transcript",
|
|
20
|
+
translation: "Translation",
|
|
21
|
+
recordset: "Record set",
|
|
22
|
+
url: "Link",
|
|
23
|
+
step: "stepchild",
|
|
24
|
+
MILI: "Military service",
|
|
25
|
+
ENGA: "Engagement",
|
|
26
|
+
RESI: "Residence",
|
|
27
|
+
recorded: "Recorded",
|
|
28
|
+
language: "Language",
|
|
29
|
+
evidence: "Evidence",
|
|
30
|
+
original: "original record",
|
|
31
|
+
derivative: "copy or index",
|
|
32
|
+
authored: "compiled work",
|
|
33
|
+
primary: "primary information",
|
|
34
|
+
secondary: "secondary information",
|
|
35
|
+
unknown: "information not assessed",
|
|
36
|
+
age: "Age",
|
|
37
|
+
quote: "Record",
|
|
38
|
+
name: "Name",
|
|
39
|
+
family: "The family",
|
|
40
|
+
house: "House No.",
|
|
41
|
+
cause: "Cause",
|
|
42
|
+
birth: "own child",
|
|
43
|
+
adopted: "adopted child",
|
|
44
|
+
foster: "foster child",
|
|
45
|
+
unknownRelation: "relation unknown",
|
|
46
|
+
},
|
|
47
|
+
cs: {
|
|
48
|
+
lead: "Vodítko — nedoloženo záznamem.",
|
|
49
|
+
possible: "Možné — slabý doklad.",
|
|
50
|
+
header: "Vygenerováno programem Strom Research z evidence výzkumu. Údaje bez citace pramene nejsou doloženy záznamem.",
|
|
51
|
+
generated: "Soubor je generovaný — needitujte ho; změny se dělají ve výzkumu.",
|
|
52
|
+
OCCU: "Povolání",
|
|
53
|
+
RELI: "Vyznání",
|
|
54
|
+
TITL: "Titul",
|
|
55
|
+
NATI: "Národnost",
|
|
56
|
+
godparent: "kmotr/kmotra",
|
|
57
|
+
witness: "svědek",
|
|
58
|
+
officiant: "oddávající/křtící",
|
|
59
|
+
informant: "oznamovatel",
|
|
60
|
+
midwife: "porodní bába",
|
|
61
|
+
other: "účastník",
|
|
62
|
+
transcript: "Přepis",
|
|
63
|
+
translation: "Překlad",
|
|
64
|
+
recordset: "Fond / svazek",
|
|
65
|
+
url: "Odkaz",
|
|
66
|
+
step: "nevlastní dítě",
|
|
67
|
+
MILI: "Vojenská služba",
|
|
68
|
+
ENGA: "Zasnoubení",
|
|
69
|
+
RESI: "Bydliště",
|
|
70
|
+
recorded: "Zapsáno",
|
|
71
|
+
language: "Jazyk",
|
|
72
|
+
evidence: "Doklad",
|
|
73
|
+
original: "původní záznam",
|
|
74
|
+
derivative: "opis nebo index",
|
|
75
|
+
authored: "zpracované dílo",
|
|
76
|
+
primary: "primární informace",
|
|
77
|
+
secondary: "sekundární informace",
|
|
78
|
+
unknown: "informace nehodnocena",
|
|
79
|
+
age: "Věk",
|
|
80
|
+
quote: "Zápis",
|
|
81
|
+
name: "Jméno",
|
|
82
|
+
family: "Rodina",
|
|
83
|
+
house: "čp.",
|
|
84
|
+
cause: "Příčina",
|
|
85
|
+
birth: "vlastní dítě",
|
|
86
|
+
adopted: "osvojené dítě",
|
|
87
|
+
foster: "dítě v pěstounské péči",
|
|
88
|
+
unknownRelation: "vztah neznámý",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/** GEDCOM RELA for our roles: canonical English, as the Strom app and other programs read them. */
|
|
92
|
+
export const RELA = {
|
|
93
|
+
godparent: "Godparent",
|
|
94
|
+
witness: "Witness",
|
|
95
|
+
officiant: "Officiant",
|
|
96
|
+
informant: "Informant",
|
|
97
|
+
midwife: "Midwife",
|
|
98
|
+
other: "Participant",
|
|
99
|
+
};
|
|
100
|
+
export function labels(lang) {
|
|
101
|
+
const table = LABELS[lang] ?? LABELS.en;
|
|
102
|
+
return (k) => table[k] ?? LABELS.en[k];
|
|
103
|
+
}
|