baesian 0.0.0-stage → 1.0.1

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.
Files changed (69) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +217 -2
  3. package/dist/agents.js +249 -0
  4. package/dist/api.js +13 -0
  5. package/dist/bin.js +14 -0
  6. package/dist/ci.js +82 -0
  7. package/dist/cite.js +66 -0
  8. package/dist/cli.js +831 -0
  9. package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
  10. package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
  11. package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
  12. package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
  13. package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
  14. package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
  15. package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
  16. package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
  17. package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
  18. package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
  19. package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
  20. package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
  21. package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
  22. package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
  23. package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
  24. package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
  25. package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
  26. package/dist/client/assets/index-9yf70Vqv.css +1 -0
  27. package/dist/client/assets/index-Dwg9e0YR.js +15 -0
  28. package/dist/client/index.html +15 -0
  29. package/dist/facts.js +278 -0
  30. package/dist/folder-storage.js +71 -0
  31. package/dist/frontmatter.js +38 -0
  32. package/dist/gate-paths.js +77 -0
  33. package/dist/gate.js +237 -0
  34. package/dist/git.js +100 -0
  35. package/dist/globs.js +120 -0
  36. package/dist/hookblock.js +146 -0
  37. package/dist/hooks.js +911 -0
  38. package/dist/ids.js +23 -0
  39. package/dist/login.js +301 -0
  40. package/dist/mcp.js +303 -0
  41. package/dist/merges.js +849 -0
  42. package/dist/modes.js +245 -0
  43. package/dist/notify.js +91 -0
  44. package/dist/ops.js +381 -0
  45. package/dist/passkey.js +86 -0
  46. package/dist/personal.js +163 -0
  47. package/dist/practice-answers.js +42 -0
  48. package/dist/proposals.js +197 -0
  49. package/dist/quote.js +28 -0
  50. package/dist/report.js +261 -0
  51. package/dist/routing.js +411 -0
  52. package/dist/schema.js +1178 -0
  53. package/dist/server.js +514 -0
  54. package/dist/similarity.js +45 -0
  55. package/dist/sshsig.js +159 -0
  56. package/dist/state.js +135 -0
  57. package/dist/storage.js +61 -0
  58. package/dist/store.js +2259 -0
  59. package/dist/style.js +408 -0
  60. package/dist/sync.js +797 -0
  61. package/dist/trailers.js +121 -0
  62. package/dist/trust.js +222 -0
  63. package/dist/usage-fields.js +161 -0
  64. package/dist/vault.js +849 -0
  65. package/dist/verify.js +443 -0
  66. package/dist/version.js +70 -0
  67. package/dist/wait.js +449 -0
  68. package/dist/wikilinks.js +25 -0
  69. package/package.json +71 -4
