strom-research 1.13.1 → 1.13.2-beta.2
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/assets/lang/cs.json +7 -0
- package/assets/lang/de.json +7 -0
- package/assets/method/core.md +13 -0
- package/assets/method/recording.md +4 -1
- package/dist/cli/menu-links.js +26 -3
- package/dist/cli/ui.js +3 -0
- package/dist/commands/analysis.js +324 -14
- package/dist/commands/links.js +3 -2
- package/dist/commands/output.js +6 -2
- package/dist/commands/session.js +5 -2
- package/dist/core/changelog.js +2 -0
- package/dist/core/conflicts.js +105 -0
- package/dist/core/directions.js +26 -5
- package/dist/core/edge.js +48 -7
- package/dist/core/links.js +8 -2
- package/dist/core/live.js +168 -5
- package/dist/core/model.js +7 -0
- package/dist/core/phrases.js +4 -0
- package/dist/core/review.js +8 -0
- package/dist/core/schema.js +26 -3
- package/dist/core/stromapp.js +45 -0
- package/dist/core/sync.js +38 -5
- package/dist/core/tree.js +1 -1
- package/dist/core/validate.js +39 -0
- package/dist/gedcom/export.js +77 -5
- package/dist/gedcom/lines.js +1 -1
- package/dist/gedcom/validate.js +14 -4
- package/package.json +4 -1
package/assets/lang/cs.json
CHANGED
|
@@ -345,6 +345,8 @@
|
|
|
345
345
|
"sync.verify.done": "každý záznam dohledaný a přečtený nezávisle (snímek zaregistrovaný, strom read M… --blind): čtení se shodují → poznámka u pramene a údaje zůstávají; liší se → rozpor s oběma čteními",
|
|
346
346
|
"sync.reading.note": "Uživatel přečetl tento záznam v aplikaci Strom jinak ({source}); čtení výzkumu platí, dokud se záznam nepřečte znovu:",
|
|
347
347
|
"sync.reading.page": "strana",
|
|
348
|
+
"conflict.app.decided": "rozhodnutí uživatele v aplikaci Strom",
|
|
349
|
+
"conflict.app.noted": "{decided}: {note}",
|
|
348
350
|
"sync.reading.text": "přepis",
|
|
349
351
|
"archive.held": "odloženo: výzkum je archiv (data přicházejí z aplikace Strom); do fronty se vrátí, až se zapne práce s agentem",
|
|
350
352
|
"archive.removed": "odebráno v aplikaci Strom",
|
|
@@ -1066,6 +1068,9 @@
|
|
|
1066
1068
|
"ui.link.conflict.decided": "Tento rozpor už je rozhodnutý: {resolution}",
|
|
1067
1069
|
"ui.link.what.conflict.decide": "rozhodnout rozpor: {title}",
|
|
1068
1070
|
"ui.link.what.conflict.agent": "nechat rozpor na agentovi ({agent}): {title}",
|
|
1071
|
+
"ui.link.what.conflict.user": "rozhodnout rozpor: {title} — ponechat hodnotu z aplikace: {value}",
|
|
1072
|
+
"ui.link.what.conflict.research": "rozhodnout rozpor: {title} — převzít hodnotu výzkumu: {value}",
|
|
1073
|
+
"ui.link.conflict.empty": "(prázdné)",
|
|
1069
1074
|
"ui.link.conflict.pick": "Co platí?",
|
|
1070
1075
|
"ui.link.conflict.other": "Něco jiného (napíšu sám)",
|
|
1071
1076
|
"ui.link.conflict.own": "Co platí?",
|
|
@@ -1264,6 +1269,8 @@
|
|
|
1264
1269
|
"log.lesson.add": "Poznatek: {name}",
|
|
1265
1270
|
"log.hypothesis.add": "Hypotéza: {name}",
|
|
1266
1271
|
"log.hypothesis.argue": "Hypotéza zvážena: {name}",
|
|
1272
|
+
"log.hypothesis.link": "Co by verze hypotézy spojila: {name}",
|
|
1273
|
+
"log.hypothesis.variant": "Další možnost hypotézy: {name}",
|
|
1267
1274
|
"log.hypothesis.decide": "Hypotéza rozhodnuta: {name}",
|
|
1268
1275
|
"log.conflict.add": "Záznamy si odporují: {name}",
|
|
1269
1276
|
"log.conflict.resolve": "Rozpor vyřešen: {name}",
|
package/assets/lang/de.json
CHANGED
|
@@ -345,6 +345,8 @@
|
|
|
345
345
|
"sync.verify.done": "jeder Eintrag gefunden und unabhängig gelesen (Bild registriert, strom read M… --blind): die Lesungen stimmen überein → Notiz an der Quelle, die Angaben bleiben; sie weichen ab → ein Widerspruch mit beiden Lesungen",
|
|
346
346
|
"sync.reading.note": "Der Benutzer hat diesen Eintrag in der Strom-App anders gelesen ({source}); die Lesung der Forschung gilt, bis er erneut gelesen ist:",
|
|
347
347
|
"sync.reading.page": "Seite",
|
|
348
|
+
"conflict.app.decided": "Entscheidung des Nutzers in der Strom-App",
|
|
349
|
+
"conflict.app.noted": "{decided}: {note}",
|
|
348
350
|
"sync.reading.text": "Abschrift",
|
|
349
351
|
"archive.held": "zurückgestellt: die Forschung ist ein Archiv (die Daten kommen aus der Strom-App); zurück in der Warteschlange, wenn die Arbeit mit einem Agenten eingeschaltet wird",
|
|
350
352
|
"archive.removed": "in der Strom-App entfernt",
|
|
@@ -1066,6 +1068,9 @@
|
|
|
1066
1068
|
"ui.link.conflict.decided": "Dieser Widerspruch ist bereits entschieden: {resolution}",
|
|
1067
1069
|
"ui.link.what.conflict.decide": "den Widerspruch entscheiden: {title}",
|
|
1068
1070
|
"ui.link.what.conflict.agent": "den Widerspruch dem Agenten überlassen ({agent}): {title}",
|
|
1071
|
+
"ui.link.what.conflict.user": "den Widerspruch entscheiden: {title} — den Wert aus der App behalten: {value}",
|
|
1072
|
+
"ui.link.what.conflict.research": "den Widerspruch entscheiden: {title} — den Wert der Forschung übernehmen: {value}",
|
|
1073
|
+
"ui.link.conflict.empty": "(leer)",
|
|
1069
1074
|
"ui.link.conflict.pick": "Was stimmt?",
|
|
1070
1075
|
"ui.link.conflict.other": "Etwas anderes (ich schreibe es)",
|
|
1071
1076
|
"ui.link.conflict.own": "Was stimmt?",
|
|
@@ -1264,6 +1269,8 @@
|
|
|
1264
1269
|
"log.lesson.add": "Gelernt: {name}",
|
|
1265
1270
|
"log.hypothesis.add": "Eine Hypothese: {name}",
|
|
1266
1271
|
"log.hypothesis.argue": "Hypothese abgewogen: {name}",
|
|
1272
|
+
"log.hypothesis.link": "Was eine Variante der Hypothese verbinden würde: {name}",
|
|
1273
|
+
"log.hypothesis.variant": "Eine weitere Möglichkeit der Hypothese: {name}",
|
|
1267
1274
|
"log.hypothesis.decide": "Hypothese entschieden: {name}",
|
|
1268
1275
|
"log.conflict.add": "Die Einträge widersprechen sich: {name}",
|
|
1269
1276
|
"log.conflict.resolve": "Widerspruch geklärt: {name}",
|
package/assets/method/core.md
CHANGED
|
@@ -48,6 +48,19 @@ conclusion so the next researcher can follow it.
|
|
|
48
48
|
queue holds back every other task about people off the tree
|
|
49
49
|
(`strom task list --off-tree`); they come back by themselves once the link
|
|
50
50
|
is recorded.
|
|
51
|
+
- **Say what a hypothesis would connect.** A hypothesis about a connection —
|
|
52
|
+
whose child, the same person, a couple, siblings — gets each variant's links
|
|
53
|
+
when you record or argue it: `strom hypothesis link H… B --child P… --of F…`
|
|
54
|
+
(or `--parents P… [P…]`), `--same P… P…`, `--partners P… P…`,
|
|
55
|
+
`--siblings P… P…`. The Strom app shows them for a look and where the tree
|
|
56
|
+
ends; nothing is linked until the hypothesis is decided and the link
|
|
57
|
+
recorded. A variant whose people are not in the research has none: its
|
|
58
|
+
words only. An older open hypothesis you work on that has none gets them
|
|
59
|
+
then (`strom hypothesis list --unlinked`). A variant's letter stays its
|
|
60
|
+
claim's: a claim that would connect other people is a new variant
|
|
61
|
+
(`strom hypothesis variant H… "<claim>"`). Deciding one whose variants have
|
|
62
|
+
links names the variant it is decided for:
|
|
63
|
+
`strom hypothesis decide H… --variant B --decision "…"`.
|
|
51
64
|
- **Stay within the task.** New questions become new tasks (`strom task add`,
|
|
52
65
|
with where and done-when), not detours. A new task whose images are not here
|
|
53
66
|
gets them now: through the archive's connector (built first when it has
|
|
@@ -67,7 +67,10 @@ without --dry-run. `#name` labels what a line creates, `@name` uses it later.
|
|
|
67
67
|
person with a conflict only once something else proves it. What would decide
|
|
68
68
|
it, if not at hand, is a task about it: `strom task add … --about X…` (or H…).
|
|
69
69
|
`strom conflict resolve X… --resolution "…" --reasoning "…"` when the
|
|
70
|
-
evidence decides it.
|
|
70
|
+
evidence decides it. What the user decided (in the Strom app, or from its
|
|
71
|
+
link) stands: an agent decides it again — or opens a conflict of that fact
|
|
72
|
+
again — only on a source the decision did not weigh (`--source S…`) and with
|
|
73
|
+
`--reason "…"`; the same record read again changes nothing. A decided
|
|
71
74
|
hypothesis that a new record overturns is decided again (the earlier
|
|
72
75
|
decision is kept): `strom hypothesis decide H… --decision "…" --reason "…"`.
|
|
73
76
|
People named wrongly by the weaker record keep that name as a variant
|
package/dist/cli/menu-links.js
CHANGED
|
@@ -28,6 +28,7 @@ import { isValidLang } from "../core/lang.js";
|
|
|
28
28
|
import { agentsHere, suggestedWay, waysHere } from "../core/apps.js";
|
|
29
29
|
import { chooseWay } from "./ways.js";
|
|
30
30
|
import { claimText } from "../core/people.js";
|
|
31
|
+
import { sideOf, sideText, sidesOf } from "../core/conflicts.js";
|
|
31
32
|
import { PROFILES } from "../agents/profiles.js";
|
|
32
33
|
import { foldText, safeFolderName } from "../core/text.js";
|
|
33
34
|
import { StromError } from "../core/errors.js";
|
|
@@ -126,8 +127,11 @@ export function sessionsView(ctx, lang, root) {
|
|
|
126
127
|
if (all.length > shown.length)
|
|
127
128
|
out(ctx, ` ${t("ui.link.sessions.more", { n: all.length - shown.length })}`);
|
|
128
129
|
}
|
|
129
|
-
/**
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* A conflict the person decides: which claim holds (or their own words), and why. The side picked in the Strom app
|
|
132
|
+
* already (take): only confirmed, and why — that side taken (a conflict not decided by side: the claims offered).
|
|
133
|
+
*/
|
|
134
|
+
export async function decideConflict(ctx, run, lang, root, id, take) {
|
|
131
135
|
const t = translator(lang);
|
|
132
136
|
const tree = Tree.open(root, ctx.env);
|
|
133
137
|
const c = tree.get(id);
|
|
@@ -135,6 +139,22 @@ export async function decideConflict(ctx, run, lang, root, id) {
|
|
|
135
139
|
return stop(ctx, t("ui.link.conflict.none", { id }));
|
|
136
140
|
if (c.state === "resolved")
|
|
137
141
|
return stop(ctx, t("ui.link.conflict.decided", { resolution: c.resolution ?? "" }));
|
|
142
|
+
const sides = take ? sidesOf(c) : undefined;
|
|
143
|
+
if (take && sides) {
|
|
144
|
+
const value = sideText(tree, c, sides[take]) || t("ui.link.conflict.empty");
|
|
145
|
+
if (!(await asks(ctx, lang, root, t(take === "user" ? "ui.link.what.conflict.user" : "ui.link.what.conflict.research", { title: c.title, value }))))
|
|
146
|
+
return false;
|
|
147
|
+
let why = "";
|
|
148
|
+
while (!why) {
|
|
149
|
+
why = (await ctx.ask(t("ui.link.conflict.why"))).trim();
|
|
150
|
+
if (why === "0" || outOfAnswers(ctx))
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if ((await run(["conflict", "resolve", id, `--take=${take}`, `--reasoning=${t("ui.link.conflict.by", { why })}`], true)) !== 0)
|
|
154
|
+
return false;
|
|
155
|
+
out(ctx, t("ui.link.conflict.done"));
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
138
158
|
if (!(await asks(ctx, lang, root, t("ui.link.what.conflict.decide", { title: c.title }))))
|
|
139
159
|
return false;
|
|
140
160
|
const claim = (x) => `${claimText(tree, c, x)}${x.source ? ` — ${tree.get(x.source)?.title ?? x.source}` : ""}`;
|
|
@@ -153,7 +173,10 @@ export async function decideConflict(ctx, run, lang, root, id) {
|
|
|
153
173
|
if (why === "0" || outOfAnswers(ctx))
|
|
154
174
|
return false;
|
|
155
175
|
}
|
|
156
|
-
|
|
176
|
+
// a side of a conflict of the user's edit picked: that side taken — the user's value written into the fact (found: the
|
|
177
|
+
// value with its source named neither side, the conflict closed and nothing written)
|
|
178
|
+
const side = i < c.claims.length ? sideOf(c, c.claims[i]) : undefined;
|
|
179
|
+
if ((await run(["conflict", "resolve", id, `--resolution=${resolution}`, ...(side ? [`--take=${side}`] : []), `--reasoning=${t("ui.link.conflict.by", { why })}`], true)) !== 0)
|
|
157
180
|
return false;
|
|
158
181
|
out(ctx, t("ui.link.conflict.done"));
|
|
159
182
|
return true;
|
package/dist/cli/ui.js
CHANGED
|
@@ -982,6 +982,9 @@ export const UI = {
|
|
|
982
982
|
"ui.link.conflict.decided": "This conflict is decided already: {resolution}",
|
|
983
983
|
"ui.link.what.conflict.decide": "decide the conflict: {title}",
|
|
984
984
|
"ui.link.what.conflict.agent": "leave the conflict to the agent ({agent}): {title}",
|
|
985
|
+
"ui.link.what.conflict.user": "decide the conflict: {title} — keep the value from the app: {value}",
|
|
986
|
+
"ui.link.what.conflict.research": "decide the conflict: {title} — take the research's value: {value}",
|
|
987
|
+
"ui.link.conflict.empty": "(empty)",
|
|
985
988
|
"ui.link.conflict.pick": "Which is right?",
|
|
986
989
|
"ui.link.conflict.other": "Something else (I write it)",
|
|
987
990
|
"ui.link.conflict.own": "What is right?",
|
|
@@ -3,7 +3,7 @@ import { register } from "../cli/registry.js";
|
|
|
3
3
|
import { ui } from "../cli/ui.js";
|
|
4
4
|
import { lines, moreLine, paginate, table, truncate } from "../cli/format.js";
|
|
5
5
|
import { UsageError } from "../core/errors.js";
|
|
6
|
-
import { LESSON_MAX, LESSON_SCOPES, RECORD_TYPES, SEARCH_METHODS, SEARCH_RESULTS, } from "../core/model.js";
|
|
6
|
+
import { LESSON_MAX, LESSON_SCOPES, RECORD_TYPES, SEARCH_METHODS, SEARCH_RESULTS, VARIANT_LINK_KINDS, } from "../core/model.js";
|
|
7
7
|
import { create, csvOpt, listOpt, normId, requireRecord, update } from "../core/records.js";
|
|
8
8
|
import { ancestorGenerations, birthEvent, claimText, conflictTitle, deathEvent, displayName, familiesAsPartner, lifespan, parentsOf, primaryName, resolvePerson } from "../core/people.js";
|
|
9
9
|
import { changeLines } from "../core/changelog.js";
|
|
@@ -11,6 +11,7 @@ import { isAgent } from "../core/which.js";
|
|
|
11
11
|
import { foldText } from "../core/text.js";
|
|
12
12
|
import { makeNote } from "../core/actions.js";
|
|
13
13
|
import { takeSide } from "../core/sync.js";
|
|
14
|
+
import { againAllowed, claimOf, isEditConflict, weighedSources } from "../core/conflicts.js";
|
|
14
15
|
import { currentSession } from "../core/session.js";
|
|
15
16
|
import { typeOfId } from "../core/tree.js";
|
|
16
17
|
import { resolveResearch } from "./research.js";
|
|
@@ -81,6 +82,138 @@ function stillOpen(tree, id) {
|
|
|
81
82
|
const open = tasksAbout(tree, id).filter((t) => !["done", "dropped"].includes(t.state));
|
|
82
83
|
return open.length ? ui(tree.lang, "ui.conflict.tasks-open", { id, tasks: open.map((t) => t.id).join(", "), first: open[0].id }) : undefined;
|
|
83
84
|
}
|
|
85
|
+
// ── what a variant would connect (strom hypothesis link) ───────────────────
|
|
86
|
+
/** The next letter of a hypothesis's variants: after the last one used (A, B → C), never one before it. */
|
|
87
|
+
function nextLabel(labels) {
|
|
88
|
+
const letters = labels.filter((l) => /^[A-Z]$/.test(l)).map((l) => l.charCodeAt(0));
|
|
89
|
+
const next = letters.length ? Math.max(...letters) + 1 : 65;
|
|
90
|
+
if (next <= 90)
|
|
91
|
+
return String.fromCharCode(next);
|
|
92
|
+
for (let n = labels.length + 1;; n++)
|
|
93
|
+
if (!labels.includes(String(n)))
|
|
94
|
+
return String(n);
|
|
95
|
+
}
|
|
96
|
+
/** IDs given to a link: repeated, or several in one value (commas or spaces). */
|
|
97
|
+
function idsOpt(v) {
|
|
98
|
+
return listOpt(v).flatMap((x) => x.split(/[\s,]+/)).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
/** A person a link may name: there, not retracted, not merged. */
|
|
101
|
+
function linkPerson(tree, ref) {
|
|
102
|
+
const id = normId(ref, "person");
|
|
103
|
+
if (typeOfId(id) !== "person")
|
|
104
|
+
throw new UsageError(`"${ref}" is not a person ID: a link names people by ID (P0001)`, { hint: "strom person list" });
|
|
105
|
+
const p = tree.get(id);
|
|
106
|
+
if (!p)
|
|
107
|
+
throw new UsageError(`no person ${id}`, { hint: "strom person list", code: "record.none", params: { kind: "person", id } });
|
|
108
|
+
if (p.mergedInto)
|
|
109
|
+
throw new UsageError(`${id} was merged into ${p.mergedInto}`, { hint: `use ${p.mergedInto}`, code: "record.merged", params: { id, into: p.mergedInto } });
|
|
110
|
+
if (p.retracted)
|
|
111
|
+
throw new UsageError(`${id} is retracted`, { hint: `strom person show ${id}` });
|
|
112
|
+
return id;
|
|
113
|
+
}
|
|
114
|
+
function distinct(ids, what) {
|
|
115
|
+
const set = [...new Set(ids)];
|
|
116
|
+
if (set.length !== ids.length)
|
|
117
|
+
throw new UsageError(`${what}: the same person twice (${ids.join(" ")})`);
|
|
118
|
+
return set;
|
|
119
|
+
}
|
|
120
|
+
/** The family whose partners are exactly these people. */
|
|
121
|
+
function familyOf(tree, partners) {
|
|
122
|
+
return tree.list("family").find((f) => !f.retracted && f.partners.length === partners.length && partners.every((p) => f.partners.includes(p)));
|
|
123
|
+
}
|
|
124
|
+
/** The link the options name (undefined: none — --remove alone), checked against the tree. */
|
|
125
|
+
function variantLink(tree, opts, extra) {
|
|
126
|
+
const more = extra.flatMap((x) => x.split(/[\s,]+/)).filter(Boolean);
|
|
127
|
+
const kinds = VARIANT_LINK_KINDS.filter((k) => opts[k] !== undefined);
|
|
128
|
+
const how = "--child P… --of F… | --child P… --parents P… [P…] | --same P… P… | --partners P… P… | --siblings P… P… [P…]";
|
|
129
|
+
if (kinds.length > 1)
|
|
130
|
+
throw new UsageError(`one link at a time: ${kinds.map((k) => `--${k}`).join(", ")} given`, { hint: how });
|
|
131
|
+
const kind = kinds[0];
|
|
132
|
+
if (!kind) {
|
|
133
|
+
if (!opts.remove)
|
|
134
|
+
throw new UsageError("say what the variant would connect", { hint: how });
|
|
135
|
+
if (more.length || opts.of !== undefined || opts.parents !== undefined)
|
|
136
|
+
throw new UsageError("--of and --parents go with --child", { hint: how });
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (kind === "child") {
|
|
140
|
+
const children = idsOpt(opts.child);
|
|
141
|
+
if (children.length !== 1)
|
|
142
|
+
throw new UsageError("--child takes one person: the child", { hint: how });
|
|
143
|
+
const child = linkPerson(tree, children[0]);
|
|
144
|
+
const parents = [...idsOpt(opts.parents), ...more];
|
|
145
|
+
if (opts.of !== undefined && parents.length)
|
|
146
|
+
throw new UsageError("a family (--of) or parents (--parents), not both", { hint: how });
|
|
147
|
+
if (opts.of !== undefined) {
|
|
148
|
+
const famId = normId(String(opts.of), "family");
|
|
149
|
+
if (typeOfId(famId) !== "family")
|
|
150
|
+
throw new UsageError(`"${String(opts.of)}" is not a family ID (F0001)`, { hint: "strom family list" });
|
|
151
|
+
const f = tree.get(famId);
|
|
152
|
+
if (!f)
|
|
153
|
+
throw new UsageError(`no family ${famId}`, { hint: "strom family list", code: "record.none", params: { kind: "family", id: famId } });
|
|
154
|
+
if (f.mergedInto)
|
|
155
|
+
throw new UsageError(`${famId} was merged into ${f.mergedInto}`, { hint: `use ${f.mergedInto}`, code: "record.merged", params: { id: famId, into: f.mergedInto } });
|
|
156
|
+
if (f.retracted)
|
|
157
|
+
throw new UsageError(`${famId} is retracted`);
|
|
158
|
+
if (f.children.some((c) => c.person === child))
|
|
159
|
+
throw new UsageError(`${child} is a child of ${famId} already: nothing uncertain to show`, { hint: "decide the hypothesis: strom hypothesis decide H… --decision \"…\"" });
|
|
160
|
+
if (f.partners.includes(child))
|
|
161
|
+
throw new UsageError(`${child} is a partner of ${famId}`);
|
|
162
|
+
return { kind, person: child, family: famId };
|
|
163
|
+
}
|
|
164
|
+
if (!parents.length)
|
|
165
|
+
throw new UsageError("whose child: --of F… (a family of the tree) or --parents P… [P…]", { hint: how });
|
|
166
|
+
if (parents.length > 2)
|
|
167
|
+
throw new UsageError("one or two parents", { hint: how });
|
|
168
|
+
const ps = distinct(parents.map((p) => linkPerson(tree, p)), "--parents");
|
|
169
|
+
if (ps.includes(child))
|
|
170
|
+
throw new UsageError(`${child} cannot be their own parent`);
|
|
171
|
+
const fam = familyOf(tree, ps);
|
|
172
|
+
if (fam?.children.some((c) => c.person === child))
|
|
173
|
+
throw new UsageError(`${child} is a child of ${fam.id} already: nothing uncertain to show`);
|
|
174
|
+
if (fam)
|
|
175
|
+
throw new UsageError(`${ps.join(" and ")} are a family of the tree: ${fam.id}`, { hint: `strom hypothesis link H… <variant> --child ${child} --of ${fam.id}` });
|
|
176
|
+
return { kind, person: child, parents: ps };
|
|
177
|
+
}
|
|
178
|
+
if (opts.of !== undefined || opts.parents !== undefined)
|
|
179
|
+
throw new UsageError("--of and --parents go with --child", { hint: how });
|
|
180
|
+
const persons = distinct([...idsOpt(opts[kind]), ...more].map((p) => linkPerson(tree, p)), `--${kind}`);
|
|
181
|
+
if (kind === "siblings" ? persons.length < 2 : persons.length !== 2)
|
|
182
|
+
throw new UsageError(kind === "siblings" ? "--siblings takes two or more people" : `--${kind} takes two people`, { hint: `--${kind} P… P…${kind === "siblings" ? " [P…]" : ""}` });
|
|
183
|
+
const together = kind === "partners" ? familyOf(tree, persons) : undefined;
|
|
184
|
+
if (together)
|
|
185
|
+
throw new UsageError(`${persons.join(" and ")} are partners of ${together.id} already: nothing uncertain to show`);
|
|
186
|
+
return { kind, persons };
|
|
187
|
+
}
|
|
188
|
+
/** One link as a key: the same link given again is the same, whatever the order of its people. */
|
|
189
|
+
function linkKey(l) {
|
|
190
|
+
return l.kind === "child" ? `child ${l.person} ${l.family ?? [...(l.parents ?? [])].sort().join("+")}` : `${l.kind} ${[...l.persons].sort().join("+")}`;
|
|
191
|
+
}
|
|
192
|
+
/** One link in a few words: "child P0006 of F0001", "same P0005 = P0003". */
|
|
193
|
+
export function linkText(l) {
|
|
194
|
+
if (l.kind === "child")
|
|
195
|
+
return `child ${l.person} of ${l.family ?? (l.parents ?? []).join(" + ")}`;
|
|
196
|
+
return `${l.kind} ${l.persons.join(l.kind === "same" ? " = " : l.kind === "partners" ? " + " : ", ")}`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* What a new link would say that the variant did not (its letter is one claim's: the Strom app remembers "H0022,
|
|
200
|
+
* variant B"): links again after all were taken off, naming anyone they did not; a child it named, given other
|
|
201
|
+
* parents. Adding to what it says is fine.
|
|
202
|
+
*/
|
|
203
|
+
function otherPeople(v, l) {
|
|
204
|
+
const before = new Set(v.linked ?? []);
|
|
205
|
+
if (!before.size)
|
|
206
|
+
return [];
|
|
207
|
+
const ids = linkIds(l);
|
|
208
|
+
if (!v.links?.length)
|
|
209
|
+
return ids.filter((x) => !before.has(x));
|
|
210
|
+
if (l.kind === "child" && before.has(l.person))
|
|
211
|
+
return ids.filter((x) => x !== l.person && !before.has(x));
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
function linkIds(l) {
|
|
215
|
+
return l.kind === "child" ? [l.person, ...(l.family ? [l.family] : []), ...(l.parents ?? [])] : l.persons;
|
|
216
|
+
}
|
|
84
217
|
// ── searches ───────────────────────────────────────────────────────────────
|
|
85
218
|
function yearsRange(y) {
|
|
86
219
|
const m = y ? /^(\d{3,4})(?:-(\d{3,4}))?$/.exec(y) : null;
|
|
@@ -320,6 +453,21 @@ register({
|
|
|
320
453
|
const fact = typeof opts.fact === "string" ? opts.fact.trim().toUpperCase() : undefined;
|
|
321
454
|
if (fact !== undefined && !/^(?:[A-Z]{3,4}|_[A-Z]{2,6})$/.test(fact))
|
|
322
455
|
throw new UsageError(`--fact is a GEDCOM tag (BIRT, DEAT, NAME, SEX…), not "${opts.fact}"`);
|
|
456
|
+
// the user decided this fact (in the Strom app, at a terminal): an agent opens it again only on a source that
|
|
457
|
+
// decision did not weigh, with a reason — never because it reads the same record again
|
|
458
|
+
if (fact && tree.actor !== "user") {
|
|
459
|
+
const reason = typeof opts.reason === "string" && opts.reason.trim() ? opts.reason.trim() : undefined;
|
|
460
|
+
for (const d of tree.list("conflict").filter((c) => c.state === "resolved" && c.decidedBy === "user" && c.fact === fact && c.subject.some((s) => subject.includes(s)))) {
|
|
461
|
+
const weighed = weighedSources(tree, d);
|
|
462
|
+
const fresh = claims.map((c) => c.source).filter((s) => !!s && !weighed.has(s));
|
|
463
|
+
if (!fresh.length || !reason)
|
|
464
|
+
throw new UsageError(`the user decided ${fact} of ${d.subject.join(", ")} in ${d.id}: "${truncate(d.resolution ?? "", 80)}" — an agent opens it again only on a source that decision did not weigh, with --reason`, {
|
|
465
|
+
hint: `strom conflict show ${d.id}; a new record: strom conflict add … --claim "S…: …" --reason "<what it shows>" — or ask the user`,
|
|
466
|
+
code: "conflict.user-decided",
|
|
467
|
+
params: { id: d.id, resolution: d.resolution ?? "" },
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
323
471
|
const x = create(tree, "conflict", { title: args[0].trim(), ...(fact ? { fact } : {}), subject, claims, state: "open", note: opts.note }, (id) => `+${id} conflict "${truncate(args[0], 60)}"`, subject);
|
|
324
472
|
return { text: written(tree), data: { conflict: x } };
|
|
325
473
|
},
|
|
@@ -329,11 +477,15 @@ register({
|
|
|
329
477
|
group: "analysis",
|
|
330
478
|
tree: true,
|
|
331
479
|
writes: true,
|
|
480
|
+
description: "A decided conflict is decided again only with --reason (the earlier decision is kept in a note). One the user decided " +
|
|
481
|
+
"(in the Strom app, or at a terminal) stands: an agent decides it again only on a source the decision did not weigh " +
|
|
482
|
+
"(--source S…) and with --reason; a side once taken is not taken again — change the facts with the fact commands.",
|
|
332
483
|
args: [{ name: "conflict", description: "conflict ID (X0001)", required: true }],
|
|
333
484
|
options: [
|
|
334
485
|
{ name: "resolution", type: "string", value: "<text>", description: "the conclusion (with --take: the value taken, by default)" },
|
|
335
486
|
{ name: "reasoning", type: "string", value: "<text>", description: "why — which evidence outweighs which" },
|
|
336
487
|
{ name: "take", type: "string", value: "<user|research>", description: "a conflict of the user's edit in the Strom app: whose value the fact keeps — the user's is written into it" },
|
|
488
|
+
{ name: "source", type: "string", multiple: true, value: "<S…>", description: "deciding again what the user decided: the new source it rests on (one none of its claims names)" },
|
|
337
489
|
],
|
|
338
490
|
run(ctx, { args, opts }) {
|
|
339
491
|
const tree = ctx.tree();
|
|
@@ -343,14 +495,16 @@ register({
|
|
|
343
495
|
if (take !== undefined && take !== "user" && take !== "research")
|
|
344
496
|
throw new UsageError(`--take is user or research, not "${opts.take}"`, { code: "conflict.take", params: { take: String(opts.take) } });
|
|
345
497
|
// a conflict of the user's edit in the Strom app: a fact's, a child's parents, a name's or a sex's
|
|
346
|
-
const ofEdit =
|
|
498
|
+
const ofEdit = isEditConflict(c0);
|
|
347
499
|
if (take && !ofEdit)
|
|
348
500
|
throw new UsageError(`${id} is no conflict of an edit in the Strom app: write what you conclude with the commands that change facts, then resolve it with --resolution`, { hint: `strom conflict show ${id}`, code: "conflict.no-edit", params: { id } });
|
|
349
501
|
// the conclusion naming one side's value exactly: that side taken (found on Mac: "tesař" resolved, the fact left
|
|
350
502
|
// "kovář", the app then showing kovář in silence)
|
|
351
|
-
const user = c0
|
|
352
|
-
const research = c0
|
|
353
|
-
|
|
503
|
+
const user = claimOf(c0, "user");
|
|
504
|
+
const research = claimOf(c0, "research");
|
|
505
|
+
// …with the source it rests on after it, as the decide link of the Strom app writes it: "4 MAR 1885 (S0004)"
|
|
506
|
+
// (found: the user's value picked there closed the conflict and wrote nothing)
|
|
507
|
+
const said = opts.resolution ? foldText(String(opts.resolution).replace(/\s*\((?:[Ss]\d+(?:\s*,\s*[Ss]\d+)*)\)\s*$/u, "")).trim() : undefined;
|
|
354
508
|
// (as strom compares it, or in words as the person reads it)
|
|
355
509
|
const names = (c) => (c ? [c.value, claimText(tree, c0, c)].map((v) => foldText(v).trim()) : []);
|
|
356
510
|
const side = take ?? (ofEdit && said ? (names(user).includes(said) ? "user" : names(research).includes(said) ? "research" : undefined) : undefined);
|
|
@@ -363,13 +517,31 @@ register({
|
|
|
363
517
|
throw new UsageError("--reasoning is required", { hint: `strom conflict resolve ${id} … --reasoning "<why>"`, code: "conflict.needs-reasoning", params: { id } });
|
|
364
518
|
if (!resolution)
|
|
365
519
|
throw new UsageError("--resolution is required", { ...(ofEdit ? { hint: `or take a side: strom conflict resolve ${id} --take user|research` } : {}), code: "conflict.needs-resolution", params: { id } });
|
|
520
|
+
const reason = typeof opts.reason === "string" && opts.reason.trim() ? opts.reason.trim() : undefined;
|
|
521
|
+
const sources = listOpt(opts.source).map((s) => requireRecord(tree, s, "source").id);
|
|
522
|
+
// who decides: the user (in the Strom app through the bridge, or at a terminal), else an agent
|
|
523
|
+
const by = tree.actor === "user" ? "user" : "agent";
|
|
524
|
+
const inApp = ctx.env.STROM_FOR_APP === "1";
|
|
366
525
|
let did;
|
|
526
|
+
let again;
|
|
367
527
|
const x = tree.withTreeLock(() => {
|
|
528
|
+
// read afresh under the lock: decided meanwhile (another strom, the app) — never decided over in silence
|
|
529
|
+
const now = tree.get(id);
|
|
530
|
+
if (now.state === "resolved") {
|
|
531
|
+
againAllowed(tree, now, { reason, sources, by, side });
|
|
532
|
+
again = now;
|
|
533
|
+
}
|
|
368
534
|
if (side)
|
|
369
|
-
did = takeSide(tree,
|
|
370
|
-
|
|
535
|
+
did = takeSide(tree, now, side, String(opts.reasoning));
|
|
536
|
+
const earlier = again ? [makeNote(tree, truncate(`earlier decided${again.decidedBy ? ` by the ${again.decidedBy}` : ""}${again.taken ? ` (the ${again.taken === "user" ? "user's" : "research's"} value)` : ""}: ${again.resolution ?? ""} — ${again.reasoning ?? ""}`, 480))] : [];
|
|
537
|
+
const newer = again && sources.length ? [makeNote(tree, truncate(`decided again on ${sources.join(", ")}: ${reason}`, 480))] : [];
|
|
538
|
+
return update(tree, id, "conflict", (c) => {
|
|
539
|
+
const { taken: _t, decidedIn: _i, ...rest } = c;
|
|
540
|
+
return { ...rest, state: "resolved", resolution, reasoning: String(opts.reasoning), decidedBy: by, ...(inApp ? { decidedIn: "app" } : {}), decidedAt: new Date().toISOString(), ...(side ? { taken: side } : {}), notes: [...c.notes, ...earlier, ...newer] };
|
|
541
|
+
}, {
|
|
371
542
|
op: "conflict.resolve",
|
|
372
|
-
summary: `${id} resolved${side ? ` — the ${side === "user" ? "user's edit" : "research's"} taken` : ""}`,
|
|
543
|
+
summary: `${id} ${again ? "decided again" : "resolved"}${side ? ` — the ${side === "user" ? "user's edit" : "research's"} taken` : ""}${inApp ? " in the Strom app" : ""}`,
|
|
544
|
+
...(reason ? { reason } : {}),
|
|
373
545
|
});
|
|
374
546
|
});
|
|
375
547
|
// what was saved, in the research's language — the user's decision, whoever typed it (found on Mac: "X0001
|
|
@@ -465,6 +637,124 @@ register({
|
|
|
465
637
|
}, { op: "hypothesis.argue", summary: `${id} ${args[1]} ${opts.for ? "+for" : "+against"}` });
|
|
466
638
|
return { text: written(tree), data: { hypothesis: h } };
|
|
467
639
|
},
|
|
640
|
+
}, {
|
|
641
|
+
path: ["hypothesis", "variant"],
|
|
642
|
+
summary: "Add a variant to an open hypothesis — a new letter, never one used before",
|
|
643
|
+
group: "analysis",
|
|
644
|
+
tree: true,
|
|
645
|
+
writes: true,
|
|
646
|
+
description: "A variant's letter stays one claim's (the Strom app remembers it): a claim that changes whom it would connect is a new variant.",
|
|
647
|
+
args: [
|
|
648
|
+
{ name: "hypothesis", description: "hypothesis ID (H0001)", required: true },
|
|
649
|
+
{ name: "claim", description: 'the claim, e.g. "the son of Josef from No. 12" (or "C: …" with its letter)', required: true },
|
|
650
|
+
],
|
|
651
|
+
examples: ['strom hypothesis variant H0001 "syn Josefa z čp. 12"'],
|
|
652
|
+
run(ctx, { args }) {
|
|
653
|
+
const tree = ctx.tree();
|
|
654
|
+
const id = normId(args[0], "hypothesis");
|
|
655
|
+
const m = /^([\p{L}\p{N}]{1,3})\s*:\s*(.+)$/u.exec(args[1].trim());
|
|
656
|
+
const claim = (m ? m[2] : args[1]).trim();
|
|
657
|
+
if (!claim)
|
|
658
|
+
throw new UsageError("the claim is empty");
|
|
659
|
+
// (a writing command holds the tree's lock from its first read: the letter picked here is the one written)
|
|
660
|
+
const now = requireRecord(tree, id, "hypothesis");
|
|
661
|
+
if (now.state !== "open")
|
|
662
|
+
throw new UsageError(`${id} is ${now.state}: a variant is added to an open hypothesis`, { hint: `strom hypothesis show ${id}` });
|
|
663
|
+
if (m && now.variants.some((v) => foldText(v.label) === foldText(m[1])))
|
|
664
|
+
throw new UsageError(`${id} has a variant ${m[1]} already: a letter is never used twice`, { hint: "leave the letter out: strom picks the next one" });
|
|
665
|
+
const label = m ? m[1] : nextLabel(now.variants.map((v) => v.label));
|
|
666
|
+
const h = update(tree, id, "hypothesis", (h) => ({ ...h, variants: [...h.variants, { label, claim, support: [], against: [] }] }), {
|
|
667
|
+
op: "hypothesis.variant",
|
|
668
|
+
summary: `${id} +${label}: ${truncate(claim, 60)}`,
|
|
669
|
+
});
|
|
670
|
+
return { text: written(tree), data: { hypothesis: h, variant: label } };
|
|
671
|
+
},
|
|
672
|
+
}, {
|
|
673
|
+
path: ["hypothesis", "link"],
|
|
674
|
+
summary: "Say what a variant would connect — whose child, the same person, a couple, siblings — never a link of the tree",
|
|
675
|
+
group: "analysis",
|
|
676
|
+
tree: true,
|
|
677
|
+
writes: true,
|
|
678
|
+
description: [
|
|
679
|
+
"A variant about a connection names its people by ID: the Strom app shows what it would connect (drawn apart, only",
|
|
680
|
+
"for a look) and where the tree ends, the parents named but not linked. Nothing is linked in the tree until the",
|
|
681
|
+
"hypothesis is decided and the link recorded (family add / family child). One link per call; a variant may have several.",
|
|
682
|
+
" --child P… --of F… the child of a family of the tree",
|
|
683
|
+
" --child P… --parents P… [P…] the child of one or two parents who have no family together",
|
|
684
|
+
" --same P… P… two records of one person",
|
|
685
|
+
" --partners P… P… a couple",
|
|
686
|
+
" --siblings P… P… [P…] siblings, their parents unknown",
|
|
687
|
+
"--remove takes that link off again; --remove alone, every link of the variant.",
|
|
688
|
+
"A variant's letter stays its claim's (the Strom app remembers it): links may be added to it, never links naming",
|
|
689
|
+
"other people once it had some — that is a new variant (strom hypothesis variant). Deciding: decide --variant <label>.",
|
|
690
|
+
].join("\n"),
|
|
691
|
+
args: [
|
|
692
|
+
{ name: "hypothesis", description: "hypothesis ID (H0001)", required: true },
|
|
693
|
+
{ name: "variant", description: "variant label, e.g. B", required: true },
|
|
694
|
+
{ name: "people", description: "the second (and further) person of --same, --partners, --siblings or --parents", variadic: true },
|
|
695
|
+
],
|
|
696
|
+
options: [
|
|
697
|
+
{ name: "child", type: "string", value: "<P…>", description: "the child whose parents the variant names" },
|
|
698
|
+
{ name: "of", type: "string", value: "<F…>", description: "…the family of the tree it would be a child of" },
|
|
699
|
+
{ name: "parents", type: "string", multiple: true, value: "<P…>", description: "…or its one or two parents (no family of theirs)" },
|
|
700
|
+
{ name: "same", type: "string", multiple: true, value: "<P…>", description: "two records of one person" },
|
|
701
|
+
{ name: "partners", type: "string", multiple: true, value: "<P…>", description: "a couple" },
|
|
702
|
+
{ name: "siblings", type: "string", multiple: true, value: "<P…>", description: "two or more siblings" },
|
|
703
|
+
{ name: "remove", type: "boolean", description: "take the link off (alone: every link of the variant)" },
|
|
704
|
+
],
|
|
705
|
+
examples: [
|
|
706
|
+
"strom hypothesis link H0001 B --child P0006 --of F0001",
|
|
707
|
+
"strom hypothesis link H0001 B --child P0006 --parents P0002 P0003",
|
|
708
|
+
"strom hypothesis link H0001 B --same P0005 P0003",
|
|
709
|
+
"strom hypothesis link H0001 A --remove",
|
|
710
|
+
],
|
|
711
|
+
run(ctx, { args, opts }) {
|
|
712
|
+
const tree = ctx.tree();
|
|
713
|
+
const id = normId(args[0], "hypothesis");
|
|
714
|
+
const link = variantLink(tree, opts, args.slice(2));
|
|
715
|
+
const said = link ? linkText(link) : "every link";
|
|
716
|
+
const h = update(tree, id, "hypothesis", (h) => {
|
|
717
|
+
if (h.state !== "open")
|
|
718
|
+
throw new UsageError(`${id} is ${h.state}: what a variant would connect is for an open hypothesis`, { hint: `strom hypothesis show ${id}` });
|
|
719
|
+
const v = h.variants.find((x) => foldText(x.label) === foldText(args[1]));
|
|
720
|
+
if (!v)
|
|
721
|
+
throw new UsageError(`no variant ${args[1]} in ${id}`, { hint: h.variants.map((x) => x.label).join(", ") });
|
|
722
|
+
const had = v.links ?? [];
|
|
723
|
+
const at = link ? had.findIndex((l) => linkKey(l) === linkKey(link)) : -1;
|
|
724
|
+
if (opts.remove) {
|
|
725
|
+
if (!had.length)
|
|
726
|
+
throw new UsageError(`${id} ${v.label} has no links`, { hint: `strom hypothesis show ${id}` });
|
|
727
|
+
if (link && at < 0)
|
|
728
|
+
throw new UsageError(`${id} ${v.label} has no link ${linkText(link)}`, { hint: had.map(linkText).join(" · ") });
|
|
729
|
+
const rest = link ? had.filter((_, i) => i !== at) : [];
|
|
730
|
+
if (rest.length)
|
|
731
|
+
v.links = rest;
|
|
732
|
+
else
|
|
733
|
+
delete v.links;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
if (at >= 0)
|
|
737
|
+
throw new UsageError(`${id} ${v.label} has that link already: ${linkText(link)}`, { hint: `strom hypothesis show ${id}` });
|
|
738
|
+
const other = otherPeople(v, link);
|
|
739
|
+
if (other.length)
|
|
740
|
+
throw new UsageError(`${id} ${v.label} said other people before (${(v.linked ?? []).join(" ")}): a variant's letter stays one claim's — ${other.join(" ")} is a new variant`, {
|
|
741
|
+
hint: `strom hypothesis variant ${id} "<the claim>", then strom hypothesis link ${id} <its letter> …`,
|
|
742
|
+
});
|
|
743
|
+
v.links = [...had, link];
|
|
744
|
+
v.linked = [...new Set([...(v.linked ?? []), ...linkIds(link)])];
|
|
745
|
+
}
|
|
746
|
+
return h;
|
|
747
|
+
}, {
|
|
748
|
+
op: "hypothesis.link",
|
|
749
|
+
summary: `${id} ${args[1]}: ${opts.remove ? `${said} taken off` : said}`,
|
|
750
|
+
targets: link ? linkIds(link) : [],
|
|
751
|
+
});
|
|
752
|
+
const v = h.variants.find((x) => foldText(x.label) === foldText(args[1]));
|
|
753
|
+
return {
|
|
754
|
+
text: lines(written(tree), ...(v.links ?? []).map((l) => ` ${v.label} ⇢ ${linkText(l)}`)),
|
|
755
|
+
data: { hypothesis: h, variant: v.label, links: v.links ?? [] },
|
|
756
|
+
};
|
|
757
|
+
},
|
|
468
758
|
}, {
|
|
469
759
|
path: ["hypothesis", "decide"],
|
|
470
760
|
summary: "Decide a hypothesis (or abandon it) — again, with a reason, when a new record overturns the decision",
|
|
@@ -475,6 +765,7 @@ register({
|
|
|
475
765
|
args: [{ name: "hypothesis", description: "hypothesis ID", required: true }],
|
|
476
766
|
options: [
|
|
477
767
|
{ name: "decision", type: "string", value: "<text>", description: "which variant and why" },
|
|
768
|
+
{ name: "variant", type: "string", value: "<label>", description: "the variant it is decided for (always when its variants have links)" },
|
|
478
769
|
{ name: "abandon", type: "boolean", description: "none of the variants can be decided" },
|
|
479
770
|
],
|
|
480
771
|
run(ctx, { args, opts }) {
|
|
@@ -483,14 +774,20 @@ register({
|
|
|
483
774
|
throw new UsageError("--decision is required");
|
|
484
775
|
const id = normId(args[0], "hypothesis");
|
|
485
776
|
const reason = typeof opts.reason === "string" && opts.reason.trim() ? opts.reason.trim() : undefined;
|
|
777
|
+
if (opts.variant !== undefined && opts.abandon)
|
|
778
|
+
throw new UsageError("--variant names the variant it is decided for: not with --abandon");
|
|
486
779
|
const h = update(tree, id, "hypothesis", (h) => {
|
|
487
780
|
// A decision is overturned in the open: the earlier one stays readable.
|
|
488
781
|
const earlier = h.state !== "open" && h.decision ? h.decision : undefined;
|
|
489
782
|
if (earlier && !reason)
|
|
490
783
|
throw new UsageError(`${id} is already ${h.state}: "${truncate(earlier, 80)}"`, { hint: 'deciding again needs --reason (e.g. "the marriage entry of 1839 overturns it")' });
|
|
491
784
|
const notes = earlier ? [...h.notes, makeNote(tree, truncate(`earlier ${h.state}: ${earlier}`, 480))] : h.notes;
|
|
492
|
-
|
|
493
|
-
|
|
785
|
+
const chosen = opts.variant === undefined ? undefined : h.variants.find((x) => foldText(x.label) === foldText(String(opts.variant)));
|
|
786
|
+
if (opts.variant !== undefined && !chosen)
|
|
787
|
+
throw new UsageError(`no variant ${String(opts.variant)} in ${id}`, { hint: h.variants.map((x) => x.label).join(", ") });
|
|
788
|
+
const { chosen: _was, ...rest } = h;
|
|
789
|
+
return { ...rest, state: opts.abandon ? "abandoned" : "decided", decision: String(opts.decision), ...(chosen ? { chosen: chosen.label } : {}), notes };
|
|
790
|
+
}, { op: "hypothesis.decide", summary: `${id} ${opts.abandon ? "abandoned" : "decided"}${opts.variant !== undefined ? ` for ${String(opts.variant)}` : ""}${reason ? " again" : ""}`, reason });
|
|
494
791
|
// decided: the sources it names that its people's facts do not cite yet — what they say goes on the people now
|
|
495
792
|
const unrecorded = opts.abandon ? [] : sourcesNotOnPeople(tree, h);
|
|
496
793
|
const say = unrecorded.map((u) => `${u.person}: ${u.sources.join(", ")} — named by ${id}, cited by none of ${u.person}'s facts: what they say of ${u.person} goes on ${u.person} now (event add ${u.person} … --cite ${u.sources[0]}); a note or the diary does not reach the tree`);
|
|
@@ -501,11 +798,24 @@ register({
|
|
|
501
798
|
summary: "Open hypotheses",
|
|
502
799
|
group: "analysis",
|
|
503
800
|
tree: true,
|
|
504
|
-
options: [
|
|
801
|
+
options: [
|
|
802
|
+
{ name: "all", type: "boolean", description: "include decided and abandoned" },
|
|
803
|
+
{ name: "unlinked", type: "boolean", description: "only those no variant of which says what it would connect (strom hypothesis link)" },
|
|
804
|
+
],
|
|
805
|
+
examples: ["strom hypothesis list", "strom hypothesis list --unlinked"],
|
|
505
806
|
run(ctx, { opts }) {
|
|
506
|
-
const
|
|
807
|
+
const linked = (h) => h.variants.some((v) => v.links?.length);
|
|
808
|
+
const all = ctx
|
|
809
|
+
.tree()
|
|
810
|
+
.list("hypothesis")
|
|
811
|
+
.filter((h) => (opts.all || h.state === "open") && !(opts.unlinked && linked(h)));
|
|
812
|
+
const links = (h) => h.variants.reduce((n, v) => n + (v.links?.length ?? 0), 0);
|
|
507
813
|
return {
|
|
508
|
-
text: all.length
|
|
814
|
+
text: all.length
|
|
815
|
+
? table(all.map((h) => [h.id, h.state, truncate(h.question, 60), h.variants.map((v) => v.label).join("/"), links(h) ? `${links(h)} link${links(h) === 1 ? "" : "s"}` : ""]))
|
|
816
|
+
: opts.unlinked
|
|
817
|
+
? "no open hypothesis without links"
|
|
818
|
+
: "no open hypotheses",
|
|
509
819
|
data: { hypotheses: all },
|
|
510
820
|
};
|
|
511
821
|
},
|
|
@@ -519,7 +829,7 @@ register({
|
|
|
519
829
|
const tree = ctx.tree();
|
|
520
830
|
const h = requireRecord(tree, args[0], "hypothesis");
|
|
521
831
|
return {
|
|
522
|
-
text: lines(`${h.id} ${h.question} [${h.state}]`, `about ${h.subject.join(" ")}`, taskLines(tree, h.id), ...h.variants.flatMap((v) => [`\n${v.label}: ${v.claim}`, ...v.support.map((s) => ` + ${s}`), ...v.against.map((s) => ` − ${s}`)]), h.decision ? `\ndecision ${h.decision}` : undefined),
|
|
832
|
+
text: lines(`${h.id} ${h.question} [${h.state}]`, `about ${h.subject.join(" ")}`, taskLines(tree, h.id), ...h.variants.flatMap((v) => [`\n${v.label}: ${v.claim}`, ...v.support.map((s) => ` + ${s}`), ...v.against.map((s) => ` − ${s}`), ...(v.links ?? []).map((l) => ` ⇢ ${linkText(l)}`)]), h.decision ? `\ndecision${h.chosen ? ` (for ${h.chosen})` : ""} ${h.decision}` : undefined),
|
|
523
833
|
data: { hypothesis: h },
|
|
524
834
|
};
|
|
525
835
|
},
|
package/dist/commands/links.js
CHANGED
|
@@ -160,7 +160,7 @@ async function inTerminal(ctx, link) {
|
|
|
160
160
|
case "conflict": {
|
|
161
161
|
if (link.do === "decide") {
|
|
162
162
|
const { decideConflict } = await import("../cli/menu-links.js");
|
|
163
|
-
return end(await decideConflict(ctx, run, treeLang, root, link.id));
|
|
163
|
+
return end(await decideConflict(ctx, run, treeLang, root, link.id, link.take));
|
|
164
164
|
}
|
|
165
165
|
// left to the agent: it costs — said and asked first, then the conversation about it
|
|
166
166
|
const say = aboutConflict(root, ctx, link.id);
|
|
@@ -323,7 +323,8 @@ register({
|
|
|
323
323
|
"strom-research://review?tree=<id>&person=P0012&scope=person|family|line — the person reviewed (strom review).\n" +
|
|
324
324
|
"strom-research://research?tree=<id>&person=P0012&direction=ancestors|descendants — a new direction (research new).\n" +
|
|
325
325
|
"strom-research://task?…&task=T0007&do=park|drop|wake — the task put aside, given up, back in the queue (the reason asked).\n" +
|
|
326
|
-
"strom-research://conflict?tree=<id>&id=X0007&do=decide|agent — decided by the user (which claim, why) or left to the agent
|
|
326
|
+
"strom-research://conflict?tree=<id>&id=X0007&do=decide|agent — decided by the user (which claim, why) or left to the agent;\n" +
|
|
327
|
+
" &take=user|research — the side picked in the app: only confirmed, and why.\n" +
|
|
327
328
|
"strom-research://story?tree=<id>&person=P0012[&partner=P0013]&do=final|keep — the story approved (strom story approve), of a couple with partner=.\n" +
|
|
328
329
|
"strom-research://sync-undo?tree=<id>&intake=I0042 — a sending from the app taken back (strom sync undo).\n" +
|
|
329
330
|
"strom-research://sync?tree=<id>&do=show[&intake=R…] — a tree the app sent that waits: shown, then written or thrown away.\n" +
|