strom-research 1.7.0 → 1.9.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/README.md +3 -0
- package/assets/lang/cs.json +117 -2
- package/assets/lang/de.json +117 -2
- package/assets/method/core.md +8 -0
- package/assets/method/narrate.md +5 -1
- package/assets/plugins/gates/claude-usage/gate.ts +2 -1
- package/dist/agents/files.js +6 -4
- package/dist/cli/main.js +21 -3
- package/dist/cli/menu-links.js +35 -1
- package/dist/cli/menu-research.js +41 -2
- package/dist/cli/menu-settings.js +17 -1
- package/dist/cli/menu.js +19 -8
- package/dist/cli/ui.js +57 -1
- package/dist/commands/guide.js +6 -1
- package/dist/commands/links.js +39 -3
- package/dist/commands/output.js +4 -2
- package/dist/commands/pack.js +21 -18
- package/dist/commands/research.js +42 -1
- package/dist/commands/session.js +210 -11
- package/dist/commands/setup.js +3 -0
- package/dist/commands/start.js +6 -3
- package/dist/commands/story.js +47 -6
- package/dist/commands/tasks.js +50 -12
- package/dist/commands/trees.js +82 -2
- package/dist/core/changelog.js +223 -0
- package/dist/core/clock.js +20 -0
- package/dist/core/config.js +2 -0
- package/dist/core/connector.js +25 -1
- package/dist/core/directions.js +41 -0
- package/dist/core/edge.js +296 -0
- package/dist/core/frontier.js +12 -4
- package/dist/core/kin.js +40 -1
- package/dist/core/links.js +17 -4
- package/dist/core/live.js +428 -52
- package/dist/core/pack.js +31 -0
- package/dist/core/phrases.js +61 -0
- package/dist/core/queue.js +15 -4
- package/dist/core/relocate.js +4 -4
- package/dist/core/session.js +8 -2
- package/dist/core/stromapp.js +22 -0
- package/dist/core/trash.js +72 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/workers.js +28 -3
- package/dist/gedcom/export.js +105 -1
- package/dist/gedcom/validate.js +7 -1
- package/dist/runners/claude.js +2 -1
- package/dist/runners/jsonl.js +2 -2
- package/dist/runners/runner.js +16 -3
- package/dist/runners/script.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// The edge of the tree: the people above whom it does not go on — no parents, one of them, or parents no record
|
|
2
|
+
// proves — and what the research knows there. Why it ends (what the records say so far: the known books of the
|
|
3
|
+
// birth, the years searched in vain, the records that begin later or are lost), what comes next (the work on it and
|
|
4
|
+
// where it stands in the queue, or that the user decides), and the hypotheses that would join a family nothing
|
|
5
|
+
// links to the tree yet. Read only, computed from the research: what the Strom app shows on the card where the
|
|
6
|
+
// tree ends (_STROM_EDGE, _STROM_ISLAND) and what `strom edge` says.
|
|
7
|
+
import { now } from "./tree.js";
|
|
8
|
+
import { Settings } from "./config.js";
|
|
9
|
+
import { aboutPeople, directionOf, scopes } from "./directions.js";
|
|
10
|
+
import { birthEstimate, birthRecordProven, birthWindow, frontier, FRONTIER_LEVELS, RECORD_KINDS, recordsetsCovering } from "./frontier.js";
|
|
11
|
+
import { kinship, offTree } from "./kin.js";
|
|
12
|
+
import { familiesAsChild, familiesAsPartner, parentsOf, primaryName } from "./people.js";
|
|
13
|
+
import { livingBorn } from "./review.js";
|
|
14
|
+
import { effectiveState, rankTasks } from "./queue.js";
|
|
15
|
+
import { subjectPeople } from "./records.js";
|
|
16
|
+
import { foldText } from "./text.js";
|
|
17
|
+
/** What is missing above the person: both parents, one of them, or a record that proves the parents recorded. */
|
|
18
|
+
export const EDGE_MISSING = ["parents", "father", "mother", "proof"];
|
|
19
|
+
/**
|
|
20
|
+
* Whether the research reaches them: a direction of it does (in), would but stops at its limit (limit), is paused
|
|
21
|
+
* or ended (paused, done); none does — they are most likely alive (living), family of the tree no direction goes
|
|
22
|
+
* through (outside), or a family nothing links to the tree yet (off-tree).
|
|
23
|
+
*/
|
|
24
|
+
export const EDGE_SCOPES = ["in", "limit", "paused", "done", "living", "outside", "off-tree"];
|
|
25
|
+
/**
|
|
26
|
+
* What the records say so far, from the most final: the baptism names no father (born out of wedlock); every year of the
|
|
27
|
+
* birth is searched in vain or has no records — lost, before the known records begin, a gap in them; searched in
|
|
28
|
+
* vain everywhere strom knew to look; the books left are only in the archive; searched in part; not searched yet;
|
|
29
|
+
* no book of the birthplace known; the birthplace unknown; nothing to go on (no year, no place).
|
|
30
|
+
*/
|
|
31
|
+
export const EDGE_ENDS = ["unnamed", "lost", "before-records", "gap", "not-found", "offline", "partly", "unsearched", "no-books", "no-place", "no-clue"];
|
|
32
|
+
/**
|
|
33
|
+
* What comes next: an agent works on it now, it waits for the user, it is in the queue, it waits out of the queue
|
|
34
|
+
* (a direction paused, people off the tree, put aside), strom proposes it with the next session, everything strom
|
|
35
|
+
* could propose was tried (the user decides), nothing.
|
|
36
|
+
*/
|
|
37
|
+
export const EDGE_NEXT = ["working", "waiting", "queued", "held", "proposed", "decide", "none"];
|
|
38
|
+
const BIRTH_KINDS = RECORD_KINDS.BIRT;
|
|
39
|
+
const LIVE = new Set(["open", "doing", "waiting", "parked"]);
|
|
40
|
+
const FINISHED = new Set(["done", "dropped"]);
|
|
41
|
+
const EDGE_FACTS = new Set(["BIRT", "CHR", "BAPM", "FAMC"]);
|
|
42
|
+
function yearsOf(text) {
|
|
43
|
+
const m = /^\s*(\d{3,4})(?:\s*[-–]\s*(\d{3,4}))?\s*$/.exec(text ?? "");
|
|
44
|
+
if (!m)
|
|
45
|
+
return undefined;
|
|
46
|
+
const a = Number(m[1]);
|
|
47
|
+
const b = m[2] ? Number(m[2]) : a;
|
|
48
|
+
return { from: Math.min(a, b), to: Math.max(a, b) };
|
|
49
|
+
}
|
|
50
|
+
/** Single years as ranges ([1780, 1781, 1782, 1790] → 1780–1782, 1790). */
|
|
51
|
+
function ranges(years) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const y of [...years].sort((a, b) => a - b)) {
|
|
54
|
+
const last = out[out.length - 1];
|
|
55
|
+
if (last && y === last.to + 1)
|
|
56
|
+
last.to = y;
|
|
57
|
+
else
|
|
58
|
+
out.push({ from: y, to: y });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
const within = (y, r) => !r || (y >= r.from && y <= r.to);
|
|
63
|
+
/** The families nothing links to the tree, person by person (none when the tree has nobody it is for). */
|
|
64
|
+
function islands(tree, kin, tasks, hypotheses) {
|
|
65
|
+
const out = new Map();
|
|
66
|
+
if (!kin.size)
|
|
67
|
+
return out;
|
|
68
|
+
const off = offTree(tree);
|
|
69
|
+
for (const p of tree.list("person")) {
|
|
70
|
+
if (p.retracted || kin.has(p.id) || out.has(p.id))
|
|
71
|
+
continue;
|
|
72
|
+
const people = new Set();
|
|
73
|
+
const queue = [p.id];
|
|
74
|
+
while (queue.length) {
|
|
75
|
+
const id = queue.shift();
|
|
76
|
+
if (people.has(id) || kin.has(id))
|
|
77
|
+
continue;
|
|
78
|
+
people.add(id);
|
|
79
|
+
queue.push(...familiesAsPartner(tree, id).flatMap((f) => [...f.partners, ...f.children.map((c) => c.person)]), ...familiesAsChild(tree, id).flatMap((f) => f.partners));
|
|
80
|
+
}
|
|
81
|
+
const island = {
|
|
82
|
+
people: [...people],
|
|
83
|
+
hypotheses: hypotheses
|
|
84
|
+
.map((h) => ({ h, about: aboutPeople(tree, h.subject) }))
|
|
85
|
+
.filter(({ about }) => about.some((x) => people.has(x)) && about.some((x) => kin.has(x)))
|
|
86
|
+
.map(({ h, about }) => ({ id: h.id, joins: about.filter((x) => kin.has(x)) })),
|
|
87
|
+
held: tasks.filter((t) => LIVE.has(t.state) && off(t) && aboutPeople(tree, t.subject).some((x) => people.has(x))).length,
|
|
88
|
+
};
|
|
89
|
+
for (const id of people)
|
|
90
|
+
out.set(id, island);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** Every edge of the tree with something the research knows of it, and the families nothing links to the tree. */
|
|
95
|
+
export function treeEdges(tree) {
|
|
96
|
+
const lang = tree.lang;
|
|
97
|
+
const today = now().slice(0, 10);
|
|
98
|
+
const kin = kinship(tree);
|
|
99
|
+
const all = scopes(tree).filter((s) => !s.research.retracted);
|
|
100
|
+
const tasks = tree.list("task").filter((t) => !t.retracted);
|
|
101
|
+
const hypotheses = tree.list("hypothesis").filter((h) => !h.retracted && h.state === "open");
|
|
102
|
+
const conflicts = tree.list("conflict").filter((c) => !c.retracted && c.state === "open");
|
|
103
|
+
const searches = tree.list("search").filter((q) => !q.retracted);
|
|
104
|
+
const sessions = tree.list("session");
|
|
105
|
+
const researches = new Map(tree.list("research").map((r) => [r.id, r]));
|
|
106
|
+
const ranked = rankTasks(tree, tasks, new Settings(tree.env, {}).strategy(tree.config)).map((r) => r.task.id);
|
|
107
|
+
const position = new Map(ranked.map((id, i) => [id, i + 1]));
|
|
108
|
+
const off = offTree(tree);
|
|
109
|
+
const isles = islands(tree, kin, tasks, hypotheses);
|
|
110
|
+
const frontiers = new Map();
|
|
111
|
+
const frontierOf = (r) => {
|
|
112
|
+
if (!frontiers.has(r.id))
|
|
113
|
+
frontiers.set(r.id, new Map(frontier(tree, r).map((i) => [i.person.id, i])));
|
|
114
|
+
return frontiers.get(r.id);
|
|
115
|
+
};
|
|
116
|
+
const hypAbout = hypotheses.map((h) => ({ h, about: aboutPeople(tree, h.subject) }));
|
|
117
|
+
const edges = new Map();
|
|
118
|
+
for (const p of tree.list("person")) {
|
|
119
|
+
if (p.retracted)
|
|
120
|
+
continue;
|
|
121
|
+
const parents = parentsOf(tree, p.id);
|
|
122
|
+
const proven = birthRecordProven(p);
|
|
123
|
+
let missing;
|
|
124
|
+
if (parents.length === 0)
|
|
125
|
+
missing = "parents";
|
|
126
|
+
else if (parents.length === 1)
|
|
127
|
+
missing = parents[0].sex === "F" ? "father" : parents[0].sex === "M" ? "mother" : "parents";
|
|
128
|
+
else if (!proven)
|
|
129
|
+
missing = "proof";
|
|
130
|
+
else
|
|
131
|
+
continue;
|
|
132
|
+
// ── whether the research reaches them ──
|
|
133
|
+
const est = birthEstimate(tree, p, lang);
|
|
134
|
+
const window = birthWindow(est);
|
|
135
|
+
const reach = all.filter((s) => (s.people.get(p.id) ?? 0) >= 1).sort((a, b) => a.people.size - b.people.size || a.research.id.localeCompare(b.research.id));
|
|
136
|
+
const limited = (s) => {
|
|
137
|
+
const l = s.research.limits;
|
|
138
|
+
return !!((l?.generations && s.people.get(p.id) >= l.generations) || (l?.before && est.year && est.year < l.before));
|
|
139
|
+
};
|
|
140
|
+
const active = reach.filter((s) => s.research.state === "active");
|
|
141
|
+
const through = active.find((s) => !limited(s)) ?? active[0] ?? reach.find((s) => s.research.state === "paused") ?? reach[0];
|
|
142
|
+
const scope = active.some((s) => !limited(s))
|
|
143
|
+
? "in"
|
|
144
|
+
: active.length
|
|
145
|
+
? "limit"
|
|
146
|
+
: through
|
|
147
|
+
? through.research.state
|
|
148
|
+
: livingBorn(tree, p) !== undefined
|
|
149
|
+
? "living"
|
|
150
|
+
: kin.size && !kin.has(p.id)
|
|
151
|
+
? "off-tree"
|
|
152
|
+
: "outside";
|
|
153
|
+
// ── the books of the birth, and the work on the parents ──
|
|
154
|
+
const placeBooks = est.place ? recordsetsCovering(tree, est.place, undefined, BIRTH_KINDS).filter((b) => !b.retracted) : [];
|
|
155
|
+
const books = placeBooks.filter((b) => !window || !b.years || (() => {
|
|
156
|
+
const y = yearsOf(b.years);
|
|
157
|
+
return !y || (y.from <= window.to && y.to >= window.from);
|
|
158
|
+
})());
|
|
159
|
+
const bookIds = new Set(books.map((b) => b.id));
|
|
160
|
+
const hyps = hypAbout.filter(({ about }) => about.includes(p.id)).map(({ h, about }) => ({ h, about }));
|
|
161
|
+
const hypIds = new Set(hyps.map(({ h }) => h.id));
|
|
162
|
+
const onParents = (t) => (t.subject.includes(p.id) && (FRONTIER_LEVELS.has(t.level) || ((t.level === "verify" || t.level === "enrich") && t.where.some((w) => bookIds.has(w))))) ||
|
|
163
|
+
t.subject.some((s) => hypIds.has(s));
|
|
164
|
+
const mine = tasks.filter(onParents);
|
|
165
|
+
const mineIds = new Set(mine.map((t) => t.id));
|
|
166
|
+
const surname = foldText(primaryName(p).surname);
|
|
167
|
+
const sought = searches.filter((q) => (q.task ? mineIds.has(q.task) : !!surname && (q.scope.surnames ?? []).some((s) => foldText(s) === surname) && q.recordsets.some((b) => bookIds.has(b))));
|
|
168
|
+
const edgeTasks = mine
|
|
169
|
+
.filter((t) => LIVE.has(effectiveState(t, today)))
|
|
170
|
+
.map((t) => {
|
|
171
|
+
const state = effectiveState(t, today);
|
|
172
|
+
const at = position.get(t.id);
|
|
173
|
+
const dir = directionOf(tree, t, all);
|
|
174
|
+
const dirState = dir ? researches.get(dir)?.state : undefined;
|
|
175
|
+
const held = state === "parked" ? "parked" : state !== "open" || at ? undefined : dirState === "paused" || dirState === "done" ? dirState : off(t) ? "off-tree" : undefined;
|
|
176
|
+
const on = state === "waiting" ? t.waitingOn : state === "parked" ? t.parkedReason : undefined;
|
|
177
|
+
return {
|
|
178
|
+
id: t.id,
|
|
179
|
+
level: t.level,
|
|
180
|
+
state,
|
|
181
|
+
what: t.what,
|
|
182
|
+
...(at && state === "open" ? { position: at } : {}),
|
|
183
|
+
...(held ? { held } : {}),
|
|
184
|
+
...(state === "parked" && t.parkedUntil ? { until: t.parkedUntil } : {}),
|
|
185
|
+
...(on ? { on } : {}),
|
|
186
|
+
};
|
|
187
|
+
})
|
|
188
|
+
.sort((a, b) => (a.position ?? 1e9) - (b.position ?? 1e9) || a.id.localeCompare(b.id));
|
|
189
|
+
const tried = mine.filter((t) => FINISHED.has(t.state)).map((t) => t.id);
|
|
190
|
+
const item = through && through.research.state === "active" ? frontierOf(through.research).get(p.id) : undefined;
|
|
191
|
+
const next = edgeTasks.some((t) => t.state === "doing")
|
|
192
|
+
? "working"
|
|
193
|
+
: edgeTasks.some((t) => t.state === "waiting")
|
|
194
|
+
? "waiting"
|
|
195
|
+
: edgeTasks.some((t) => t.position)
|
|
196
|
+
? "queued"
|
|
197
|
+
: edgeTasks.length
|
|
198
|
+
? "held"
|
|
199
|
+
: scope === "in" && item?.proposal
|
|
200
|
+
? "proposed"
|
|
201
|
+
: tried.length || item?.exhausted
|
|
202
|
+
? "decide"
|
|
203
|
+
: "none";
|
|
204
|
+
// ── what the records say so far: each year of the window searched in vain, without records, or open ──
|
|
205
|
+
const covered = [];
|
|
206
|
+
const noRecords = [];
|
|
207
|
+
const lostYears = [];
|
|
208
|
+
const open = [];
|
|
209
|
+
if (window && placeBooks.length)
|
|
210
|
+
for (let y = window.from; y <= window.to; y++) {
|
|
211
|
+
const of = books.filter((b) => within(y, yearsOf(b.years)));
|
|
212
|
+
const kept = of.filter((b) => b.access !== "lost");
|
|
213
|
+
if (!of.length)
|
|
214
|
+
noRecords.push(y);
|
|
215
|
+
else if (!kept.length)
|
|
216
|
+
lostYears.push(y);
|
|
217
|
+
else if (kept.every((b) => sought.some((q) => q.result === "negative" && q.recordsets.includes(b.id) && within(y, yearsOf(q.scope.years) ?? (q.scope.pages ? { from: 0, to: -1 } : yearsOf(b.years))))))
|
|
218
|
+
covered.push(y);
|
|
219
|
+
else
|
|
220
|
+
open.push({ year: y, online: kept.some((b) => b.access === "online-free" || b.access === "online-login") });
|
|
221
|
+
}
|
|
222
|
+
const earliest = Math.min(...placeBooks.map((b) => yearsOf(b.years)?.from ?? Infinity));
|
|
223
|
+
let end;
|
|
224
|
+
// the baptism is read and names the mother alone (a child born out of wedlock); a mother missing is one not recorded yet
|
|
225
|
+
if (missing === "father" && proven)
|
|
226
|
+
end = "unnamed";
|
|
227
|
+
else if (!est.place)
|
|
228
|
+
end = est.year ? "no-place" : "no-clue";
|
|
229
|
+
else if (!placeBooks.length)
|
|
230
|
+
end = "no-books";
|
|
231
|
+
else if (window && !open.length)
|
|
232
|
+
end = lostYears.length
|
|
233
|
+
? "lost"
|
|
234
|
+
: noRecords.length === window.to - window.from + 1 && earliest > window.to
|
|
235
|
+
? "before-records"
|
|
236
|
+
: noRecords.length
|
|
237
|
+
? noRecords.every((y) => y < earliest)
|
|
238
|
+
? "before-records"
|
|
239
|
+
: "gap"
|
|
240
|
+
: "not-found";
|
|
241
|
+
else if (item?.exhausted)
|
|
242
|
+
end = "not-found";
|
|
243
|
+
else if (window && open.every((o) => !o.online))
|
|
244
|
+
end = "offline";
|
|
245
|
+
else
|
|
246
|
+
end = sought.length || tried.length ? "partly" : "unsearched";
|
|
247
|
+
// ── the hypotheses, conflicts, and what the work cost ──
|
|
248
|
+
// a hypothesis that names people on and off the tree would join them: the people on the other side of it
|
|
249
|
+
const onTree = kin.has(p.id);
|
|
250
|
+
const edgeHyps = hyps.map(({ h, about }) => {
|
|
251
|
+
const joining = kin.size > 0 && about.some((x) => kin.has(x)) && about.some((x) => !kin.has(x));
|
|
252
|
+
const joins = joining ? about.filter((x) => kin.has(x) !== onTree) : [];
|
|
253
|
+
const isle = joins.length && onTree ? isles.get(joins[0]) : undefined;
|
|
254
|
+
return {
|
|
255
|
+
id: h.id,
|
|
256
|
+
question: h.question,
|
|
257
|
+
joins,
|
|
258
|
+
...(isle ? { island: { people: isle.people.length, held: isle.held } } : {}),
|
|
259
|
+
tests: tasks.filter((t) => LIVE.has(t.state) && t.subject.includes(h.id)).map((t) => t.id),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
const edgeConflicts = conflicts
|
|
263
|
+
.filter((c) => subjectPeople(tree, c.subject).includes(p.id) && (!c.fact || EDGE_FACTS.has(c.fact)))
|
|
264
|
+
.map((c) => c.id);
|
|
265
|
+
const spent = sessions.filter((s) => s.task && mineIds.has(s.task));
|
|
266
|
+
const cost = spent.reduce((n, s) => n + (s.metrics?.costUsd ?? 0), 0);
|
|
267
|
+
const partial = spent.some((s) => s.state !== "open" && (s.metrics?.costPartial || s.metrics?.costUsd === undefined));
|
|
268
|
+
const last = [...spent.map((s) => s.ended ?? s.started), ...sought.map((q) => q.created)].sort().pop()?.slice(0, 10);
|
|
269
|
+
const known = scope === "in" || scope === "limit" || scope === "paused" || scope === "done" || edgeTasks.length || tried.length || edgeHyps.length || sought.length || edgeConflicts.length;
|
|
270
|
+
if (!known)
|
|
271
|
+
continue;
|
|
272
|
+
edges.set(p.id, {
|
|
273
|
+
person: p.id,
|
|
274
|
+
missing,
|
|
275
|
+
scope,
|
|
276
|
+
...(through ? { research: through.research.id, generation: through.people.get(p.id) } : {}),
|
|
277
|
+
end,
|
|
278
|
+
next,
|
|
279
|
+
...(est.year || est.place ? { estimate: { ...(est.year ? { year: est.year } : {}), ...(est.place ? { place: est.place } : {}), ...(est.basis ? { basis: est.basis } : {}) } } : {}),
|
|
280
|
+
...(window ? { window } : {}),
|
|
281
|
+
...(Number.isFinite(earliest) && (end === "before-records" || noRecords.some((y) => y < earliest)) ? { recordsFrom: earliest } : {}),
|
|
282
|
+
books: books.map((b) => ({ id: b.id, title: b.title, ...(b.years ? { years: b.years } : {}), access: b.access })),
|
|
283
|
+
covered: ranges(covered),
|
|
284
|
+
noRecords: ranges(noRecords),
|
|
285
|
+
tasks: edgeTasks,
|
|
286
|
+
tried,
|
|
287
|
+
hypotheses: edgeHyps,
|
|
288
|
+
conflicts: edgeConflicts,
|
|
289
|
+
searches: sought.length,
|
|
290
|
+
sessions: spent.length,
|
|
291
|
+
...(spent.length ? { cost: Math.round(cost * 100) / 100, ...(partial ? { costPartial: true } : {}) } : {}),
|
|
292
|
+
...(last ? { last } : {}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return { edges, islands: isles };
|
|
296
|
+
}
|
package/dist/core/frontier.js
CHANGED
|
@@ -7,8 +7,8 @@ import { foldText } from "./text.js";
|
|
|
7
7
|
import { phrase } from "./phrases.js";
|
|
8
8
|
const OPEN_STATES = new Set(["open", "doing", "parked", "waiting"]);
|
|
9
9
|
const FINISHED_STATES = new Set(["done", "dropped"]);
|
|
10
|
-
const FRONTIER_LEVELS = new Set(["locate", "link", "request"]);
|
|
11
|
-
function birthRecordProven(p) {
|
|
10
|
+
export const FRONTIER_LEVELS = new Set(["locate", "link", "request"]);
|
|
11
|
+
export function birthRecordProven(p) {
|
|
12
12
|
return p.events.some((e) => ["BIRT", "CHR", "BAPM"].includes(e.kind) && (e.status === "proven" || e.status === "probable") && !e.retracted);
|
|
13
13
|
}
|
|
14
14
|
/** The kinds of record set in which an event of this kind is written down. */
|
|
@@ -120,7 +120,15 @@ export function birthEstimate(tree, p, lang) {
|
|
|
120
120
|
const year = ownYear ?? (marrYear ? marrYear - 25 : eldest ? eldest.year - 28 : undefined);
|
|
121
121
|
const place = own?.place ?? marr?.place ?? eldest?.b?.place;
|
|
122
122
|
const from = ownYear ? undefined : marrYear ? phrase(lang, "estimate.marriage", { year: marrYear }) : eldest ? phrase(lang, "estimate.child", { year: eldest.year }) : undefined;
|
|
123
|
-
|
|
123
|
+
const basis = ownYear || !year ? undefined : marrYear ? { kind: "MARR", year: marrYear } : { kind: "child", year: eldest.year };
|
|
124
|
+
return { ...(year ? { year } : {}), ...(place ? { place } : {}), ...(from ? { from } : {}), ...(basis ? { basis } : {}) };
|
|
125
|
+
}
|
|
126
|
+
/** The years a search for a birth has to cover: a few around a known year, wider around an estimate. */
|
|
127
|
+
export function birthWindow(est) {
|
|
128
|
+
if (!est.year)
|
|
129
|
+
return undefined;
|
|
130
|
+
const span = est.from ? 10 : 3;
|
|
131
|
+
return { from: est.year - span, to: est.year + span };
|
|
124
132
|
}
|
|
125
133
|
/**
|
|
126
134
|
* The people a research is about, each with their generation from its focus:
|
|
@@ -202,7 +210,7 @@ export function frontier(tree, research) {
|
|
|
202
210
|
const tried = new Set(finished.filter((t) => t.level === "link").flatMap((t) => t.where));
|
|
203
211
|
const place = est.place;
|
|
204
212
|
// an estimate needs a wider search than a known year
|
|
205
|
-
const span = est.from ? 10 : 3;
|
|
213
|
+
const span = est.from ? 10 : 3; // as birthWindow
|
|
206
214
|
const about = est.from && year ? phrase(lang, "born.about", { year, from: est.from }) : "";
|
|
207
215
|
const reason = phrase(lang, hasBirthFamily ? "reason.unproven" : "reason.unknown");
|
|
208
216
|
const sets = candidateRecordsets(tree, place, year).filter((b) => !tried.has(b.id));
|
package/dist/core/kin.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// app opens on them), and how near anyone is to the people the research is
|
|
3
3
|
// for (which entries get their image in the Strom app first).
|
|
4
4
|
import { Settings } from "./config.js";
|
|
5
|
+
import { aboutPeople } from "./directions.js";
|
|
5
6
|
import { ancestorGenerations, familiesAsChild, familiesAsPartner } from "./people.js";
|
|
6
7
|
function researches(tree) {
|
|
7
8
|
return tree
|
|
@@ -57,7 +58,7 @@ export function mainPerson(tree) {
|
|
|
57
58
|
* their ancestors 0, the partners, children and children's partners of those 1, anyone linked by family further 2.
|
|
58
59
|
*/
|
|
59
60
|
export function kinship(tree) {
|
|
60
|
-
const roots = [...new Set([...researches(tree).
|
|
61
|
+
const roots = [...new Set([...researches(tree).flatMap((r) => [r.focus, ...(r.review?.people ?? [])]), mainPerson(tree)].filter((x) => !!x))];
|
|
61
62
|
const out = new Map();
|
|
62
63
|
for (const r of roots)
|
|
63
64
|
for (const id of ancestorGenerations(tree, r).keys())
|
|
@@ -88,3 +89,41 @@ export function kinship(tree) {
|
|
|
88
89
|
}
|
|
89
90
|
return out;
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Work about people nothing links to the tree yet: none of the people a task is about (with those of the conflicts
|
|
94
|
+
* and hypotheses it names) is kin of the people the research is for. It waits out of the queue until the user asks
|
|
95
|
+
* for it (a direction or a review of one of them makes them the research's) or a hypothesis that joins them to the
|
|
96
|
+
* tree is decided and recorded. A task that tests that hypothesis names it (--about H…) or the person of the tree
|
|
97
|
+
* it would join, and so is in the queue. The user's material (intake) and work at hand never wait; a tree with
|
|
98
|
+
* nobody to be for holds nothing.
|
|
99
|
+
*/
|
|
100
|
+
export function offTree(tree) {
|
|
101
|
+
const kin = kinship(tree);
|
|
102
|
+
if (!kin.size)
|
|
103
|
+
return () => false;
|
|
104
|
+
return (t) => {
|
|
105
|
+
if (t.level === "intake" || t.state === "doing")
|
|
106
|
+
return false;
|
|
107
|
+
const about = aboutPeople(tree, t.subject);
|
|
108
|
+
return about.length > 0 && !about.some((p) => kin.has(p));
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The open hypotheses that would join people off the tree to it: each names someone of the tree and someone of
|
|
113
|
+
* their family (the people family links join to them) — the work to do before any work on them.
|
|
114
|
+
*/
|
|
115
|
+
export function joiningHypotheses(tree, people) {
|
|
116
|
+
const kin = kinship(tree);
|
|
117
|
+
const family = new Set();
|
|
118
|
+
const queue = people.filter((p) => !kin.has(p));
|
|
119
|
+
while (queue.length) {
|
|
120
|
+
const p = queue.shift();
|
|
121
|
+
if (family.has(p))
|
|
122
|
+
continue;
|
|
123
|
+
family.add(p);
|
|
124
|
+
queue.push(...familiesAsPartner(tree, p).flatMap((f) => [...f.partners, ...f.children.map((c) => c.person)]), ...familiesAsChild(tree, p).flatMap((f) => f.partners));
|
|
125
|
+
}
|
|
126
|
+
return tree
|
|
127
|
+
.list("hypothesis")
|
|
128
|
+
.filter((h) => h.state === "open" && h.subject.some((s) => family.has(s)) && h.subject.some((s) => kin.has(s)));
|
|
129
|
+
}
|
package/dist/core/links.js
CHANGED
|
@@ -29,15 +29,19 @@ export const LINK_SCHEME = "strom-research";
|
|
|
29
29
|
* as it is now into the app's window, open the research's menu, a conversation with the agent (of one person),
|
|
30
30
|
* a task (answered, put aside, given up, back in the queue), a person's review, a new direction from a person —
|
|
31
31
|
* and (the app's second wave) a new research with a tree of the app, strom's update, the agent's sessions and
|
|
32
|
-
* what they cost, a conflict decided (by the user or the agent), a story approved, a sync taken back, the setup
|
|
32
|
+
* what they cost, a conflict decided (by the user or the agent), a story approved, a sync taken back, the setup —
|
|
33
|
+
* and (the app's 3.5.0) the research followed live in the app's window; a direction of the research paused, ended or
|
|
34
|
+
* taken up again, a conversation about one direction, a session at work asked to finish.
|
|
33
35
|
*/
|
|
34
|
-
export const LINK_ACTIONS = ["send", "excerpt", "app", "open", "chat", "task", "review", "research", "new", "update", "sessions", "conflict", "story", "sync-undo", "setup"];
|
|
36
|
+
export const LINK_ACTIONS = ["send", "excerpt", "app", "open", "chat", "task", "review", "research", "new", "update", "sessions", "conflict", "story", "sync-undo", "setup", "live", "direction", "finish"];
|
|
35
37
|
/** A person's review: the person, with the family, with the ancestors (strom review --scope). */
|
|
36
38
|
export const LINK_SCOPES = ["person", "family", "line"];
|
|
37
39
|
/** A new direction from a person (strom research new --direction). */
|
|
38
40
|
export const LINK_DIRECTIONS = ["ancestors", "descendants"];
|
|
39
41
|
/** What a link does with a task: put it aside, give it up, back into the queue (none: answer it). */
|
|
40
42
|
export const LINK_TASK_DOS = ["park", "drop", "wake"];
|
|
43
|
+
/** What a link does with a direction of the research (strom research pause|done|resume). */
|
|
44
|
+
export const LINK_DIRECTION_DOS = ["pause", "done", "resume"];
|
|
41
45
|
/** What a link does with a conflict: the user decides it, or leaves it to the agent. */
|
|
42
46
|
export const LINK_CONFLICT_DOS = ["decide", "agent"];
|
|
43
47
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -46,6 +50,8 @@ const PERSON = /^P\d{1,9}$/;
|
|
|
46
50
|
const TASK = /^T\d{1,9}$/;
|
|
47
51
|
const CONFLICT = /^X\d{1,9}$/;
|
|
48
52
|
const INPUT = /^I\d{1,9}$/;
|
|
53
|
+
const RESEARCH = /^G\d{1,9}$/;
|
|
54
|
+
const SESSION = /^N\d{1,9}$/;
|
|
49
55
|
/** The app's own mark of a tree it hands over (32 random bytes, base64url). */
|
|
50
56
|
export const APP_TOKEN = /^[A-Za-z0-9_-]{22,43}$/;
|
|
51
57
|
/** An excerpt's mark (_STROM_CLIP): letters, digits and "-", at most 32. */
|
|
@@ -103,6 +109,7 @@ export function parseLink(text) {
|
|
|
103
109
|
switch (action) {
|
|
104
110
|
case "send":
|
|
105
111
|
case "app":
|
|
112
|
+
case "live":
|
|
106
113
|
case "open":
|
|
107
114
|
case "update":
|
|
108
115
|
case "sessions":
|
|
@@ -115,8 +122,10 @@ export function parseLink(text) {
|
|
|
115
122
|
throw new LinkError("no excerpt named");
|
|
116
123
|
return { action, tree: id, source, clip };
|
|
117
124
|
}
|
|
118
|
-
case "chat":
|
|
119
|
-
|
|
125
|
+
case "chat": {
|
|
126
|
+
const research = idOf(q, "research", RESEARCH, false);
|
|
127
|
+
return { action, tree: id, ...(person ? { person } : {}), ...(research ? { research } : {}) };
|
|
128
|
+
}
|
|
120
129
|
case "task": {
|
|
121
130
|
const task = idOf(q, "task", TASK, true);
|
|
122
131
|
return { action, tree: id, task, ...(q.has("do") ? { do: oneOf(q, "do", LINK_TASK_DOS) } : {}) };
|
|
@@ -136,6 +145,10 @@ export function parseLink(text) {
|
|
|
136
145
|
return { action, tree: id, id: idOf(q, "id", CONFLICT, true), do: oneOf(q, "do", LINK_CONFLICT_DOS, "decide") };
|
|
137
146
|
case "sync-undo":
|
|
138
147
|
return { action, tree: id, intake: idOf(q, "intake", INPUT, true) };
|
|
148
|
+
case "direction":
|
|
149
|
+
return { action, tree: id, id: idOf(q, "id", RESEARCH, true), do: oneOf(q, "do", LINK_DIRECTION_DOS) };
|
|
150
|
+
case "finish":
|
|
151
|
+
return { action, tree: id, session: idOf(q, "session", SESSION, true) };
|
|
139
152
|
}
|
|
140
153
|
throw new LinkError(`unknown action "${action.slice(0, 32)}"`);
|
|
141
154
|
}
|