@@ -0,0 +1,42 @@
1
+ import { PRACTICE_FIELDS } from "./api.js";
2
+ import { PracticeEntry, describeError, newestFormat } from "./schema.js";
3
+ // Where practice answers are kept (B-rehdw, "On the service, for every device"; docs/vault/Design/Practice Cards.md
4
+ // section 5): an address of their own, apart from the state, on `baesian serve` (`GET` and `POST /api/practice`, a folder
5
+ // of the live layer, `PRACTICE_DIR`) and on the relay (`GET` and `POST .../practice`, a table of the project's object,
6
+ // `PracticeTable`, for a signed-in browser only). Both take an entry through `practiceEntry()`. Nothing an agent runs
7
+ // reads them: not `inbox`, `wait`, the MCP tools, the plugin's hooks, `sync`, the vault adapter or the gate.
8
+ /** The live layer's folder of practice entries, in a clone that isn't linked. Not one of the kinds the record copies. */
9
+ export const PRACTICE_DIR = "practice";
10
+ /** Why a practice entry wasn't taken, with the status the address answers. */
11
+ export class PracticeRefused extends Error {
12
+ status;
13
+ constructor(status, message) {
14
+ super(message);
15
+ this.status = status;
16
+ }
17
+ }
18
+ /**
19
+ * A practice entry from what the client sent and who sent it, checked against those already kept: only the fields a
20
+ * practice entry may be sent with (`PRACTICE_FIELDS`), in its format; the blind test starts once; and an entry in the
21
+ * test needs it started. Throws `PracticeRefused`.
22
+ */
23
+ export function practiceEntry(input, who, earlier, at = new Date().toISOString()) {
24
+ if (typeof input !== "object" || input === null || Array.isArray(input))
25
+ throw new PracticeRefused(400, "send a practice answer as a JSON object");
26
+ const unknown = Object.keys(input).filter((k) => !PRACTICE_FIELDS.has(k));
27
+ if (unknown.length)
28
+ throw new PracticeRefused(400, `unexpected fields: ${unknown.join(", ")}`);
29
+ const parsed = PracticeEntry.safeParse({ ...input, type: "practice", format: newestFormat("practice"), reviewer: who.reviewer, ...(who.account ? { account: who.account } : {}), client: who.client, at });
30
+ if (!parsed.success)
31
+ throw new PracticeRefused(400, `that practice answer isn't valid:\n${describeError(parsed.error)}`);
32
+ const e = parsed.data;
33
+ const started = earlier.some((x) => x.kind === "test-start");
34
+ if (e.kind === "test-start" && started)
35
+ throw new PracticeRefused(409, "the blind test has started already: it runs once");
36
+ if ((e.kind === "mark" || (e.kind === "answer" && e.test)) && !started)
37
+ throw new PracticeRefused(409, "the blind test hasn't started");
38
+ return e;
39
+ }
40
+ /** Practice entries in the order they were written. */
41
+ export const inOrder = (entries) => [...entries].sort((a, b) => a.at.localeCompare(b.at));
42
+ //# sourceMappingURL=practice-answers.js.map
@@ -0,0 +1,197 @@
1
+ import { countedCards, replayCards, rowWindows } from "./report.js";
2
+ import { LEVEL_AT, POSITION_NAMES, ROW_NAMES, chanceUnder, isOwnCard, loosenAfter, oneMissWithin, replay, rowTest, stop, tightenAt } from "./routing.js";
3
+ import { SIMILAR_TITLE, jaccard, tokens } from "./similarity.js";
4
+ // The cards Baesian files itself from the record, with no model, as it files conflict cards (D65): convention cards,
5
+ // "Make these one rule?" (D161, P16 section 6), and slider proposals, "move this slider?" (B-y6jn6, B-jtsfh). The local server
6
+ // runs `afterVerdict` when the client records an answer; a relay sync will run it when the CLI pulls one (M9).
7
+ /** Where each kind goes: a deck of its own, created when it's first used. */
8
+ export const CONVENTIONS_DECK = {
9
+ id: "conventions",
10
+ title: "Patterns in your answers",
11
+ brief: "Baesian files these itself when your answers show a pattern: decisions in one area decided alike, or a comment that states a rule. Saying yes asks an agent to draft the rule, which comes back to you as a card.\n",
12
+ };
13
+ export const PREFERENCES_DECK = {
14
+ id: "preferences",
15
+ title: "Your preferences",
16
+ brief: "Baesian files these itself from each row's record: one stop down once it's 95% sure the row's misses are under 1 in 10, one stop up once it's as sure they're over. Accepting one writes a new preference file; Baesian never moves a row on its own.\n",
17
+ };
18
+ /** A comment that states a rule (P16 section 6). The word list is a guess, to tune while dogfooding. */
19
+ export const RULE_WORDS = /\b(always|never|from now on|in general)\b/i;
20
+ /** After the client records a verdict: the convention cards and slider proposals the record now calls for, filed. */
21
+ export function afterVerdict(store, verdict) {
22
+ return [...proposeConvention(store, verdict.card), ...proposeSliderMoves(store)];
23
+ }
24
+ // ---------------------------------------------------------------- convention cards (D161)
25
+ /** A decision that can be evidence for a convention: an agent's, resolved, and not already a rule or on its way to one. */
26
+ function eligible(store, card, used, ruled) {
27
+ const id = card.meta.id;
28
+ return !isOwnCard(card.meta) && !used.has(id) && !ruled.has(id) && store.status(id) === "resolved" && !store.ruleAsked(id);
29
+ }
30
+ const areaOf = (store, card) => store.canonicalTopic(card.meta.topic).split("/")[0];
31
+ /**
32
+ * A convention card from a decision just resolved, if its answer shows a pattern (P16 section 6): a comment in rule
33
+ * words, or at least one other resolved decision in the same area whose chosen answer is alike, by the filing check's
34
+ * similarity. A decision is evidence for one convention card at most, so a pattern you've declined comes back only
35
+ * from new decisions.
36
+ */
37
+ export function proposeConvention(store, id) {
38
+ const plan = store.view(() => {
39
+ const card = store.card(id);
40
+ if (!card)
41
+ return undefined;
42
+ const conventions = store.cards().filter((c) => c.meta.convention !== undefined && store.status(c.meta.id) !== "withdrawn");
43
+ const used = new Set(conventions.flatMap((c) => c.meta.depends_on));
44
+ const ruled = new Set(store.rules().flatMap((r) => (r.meta.from_card ? [r.meta.from_card] : [])));
45
+ if (!eligible(store, card, used, ruled))
46
+ return undefined;
47
+ const verdict = store.verdicts(id).filter((v) => v.revision === card.meta.revision).at(-1);
48
+ if (verdict?.make_rule)
49
+ return undefined;
50
+ const area = areaOf(store, card);
51
+ const decided = (c) => store.decision(c.meta.id) ?? "";
52
+ if (verdict?.comment && RULE_WORDS.test(verdict.comment)) {
53
+ return { signal: "rule-words", area, cards: [card], fact: `On ${id}, “${card.meta.title}”, you chose “${decided(card)}”, and wrote: “${verdict.comment}”` };
54
+ }
55
+ const mine = tokens(decided(card));
56
+ const alike = store
57
+ .cards()
58
+ .filter((o) => o.meta.id !== id && areaOf(store, o) === area && eligible(store, o, used, ruled) && jaccard(mine, tokens(decided(o))) >= SIMILAR_TITLE)
59
+ .sort((a, b) => a.meta.created.localeCompare(b.meta.created));
60
+ if (!alike.length)
61
+ return undefined;
62
+ const cards = [...alike, card];
63
+ return { signal: "same-choice", area, cards, fact: cards.map((c) => `${c.meta.id}, “${c.meta.title}”: you chose “${decided(c)}”`).join("; ") };
64
+ });
65
+ if (!plan)
66
+ return [];
67
+ const one = plan.cards.length === 1;
68
+ const draft = {
69
+ title: `A pattern in your answers on ${plan.area}`,
70
+ question: one ? "Should this decision become a rule?" : `Should these ${plan.cards.length} decisions become one rule?`,
71
+ doing: "reading your answers for patterns",
72
+ topic: `conventions/${plan.area}/${plan.cards[0].meta.id.toLowerCase()}`,
73
+ level: "provisional",
74
+ touches: [],
75
+ reverse: "cheap",
76
+ proposal: one ? "Draft a rule from it" : "Draft a rule from these",
77
+ proposal_then: "an agent drafts the rule and its gate patterns, and it comes back to you to confirm",
78
+ alternative: one ? "Keep it to this decision" : "Keep them separate",
79
+ alternative_then: one ? "nothing is saved, and this decision isn't proposed again" : "nothing is saved, and these decisions aren't proposed again",
80
+ crux: "Would you decide the next one like these the same way, unasked?",
81
+ crux_proposal: "If so",
82
+ crux_alternative: "If it depends",
83
+ crux_fact: plan.fact,
84
+ depends_on: plan.cards.map((c) => c.meta.id),
85
+ convention: plan.signal,
86
+ };
87
+ const body = `Baesian found this pattern itself, with no model (D161): ${plan.signal === "rule-words" ? "a comment that states a rule" : "decisions in one area whose chosen answers are alike"}.\n`;
88
+ return [store.fileOwn(draft, body, CONVENTIONS_DECK)];
89
+ }
90
+ // ---------------------------------------------------------------- slider proposals (B-y6jn6, B-jtsfh)
91
+ /** What a position does to a row's cards, in a line. */
92
+ function effect(to, settings) {
93
+ const rate = (p) => `about 1 in ${Math.round(1 / p)}`;
94
+ switch (to) {
95
+ case "agent-decides":
96
+ return `agents go ahead without you on these, and ${rate(settings.rate_agent_decides)} comes back to you as an audit`;
97
+ case "sample":
98
+ return `agents go ahead without you on these, and ${rate(settings.rate_sample)} comes back to you as an audit`;
99
+ case "tell-me":
100
+ return "these reach you, and agents carry on with their pick until you answer";
101
+ case "stop-and-ask":
102
+ return "agents wait for your answer on these while you're there";
103
+ case "never-without-me":
104
+ return "agents never go ahead on these without your answer";
105
+ }
106
+ }
107
+ const named = (p) => `“${POSITION_NAMES[p]}”`;
108
+ /**
109
+ * Propose moving each row one stop (B-y6jn6), by one test both ways (B-jtsfh): down once Baesian is 95% sure the row's
110
+ * miss rate is under 1 in 10, up once it's as sure it's over (`rowTest()`, a uniform prior over the rate), from every
111
+ * final answer at the row's position since it last moved, on cards filed since the agents' model last changed
112
+ * (`rowWindows()`). A miss adds to what loosening needs and never wipes out the answers before it. Misses are as cards
113
+ * B-87tkc and B-ab06a count them, final answers on shown rows and audits on held ones. After you keep a row where it was,
114
+ * that move is proposed again only from the answers since, passing the test on their own. Not past a row's floor, and not
115
+ * while a proposal on the row is still waiting; where both ways pass, it proposes tightening. Loosening is hard, since it
116
+ * lets more decisions past you; tightening is provisional.
117
+ */
118
+ export function proposeSliderMoves(store) {
119
+ const plans = store.view(() => {
120
+ const force = store.inForce();
121
+ const counted = countedCards(store);
122
+ return rowWindows(store, counted, force).flatMap((w) => {
123
+ if (w.waiting)
124
+ return [];
125
+ for (const dir of ["up", "down"]) {
126
+ const evidence = w[dir];
127
+ if (rowTest(evidence.answered.length, evidence.misses.length) !== dir)
128
+ continue;
129
+ const to = stop(w.row, w.position, dir);
130
+ if (to)
131
+ return [{ row: w.row, from: w.position, to, dir, evidence, counted, settings: force.settings }];
132
+ }
133
+ return [];
134
+ });
135
+ });
136
+ return plans.map((p) => store.fileOwn(sliderDraft(p.row, p.from, p.to, p.dir, p.evidence, p.counted, p.settings), sliderBody(p.dir), PREFERENCES_DECK));
137
+ }
138
+ /** What made a card a miss, in the words of a slider card's evidence (B-87tkc, B-ab06a). */
139
+ const MISSED_BY = {
140
+ overturn: "your final answer went against the agent's pick",
141
+ asked: "you kept the agent's pick, and marked that you should have been asked",
142
+ blind: "a question of what you value, you chose against the agent's pick before seeing its case",
143
+ };
144
+ /** What a miss is, as a slider card says it (B-87tkc, B-ab06a). */
145
+ export const MISS_WORDS = "A miss is a final answer that overturns the agent's pick, or one marked “you should have asked me” (B-87tkc); and on a question of what you value, a blind choice against the agent's pick, whatever you answer after its case (B-ab06a).";
146
+ /** Since when a row's answers are counted, as a slider card says it: "since it last moved". */
147
+ function sinceWords(since) {
148
+ if (!since)
149
+ return "so far";
150
+ if (since.why === "moved")
151
+ return "since it last moved";
152
+ if (since.why === "kept")
153
+ return "since you last kept it here";
154
+ return `since the agents' model changed to ${since.model}`;
155
+ }
156
+ const percent = (x) => `${(Math.floor(x * 1000) / 10).toFixed(1)}%`;
157
+ function sliderDraft(row, from, to, dir, evidence, counted, settings) {
158
+ const name = ROW_NAMES[row];
159
+ const lower = name.charAt(0).toLowerCase() + name.slice(1);
160
+ const n = evidence.answered.length;
161
+ const k = evidence.misses.length;
162
+ const last = dir === "up" ? evidence.misses.at(-1) : evidence.answered.at(-1);
163
+ const moved = { ...settings, rows: { ...settings.rows, [row]: to } };
164
+ const after = replay(replayCards(counted), moved).rows[row];
165
+ const cards = (c) => `${c} ${c === 1 ? "card" : "cards"}`;
166
+ // What the move would have held of past cards, when it moves to a stop that holds any back.
167
+ const history = after && !LEVEL_AT[to] ? ` Of the ${cards(after.cards)} on this row so far, ${after.held} would have been held back at ${named(to)}.` : "";
168
+ const record = `Of your ${n} final ${n === 1 ? "answer" : "answers"} on it ${sinceWords(evidence.since)}, ${k === 0 ? "none was a miss" : k === 1 ? "1 was a miss" : `${k} were misses`}`;
169
+ const under = chanceUnder(n, k);
170
+ const fact = dir === "up"
171
+ ? `“${name}” is at ${named(from)}. On ${last.card.meta.id}, “${last.card.meta.title}”, ${MISSED_BY[last.answers.missBy ?? "overturn"]}. ${record}: Baesian is ${percent(1 - under)} sure the row's misses are over 1 in 10.${history}`
172
+ : `“${name}” is at ${named(from)}. ${record}: Baesian is ${percent(under)} sure the row's misses are under 1 in 10.${history}`;
173
+ return {
174
+ title: `The setting for ${lower}`,
175
+ question: `Where should the setting for ${lower} be now?`,
176
+ doing: "reading the record of your answers, row by row",
177
+ topic: `preferences/${row.replace(/_/g, "-")}/${last.card.meta.id.toLowerCase()}`,
178
+ level: dir === "up" ? "provisional" : "hard",
179
+ touches: [],
180
+ reverse: "cheap",
181
+ proposal: `Move it to ${named(to)}`,
182
+ proposal_then: `a new preference file moves it: ${effect(to, settings)}${settings.mode === "shadow" && !["tell-me", "stop-and-ask", "never-without-me"].includes(to) ? " (in shadow, nothing is held back yet)" : ""}`,
183
+ alternative: `Keep it at ${named(from)}`,
184
+ // B-jtsfh: a move you decline is proposed again only once the answers after it pass the test on their own.
185
+ alternative_then: "nothing changes, and Baesian proposes this move again only once your answers from now on pass the same test on their own",
186
+ crux: dir === "up" ? "Are these misses a sign of this kind of decision, or chance?" : "Can agents take this kind of decision with less of your attention?",
187
+ crux_proposal: dir === "up" ? "A sign of the kind" : "If they can",
188
+ crux_alternative: dir === "up" ? "Chance" : "Not yet",
189
+ crux_fact: fact,
190
+ pref_row: row,
191
+ pref_from: from,
192
+ pref_to: to,
193
+ };
194
+ }
195
+ /** The rule a slider card comes from, in its body (B-y6jn6, B-jtsfh), with the numbers the test gives. */
196
+ export const sliderBody = (dir) => `Baesian files this itself from the record, by a fixed rule (B-y6jn6, B-jtsfh): ${dir === "up" ? "one stop up" : "one stop down"} once it's 95% sure the row's misses are ${dir === "up" ? "over" : "under"} 1 in 10, from every final answer on the row since it last moved, with no prior lean either way. ${dir === "up" ? `That's a miss in the first ${oneMissWithin()} answers, ${tightenAt(10)} in 10, or ${tightenAt(20)} in 20.` : `That's ${loosenAfter(0)} answers with no miss, ${loosenAfter(1)} with one, or ${loosenAfter(2)} with two.`} A change of the agents' model starts the count again, and after you keep a row where it is, Baesian proposes that move again only from the answers since. ${MISS_WORDS} Accepting writes a new preference file; Baesian never moves a row on its own.\n`;
197
+ //# sourceMappingURL=proposals.js.map
package/dist/quote.js ADDED
@@ -0,0 +1,28 @@
1
+ /** A name or branch as it's printed: as it is when it's plain, else as a JSON string, since names are self-declared. */
2
+ export function plain(name) {
3
+ return /^[\w][\w .@/+-]*$/.test(name) && !name.endsWith(" ") ? name : JSON.stringify(name);
4
+ }
5
+ /** The author, as a line names them: `Gus Ellerm (a person)`, `claude-code on main (an agent)`. */
6
+ export function byLine(a) {
7
+ return "person" in a ? `${plain(a.person)} (a person)` : `${plain(a.agent)} on ${plain(a.branch)} (an agent)`;
8
+ }
9
+ /** An agent's name alone, as a line names it: `claude-code (an agent)`. */
10
+ export const agentLabel = (name) => `${plain(name)} (an agent)`;
11
+ /** A text someone else wrote, as a JSON string: one line, its quotes and newlines escaped. */
12
+ export const q = (text) => JSON.stringify(text);
13
+ /** `<what> by <author>: "<text>"`. */
14
+ export const said = (what, a, text) => `${what} by ${byLine(a)}: ${q(text)}`;
15
+ export const cardAuthor = (m) => ({ agent: m.source_agent, branch: m.source_branch });
16
+ export const reviewerOf = (v) => ({ person: v.reviewer });
17
+ /**
18
+ * One verdict on a line: what the reviewer answered, by whom, and its comment, each text quoted (D169). `accepted` is
19
+ * the proposal an accept chose, when the caller has the card: an accept names what it decided, as `wait` does.
20
+ */
21
+ export function answerLine(v, accepted) {
22
+ const what = v.choice ?? (v.response === "accept" ? accepted : undefined);
23
+ const answer = `${v.response} by ${byLine(reviewerOf(v))}${what !== undefined ? `: ${q(what)}` : ""}`;
24
+ const comment = v.comment ? `; comment: ${q(v.comment)}` : "";
25
+ const retired = v.retire_rules?.length ? `; retires ${v.retire_rules.join(", ")}` : "";
26
+ return `${answer}${comment}${retired}`;
27
+ }
28
+ //# sourceMappingURL=quote.js.map
package/dist/report.js ADDED
@@ -0,0 +1,261 @@
1
+ import { FINAL, FIXED_ROWS, ROWS, TODAY, answeredInChat, isOwnCard, outcomeOf, rank, rowOf, stop, toLoosen, } from "./routing.js";
2
+ import { PENDING, settingsOf } from "./store.js";
3
+ /**
4
+ * The agents' cards, on their rows, oldest first: Baesian's own and withdrawn cards are left out, and so are cards
5
+ * answered in chat (B-hswmx), whose choice wasn't made blind (`chatCards()` counts them). A card counts on its latest
6
+ * revision's record; one filed before the gate counts as "Today" showed it, in shadow.
7
+ */
8
+ export function countedCards(store) {
9
+ return store.view(() => store
10
+ .cards()
11
+ .flatMap((card) => {
12
+ const id = card.meta.id;
13
+ const gate = store.gateRecord(id, card.meta.revision);
14
+ if (isOwnCard(card.meta) || gate?.source === "baesian")
15
+ return [];
16
+ const status = store.status(id);
17
+ if (status === "withdrawn")
18
+ return [];
19
+ const verdicts = store.verdicts(id);
20
+ if (answeredInChat({ meta: card.meta, verdicts }))
21
+ return [];
22
+ // A crux of preference makes a blind choice against the pick a miss (B-ab06a), as the latest revision states it.
23
+ const answers = outcomeOf(verdicts, { preference: card.meta.crux_preference });
24
+ if (gate) {
25
+ return [{ card, row: gate.row, position: gate.position, outcome: gate.outcome, mode: gate.mode, drawn: gate.drawn === true, at: gate.at, held: status === "held", answers, gate }];
26
+ }
27
+ const { row, position } = rowOf(card.meta.touches, TODAY);
28
+ return [{ card, row, position, outcome: "shown", mode: "shadow", drawn: false, at: card.meta.created, held: false, answers }];
29
+ })
30
+ .sort((a, b) => a.at.localeCompare(b.at)));
31
+ }
32
+ /**
33
+ * The agents' cards answered in chat (B-hswmx), each by the row `countedCards()` would put it on: left out of every
34
+ * measure, and only counted, so the report can say how many it left out. Withdrawn ones aren't counted at all.
35
+ */
36
+ export function chatCards(store) {
37
+ return store.view(() => store.cards().flatMap((card) => {
38
+ const id = card.meta.id;
39
+ const gate = store.gateRecord(id, card.meta.revision);
40
+ if (isOwnCard(card.meta) || gate?.source === "baesian" || store.status(id) === "withdrawn")
41
+ return [];
42
+ if (!answeredInChat({ meta: card.meta, verdicts: store.verdicts(id) }))
43
+ return [];
44
+ return [gate ? gate.row : rowOf(card.meta.touches, TODAY).row];
45
+ }));
46
+ }
47
+ /** The counted cards as the replay reads them (`replay()` in src/routing.ts). */
48
+ export function replayCards(counted) {
49
+ return counted.map((c) => ({
50
+ id: c.card.meta.id,
51
+ touches: c.card.meta.touches,
52
+ reverse: c.card.meta.reverse,
53
+ ...(c.gate?.asked ? { asked: c.gate.asked } : {}),
54
+ source: c.gate?.source ?? "agent",
55
+ outcome: c.answers,
56
+ }));
57
+ }
58
+ /** When each row last moved, from the preference history: a row never moved from "Today" has no time. */
59
+ function lastMoves(store) {
60
+ const out = {};
61
+ let rows = TODAY.rows;
62
+ for (const name of store.preferenceNames()) {
63
+ let file;
64
+ try {
65
+ file = store.preferenceFile(name);
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ if (!file)
71
+ continue;
72
+ const next = settingsOf(file).rows;
73
+ for (const r of ROWS)
74
+ if (next[r] !== rows[r])
75
+ out[r] = file.at;
76
+ rows = next;
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * The agents' latest change of model (B-jtsfh, B-k0w7d): across every counted card, in filing order, the last card whose
82
+ * model is known and differs from the last one known before it. A card with no model changes nothing, and counts in
83
+ * whichever model's run it falls. Two models filing alternately keep starting the count again.
84
+ */
85
+ export function modelChange(counted) {
86
+ let known;
87
+ let change;
88
+ for (const c of counted) {
89
+ const model = c.card.meta.source_model;
90
+ if (model === undefined)
91
+ continue;
92
+ if (known !== undefined && model !== known)
93
+ change = { at: c.at, model };
94
+ known = model;
95
+ }
96
+ return change;
97
+ }
98
+ /**
99
+ * When you last kept each row where it was, for each move Baesian proposed (B-jtsfh): the time of the final answer, other
100
+ * than accepting it, on a slider proposal that's no longer waiting. Baesian proposes that move again only from the final
101
+ * answers after it, so it never asks again on the evidence you just answered.
102
+ */
103
+ function keptAt(store) {
104
+ const out = new Map();
105
+ for (const c of store.cards()) {
106
+ const m = c.meta;
107
+ if (!m.pref_row || !m.pref_from || !m.pref_to || PENDING.has(store.status(m.id)))
108
+ continue;
109
+ const last = store.verdicts(m.id).filter((v) => FINAL.has(v.response)).at(-1);
110
+ if (!last || last.response === "accept")
111
+ continue;
112
+ const key = `${m.pref_row} ${rank(m.pref_to) > rank(m.pref_from) ? "up" : "down"}`;
113
+ if ((out.get(key) ?? "") < last.at)
114
+ out.set(key, last.at);
115
+ }
116
+ return out;
117
+ }
118
+ /**
119
+ * Each row's record for moving it, under the settings in force (B-jtsfh): the final answers at its position since it last
120
+ * moved, on cards filed since the agents' model last changed; for each way, less those before you last kept it where it
121
+ * was when Baesian proposed moving it that way. A miss adds to what's needed, and never wipes out the answers before it.
122
+ */
123
+ export function rowWindows(store, counted, force = store.inForce()) {
124
+ return store.view(() => {
125
+ const moved = lastMoves(store);
126
+ const model = modelChange(counted);
127
+ const kept = keptAt(store);
128
+ const waiting = new Set(store.cards().flatMap((c) => (c.meta.pref_row && PENDING.has(store.status(c.meta.id)) ? [c.meta.pref_row] : [])));
129
+ return ROWS.map((row) => {
130
+ const position = force.settings.rows[row];
131
+ const since = moved[row];
132
+ const base = counted
133
+ .filter((c) => c.row === row && c.position === position && c.answers.final && (!since || c.answers.final.at >= since) && (!model || c.at >= model.at))
134
+ .sort((a, b) => a.answers.final.at.localeCompare(b.answers.final.at));
135
+ const evidence = (dir) => {
136
+ const keptHere = kept.get(`${row} ${dir}`);
137
+ const answered = keptHere ? base.filter((c) => c.answers.final.at > keptHere) : base;
138
+ const starts = [
139
+ ...(since ? [{ at: since, why: "moved" }] : []),
140
+ ...(model ? [{ at: model.at, why: "model", model: model.model }] : []),
141
+ ...(keptHere ? [{ at: keptHere, why: "kept" }] : []),
142
+ ];
143
+ const start = starts.sort((a, b) => a.at.localeCompare(b.at)).at(-1);
144
+ return { ...(start ? { since: start } : {}), answered, misses: answered.filter((c) => c.answers.miss) };
145
+ };
146
+ return { row, position, down: evidence("down"), up: evidence("up"), waiting: waiting.has(row) };
147
+ });
148
+ });
149
+ }
150
+ /** A row of the report with nothing counted yet. */
151
+ export const blankRow = () => ({
152
+ shown: 0,
153
+ held: 0,
154
+ wouldHold: 0,
155
+ drawn: 0,
156
+ auditsAnswered: 0,
157
+ auditsUnanswered: 0,
158
+ answered: 0,
159
+ misses: 0,
160
+ switched: 0,
161
+ blind: 0,
162
+ matched: 0,
163
+ agreed: 0,
164
+ cameRound: 0,
165
+ unblind: 0,
166
+ unanswered: 0,
167
+ heldWaiting: 0,
168
+ inChat: 0,
169
+ });
170
+ /**
171
+ * Where a card falls in your calibration's bar (D180), by its answers: a miss (overturned, should have been asked, or on
172
+ * a crux of preference chosen against blind, B-ab06a), came round (switched, on a crux of fact), chose as the agent did
173
+ * (a final accept whose blind choice was its pick); or, left out of the bar, a final accept with no blind choice, or no
174
+ * final answer at all. Each card falls in exactly one.
175
+ */
176
+ export function calibrationOf(answers) {
177
+ if (answers.miss)
178
+ return "missed";
179
+ if (answers.switched)
180
+ return "cameRound";
181
+ if (!answers.final)
182
+ return "unanswered";
183
+ return answers.final.response === "accept" && answers.final.first_choice === "proposal" ? "agreed" : "unblind";
184
+ }
185
+ /** The report (P14 section 3): per row, and pooled first, as the client shows it. */
186
+ export function routingReport(store, force = store.inForce()) {
187
+ return store.view(() => {
188
+ const counted = countedCards(store);
189
+ const rows = {};
190
+ const pooled = blankRow();
191
+ for (const row of ROWS)
192
+ rows[row] = { position: force.settings.rows[row], ...blankRow() };
193
+ for (const c of counted) {
194
+ const r = (rows[c.row] ??= { position: FIXED_ROWS.includes(c.row) ? "never-without-me" : force.settings.rows[c.row], ...blankRow() });
195
+ for (const t of [r, pooled]) {
196
+ if (c.outcome === "shown")
197
+ t.shown++;
198
+ else if (c.mode === "enforce")
199
+ t.held++;
200
+ else
201
+ t.wouldHold++;
202
+ if (c.outcome === "held" && c.mode === "enforce" && c.drawn) {
203
+ t.drawn++;
204
+ if (c.held)
205
+ t.auditsUnanswered++;
206
+ else if (c.answers.final)
207
+ t.auditsAnswered++;
208
+ }
209
+ if (c.answers.final) {
210
+ t.answered++;
211
+ if (c.answers.miss)
212
+ t.misses++;
213
+ if (c.answers.switched)
214
+ t.switched++;
215
+ if (c.answers.final.first_choice) {
216
+ t.blind++;
217
+ if (c.answers.final.first_choice === "proposal")
218
+ t.matched++;
219
+ }
220
+ }
221
+ else if (c.answers.miss)
222
+ t.misses++;
223
+ const where = calibrationOf(c.answers);
224
+ if (where === "agreed")
225
+ t.agreed++;
226
+ else if (where === "cameRound")
227
+ t.cameRound++;
228
+ else if (where === "unblind")
229
+ t.unblind++;
230
+ else if (where === "unanswered") {
231
+ t.unanswered++;
232
+ if (c.held)
233
+ t.heldWaiting++;
234
+ }
235
+ }
236
+ }
237
+ // Answered in chat (B-hswmx): left out of every count above, and said beside them.
238
+ for (const row of chatCards(store)) {
239
+ const r = (rows[row] ??= { position: FIXED_ROWS.includes(row) ? "never-without-me" : force.settings.rows[row], ...blankRow() });
240
+ r.inChat++;
241
+ pooled.inChat++;
242
+ }
243
+ // What each row now needs for a lower stop (B-jtsfh), which the calibration panel says beside its bar.
244
+ for (const w of rowWindows(store, counted, force)) {
245
+ const { since, answered, misses } = w.down;
246
+ const lower = stop(w.row, w.position, "down") !== undefined;
247
+ rows[w.row].test = {
248
+ ...(since ? { since: since.at, why: since.why } : {}),
249
+ answers: answered.length,
250
+ misses: misses.length,
251
+ ...(lower ? { toLoosen: toLoosen(answered.length, misses.length) } : {}),
252
+ waiting: w.waiting,
253
+ };
254
+ }
255
+ for (const r of [...Object.values(rows), pooled])
256
+ if (r.answered > 0 && r.misses === 0)
257
+ r.bound = 3 / r.answered;
258
+ return { rows, pooled, beforeGate: counted.filter((c) => !c.gate).length };
259
+ });
260
+ }
261
+ //# sourceMappingURL=report.js.map