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.
- package/LICENSE.md +105 -0
- package/README.md +217 -2
- package/dist/agents.js +249 -0
- package/dist/api.js +13 -0
- package/dist/bin.js +14 -0
- package/dist/ci.js +82 -0
- package/dist/cite.js +66 -0
- package/dist/cli.js +831 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
- package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
- package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
- package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
- package/dist/client/assets/index-9yf70Vqv.css +1 -0
- package/dist/client/assets/index-Dwg9e0YR.js +15 -0
- package/dist/client/index.html +15 -0
- package/dist/facts.js +278 -0
- package/dist/folder-storage.js +71 -0
- package/dist/frontmatter.js +38 -0
- package/dist/gate-paths.js +77 -0
- package/dist/gate.js +237 -0
- package/dist/git.js +100 -0
- package/dist/globs.js +120 -0
- package/dist/hookblock.js +146 -0
- package/dist/hooks.js +911 -0
- package/dist/ids.js +23 -0
- package/dist/login.js +301 -0
- package/dist/mcp.js +303 -0
- package/dist/merges.js +849 -0
- package/dist/modes.js +245 -0
- package/dist/notify.js +91 -0
- package/dist/ops.js +381 -0
- package/dist/passkey.js +86 -0
- package/dist/personal.js +163 -0
- package/dist/practice-answers.js +42 -0
- package/dist/proposals.js +197 -0
- package/dist/quote.js +28 -0
- package/dist/report.js +261 -0
- package/dist/routing.js +411 -0
- package/dist/schema.js +1178 -0
- package/dist/server.js +514 -0
- package/dist/similarity.js +45 -0
- package/dist/sshsig.js +159 -0
- package/dist/state.js +135 -0
- package/dist/storage.js +61 -0
- package/dist/store.js +2259 -0
- package/dist/style.js +408 -0
- package/dist/sync.js +797 -0
- package/dist/trailers.js +121 -0
- package/dist/trust.js +222 -0
- package/dist/usage-fields.js +161 -0
- package/dist/vault.js +849 -0
- package/dist/verify.js +443 -0
- package/dist/version.js +70 -0
- package/dist/wait.js +449 -0
- package/dist/wikilinks.js +25 -0
- package/package.json +71 -4
package/dist/routing.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { pathsLoosening } from "./gate-paths.js";
|
|
2
|
+
// The gate's rules (M8, P14, P16): rows, positions, presets, and how a card's stated facts become an outcome at filing
|
|
3
|
+
// (D144, D145). Pure and deterministic, with nothing from Node and no runtime import but the commit gate's paths, pure
|
|
4
|
+
// too, so the CLI, CI (`baesian verify`), a relay later (D158) and the client all reach the same answer. The store runs it at filing and writes its record;
|
|
5
|
+
// docs/vault/Modules/src-routing.md describes it.
|
|
6
|
+
// ---------------------------------------------------------------- rows and positions
|
|
7
|
+
/** The rows a repo sets, keyed by name in the preference file (`row_<name>`): six sliders, security and privacy (D146). */
|
|
8
|
+
export const ROWS = ["public_api", "schema", "dependency", "infra", "user_facing", "security", "privacy", "other"];
|
|
9
|
+
/** The rows fixed at "never without me", with no key in the preference file (D146). */
|
|
10
|
+
export const FIXED_ROWS = ["publishes", "money", "data_loss"];
|
|
11
|
+
export const ALL_ROWS = [...ROWS, ...FIXED_ROWS];
|
|
12
|
+
/** The five positions, from the loosest (P14's scale). The two lowest hold a card back; the rest show it at a level. */
|
|
13
|
+
export const POSITIONS = ["agent-decides", "sample", "tell-me", "stop-and-ask", "never-without-me"];
|
|
14
|
+
/** What the client calls each row, by its consequence (P14). */
|
|
15
|
+
export const ROW_NAMES = {
|
|
16
|
+
public_api: "Public interfaces and module boundaries",
|
|
17
|
+
schema: "Data formats",
|
|
18
|
+
dependency: "Runtime dependencies",
|
|
19
|
+
infra: "Delivery and infrastructure",
|
|
20
|
+
user_facing: "Product behaviour",
|
|
21
|
+
security: "Security",
|
|
22
|
+
privacy: "Privacy",
|
|
23
|
+
other: "Anything else that's hard to undo",
|
|
24
|
+
publishes: "Publishing",
|
|
25
|
+
money: "Spending",
|
|
26
|
+
data_loss: "Destroying data",
|
|
27
|
+
};
|
|
28
|
+
export const POSITION_NAMES = {
|
|
29
|
+
"agent-decides": "let the agent decide",
|
|
30
|
+
sample: "check a sample afterwards",
|
|
31
|
+
"tell-me": "tell me but carry on",
|
|
32
|
+
"stop-and-ask": "stop and ask",
|
|
33
|
+
"never-without-me": "never without me",
|
|
34
|
+
};
|
|
35
|
+
/** The level a shown card gets at each position. The two lowest positions hold it back. */
|
|
36
|
+
export const LEVEL_AT = { "tell-me": "provisional", "stop-and-ask": "blocking", "never-without-me": "hard" };
|
|
37
|
+
/** The lowest a row may be set: security, and privacy beside it, no lower than "tell me but carry on" (D146, B-z3agk). */
|
|
38
|
+
export const ROW_FLOOR = { security: "tell-me", privacy: "tell-me" };
|
|
39
|
+
/** The row each of the D11 facts falls in. A card with no fact falls in `other`. */
|
|
40
|
+
export const ROW_OF_FACT = {
|
|
41
|
+
"public-api": "public_api",
|
|
42
|
+
schema: "schema",
|
|
43
|
+
dependency: "dependency",
|
|
44
|
+
infra: "infra",
|
|
45
|
+
"user-facing": "user_facing",
|
|
46
|
+
security: "security",
|
|
47
|
+
privacy: "privacy",
|
|
48
|
+
publishes: "publishes",
|
|
49
|
+
money: "money",
|
|
50
|
+
"data-loss": "data_loss",
|
|
51
|
+
};
|
|
52
|
+
/** Which row a card counts in when several of its rows sit at the same position: the fixed rows, then security and privacy. */
|
|
53
|
+
const ROW_ORDER = ["publishes", "money", "data_loss", "security", "privacy", "public_api", "schema", "infra", "dependency", "user_facing", "other"];
|
|
54
|
+
export const rank = (p) => POSITIONS.indexOf(p);
|
|
55
|
+
const LEVELS = ["provisional", "blocking", "hard"];
|
|
56
|
+
export const levelRank = (l) => LEVELS.indexOf(l);
|
|
57
|
+
export const higherLevel = (a, b) => (b !== undefined && levelRank(b) > levelRank(a) ? b : a);
|
|
58
|
+
export const isFixed = (row) => FIXED_ROWS.includes(row);
|
|
59
|
+
/** One stop stricter or looser, within the row's floor; undefined at either end (B-y6jn6). */
|
|
60
|
+
export function stop(row, from, dir) {
|
|
61
|
+
const i = rank(from) + (dir === "up" ? 1 : -1);
|
|
62
|
+
const floor = ROW_FLOOR[row];
|
|
63
|
+
if (i < (floor ? rank(floor) : 0) || i >= POSITIONS.length)
|
|
64
|
+
return undefined;
|
|
65
|
+
return POSITIONS[i];
|
|
66
|
+
}
|
|
67
|
+
const tellMe = {
|
|
68
|
+
public_api: "tell-me",
|
|
69
|
+
schema: "tell-me",
|
|
70
|
+
dependency: "tell-me",
|
|
71
|
+
infra: "tell-me",
|
|
72
|
+
user_facing: "tell-me",
|
|
73
|
+
security: "never-without-me",
|
|
74
|
+
privacy: "never-without-me",
|
|
75
|
+
other: "tell-me",
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* What a preference change loosens, against the settings in force before it (D163): each row moved toward held,
|
|
79
|
+
* enforcing turned on, and a lower audit rate or burn-in (B-shndb), which brings fewer held cards back to you; and a
|
|
80
|
+
* commit gate that sees less, by its globs (B-qkek1, `pathsLoosening()`). Each lets more past you, so in a hosted repo it
|
|
81
|
+
* takes your passkey; tightening doesn't.
|
|
82
|
+
*/
|
|
83
|
+
export function loosening(before, after) {
|
|
84
|
+
const out = [];
|
|
85
|
+
if (before.mode === "shadow" && after.mode === "enforce")
|
|
86
|
+
out.push("it turns enforcing on");
|
|
87
|
+
for (const row of ROWS) {
|
|
88
|
+
if (rank(after.rows[row]) < rank(before.rows[row]))
|
|
89
|
+
out.push(`it moves ${row} from ${before.rows[row]} to ${after.rows[row]}`);
|
|
90
|
+
}
|
|
91
|
+
const percent = (x) => `${Math.round(x * 1000) / 10}%`;
|
|
92
|
+
for (const rate of ["rate_agent_decides", "rate_sample"]) {
|
|
93
|
+
if (after[rate] < before[rate])
|
|
94
|
+
out.push(`it lowers ${rate} from ${percent(before[rate])} to ${percent(after[rate])}`);
|
|
95
|
+
}
|
|
96
|
+
if (after.burn_in < before.burn_in)
|
|
97
|
+
out.push(`it lowers burn_in from ${before.burn_in} to ${after.burn_in}`);
|
|
98
|
+
out.push(...pathsLoosening(before.paths, after.paths));
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** The three presets (B-wwxa3), from P14 section 4's table: security and privacy at "never without me" in all three. */
|
|
102
|
+
export const PRESETS = [
|
|
103
|
+
{ id: "today", name: "Today", summary: "Every card reaches you, as it does now.", rows: tellMe },
|
|
104
|
+
{
|
|
105
|
+
id: "fewer-interruptions",
|
|
106
|
+
name: "Fewer interruptions",
|
|
107
|
+
summary: "Dependencies, infrastructure and anything else go ahead, and a sample comes back to you.",
|
|
108
|
+
rows: { ...tellMe, dependency: "sample", infra: "sample", other: "sample" },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "hands-off",
|
|
112
|
+
name: "Hands-off",
|
|
113
|
+
summary: "Every row but security and privacy goes ahead; dependencies and anything else are the agent's, with fewer audits.",
|
|
114
|
+
rows: { ...tellMe, public_api: "sample", schema: "sample", infra: "sample", user_facing: "sample", dependency: "agent-decides", other: "agent-decides" },
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
// ---------------------------------------------------------------- when Baesian proposes moving a row (B-jtsfh)
|
|
118
|
+
/** The miss rate a row is tested against (B-jtsfh): 1 in 10. */
|
|
119
|
+
export const MISS_RATE = 0.1;
|
|
120
|
+
/** How sure the test must be before Baesian proposes a move (B-jtsfh): 95%. */
|
|
121
|
+
export const SURE = 0.95;
|
|
122
|
+
/**
|
|
123
|
+
* How likely a row's miss rate is under `MISS_RATE`, from `answers` final answers with `misses` among them (B-jtsfh):
|
|
124
|
+
* the posterior from a uniform prior, Beta(misses + 1, answers - misses + 1). For whole numbers a and b, P(p < x) under
|
|
125
|
+
* Beta(a, b) is P(Binomial(a + b - 1, x) ≥ a), so it's one minus a binomial's first `misses + 1` terms.
|
|
126
|
+
*/
|
|
127
|
+
export function chanceUnder(answers, misses, x = MISS_RATE) {
|
|
128
|
+
const n = answers + 1;
|
|
129
|
+
let log = n * Math.log(1 - x);
|
|
130
|
+
let below = 0;
|
|
131
|
+
for (let i = 0; i <= misses && i < n; i++) {
|
|
132
|
+
below += Math.exp(log);
|
|
133
|
+
log += Math.log((n - i) / (i + 1)) + Math.log(x / (1 - x));
|
|
134
|
+
}
|
|
135
|
+
return Math.max(0, Math.min(1, 1 - below));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The one test, both ways (B-jtsfh): "down" once Baesian is `SURE` a row's miss rate is under `MISS_RATE`, "up" once it's
|
|
139
|
+
* as sure it's over, or undefined. Loosening after 28 final answers with no miss, 45 with one, 60 with two, 75 with three;
|
|
140
|
+
* tightening at a miss in the first two answers, 2 in 3, 3 in 10, 5 in 20, 8 in 42.
|
|
141
|
+
*/
|
|
142
|
+
export function rowTest(answers, misses) {
|
|
143
|
+
const under = chanceUnder(answers, misses);
|
|
144
|
+
if (under >= SURE)
|
|
145
|
+
return "down";
|
|
146
|
+
if (1 - under >= SURE)
|
|
147
|
+
return "up";
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/** More final answers, all without a miss, before the test proposes a lower stop: 0 once it would. */
|
|
151
|
+
export function toLoosen(answers, misses) {
|
|
152
|
+
let more = 0;
|
|
153
|
+
while (chanceUnder(answers + more, misses) < SURE)
|
|
154
|
+
more++;
|
|
155
|
+
return more;
|
|
156
|
+
}
|
|
157
|
+
/** Final answers it takes, from none, before the test proposes a lower stop, with `misses` among them: 28 with none. */
|
|
158
|
+
export const loosenAfter = (misses = 0) => toLoosen(misses, misses) + misses;
|
|
159
|
+
/** The most final answers in which a single miss proposes a higher stop: a miss in the first 2. */
|
|
160
|
+
export function oneMissWithin() {
|
|
161
|
+
let answers = 1;
|
|
162
|
+
while (rowTest(answers + 1, 1) === "up")
|
|
163
|
+
answers++;
|
|
164
|
+
return answers;
|
|
165
|
+
}
|
|
166
|
+
/** The fewest misses in `answers` final answers at which the test proposes a higher stop: 1 in 2, 3 in 10, 5 in 20. */
|
|
167
|
+
export function tightenAt(answers) {
|
|
168
|
+
let misses = 0;
|
|
169
|
+
while (misses <= answers && rowTest(answers, misses) !== "up")
|
|
170
|
+
misses++;
|
|
171
|
+
return misses;
|
|
172
|
+
}
|
|
173
|
+
/** The audit rates and burn-in every preset starts with (B-fgc4y): 1 in 10, 1 in 3, and each row's first 10 held cards. */
|
|
174
|
+
export const AUDIT_DEFAULTS = { rate_agent_decides: 0.1, rate_sample: 1 / 3, burn_in: 10 };
|
|
175
|
+
/** With no preference file, a repo runs "Today" in shadow, which routes exactly as Baesian did before the gate (P14). */
|
|
176
|
+
export const TODAY = { mode: "shadow", rows: PRESETS[0].rows, ...AUDIT_DEFAULTS, paths: {} };
|
|
177
|
+
// ---------------------------------------------------------------- the rules (rules-1)
|
|
178
|
+
/** The rules' version, in every gate record. */
|
|
179
|
+
export const GATE_VERSION = "rules-1";
|
|
180
|
+
/** The strictest row a card's facts fall in, and its position (P14: strictest wins); `other` for a card with none. */
|
|
181
|
+
export function rowOf(touches, settings) {
|
|
182
|
+
const positionOf = (row) => (isFixed(row) ? "never-without-me" : settings.rows[row]);
|
|
183
|
+
const rows = touches.length ? [...new Set(touches.map((f) => ROW_OF_FACT[f]))] : ["other"];
|
|
184
|
+
let best = rows[0];
|
|
185
|
+
for (const row of rows.slice(1)) {
|
|
186
|
+
const d = rank(positionOf(row)) - rank(positionOf(best));
|
|
187
|
+
if (d > 0 || (d === 0 && ROW_ORDER.indexOf(row) < ROW_ORDER.indexOf(best)))
|
|
188
|
+
best = row;
|
|
189
|
+
}
|
|
190
|
+
return { row: best, position: positionOf(best) };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The gate's outcome for a card (D145): its strictest row's position, raised by the floors, by the one-way rule (B-yebnn)
|
|
194
|
+
* and for Baesian's own cards and revisions that follow an answer, each only raising it. "Tell me but carry on" or higher
|
|
195
|
+
* shows it at that position's level, raised by the agent's (B-chq61: a level is a minimum only when it's shown);
|
|
196
|
+
* either of the two lowest holds it back, drawn for audit at that position's rate, or always within the row's burn-in
|
|
197
|
+
* (B-fgc4y). In shadow this is what it would have done (D148).
|
|
198
|
+
*/
|
|
199
|
+
export function route(input, settings) {
|
|
200
|
+
const { row, position } = rowOf(input.touches, settings);
|
|
201
|
+
let at = position;
|
|
202
|
+
let reason = isFixed(row) ? "floor" : "row";
|
|
203
|
+
const raise = (to, why) => {
|
|
204
|
+
if (rank(at) < rank(to)) {
|
|
205
|
+
at = to;
|
|
206
|
+
reason = why;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
if (!isFixed(row) && ROW_FLOOR[row])
|
|
210
|
+
raise(ROW_FLOOR[row], "floor");
|
|
211
|
+
if (input.reverse === "one-way")
|
|
212
|
+
raise("tell-me", "one-way");
|
|
213
|
+
if (input.source !== "agent")
|
|
214
|
+
raise("tell-me", input.source);
|
|
215
|
+
const shownAt = LEVEL_AT[at];
|
|
216
|
+
if (shownAt) {
|
|
217
|
+
const level = higherLevel(shownAt, input.asked);
|
|
218
|
+
return { row, position, outcome: "shown", level, reason: level !== shownAt ? "agent" : reason };
|
|
219
|
+
}
|
|
220
|
+
const p = at === "agent-decides" ? settings.rate_agent_decides : settings.rate_sample;
|
|
221
|
+
const held_before = input.heldBefore(row);
|
|
222
|
+
const burn_in = held_before < settings.burn_in;
|
|
223
|
+
return { row, position, outcome: "held", reason, draw: input.draw, p, held_before, burn_in, drawn: burn_in || input.draw < p };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The level a card is written with, the level in force locally (P16 section 3): a shown card's level; a held card's
|
|
227
|
+
* "provisional" when enforcing, since the agent goes ahead with its pick (B-chq61); and in shadow, where nothing is
|
|
228
|
+
* held, the agent's level or provisional, as Baesian showed it before the gate.
|
|
229
|
+
*/
|
|
230
|
+
export function levelInForce(d, mode, asked) {
|
|
231
|
+
if (d.outcome === "shown")
|
|
232
|
+
return d.level;
|
|
233
|
+
return mode === "enforce" ? "provisional" : higherLevel("provisional", asked);
|
|
234
|
+
}
|
|
235
|
+
// ---------------------------------------------------------------- misses (B-87tkc, B-ab06a)
|
|
236
|
+
/** Answers that end a card one way or the other: a choice, the reviewer's own answer, or a rejection. */
|
|
237
|
+
export const FINAL = new Set(["accept", "alternative", "correction", "reject"]);
|
|
238
|
+
/** Final answers that overturn the agent's pick. */
|
|
239
|
+
export const OVERTURNS = new Set(["alternative", "correction", "reject"]);
|
|
240
|
+
/**
|
|
241
|
+
* A card's outcome from its verdicts. `preference` is whether its crux is one of preference (`crux_preference`, on its
|
|
242
|
+
* latest revision): then the blind choice is the answer that counts, since the agent's case can persuade you of what to
|
|
243
|
+
* value but holds no fact you lacked (B-ab06a). On a crux of fact B-87tkc stands: coming round after the case isn't a miss.
|
|
244
|
+
*/
|
|
245
|
+
export function outcomeOf(verdicts, opts = {}) {
|
|
246
|
+
const last = [...verdicts].reverse().find((v) => FINAL.has(v.response));
|
|
247
|
+
const asked = verdicts.some((v) => v.should_have_asked === true);
|
|
248
|
+
if (!last)
|
|
249
|
+
return asked ? { miss: true, missBy: "asked", switched: false } : { miss: false, switched: false };
|
|
250
|
+
const final = { response: last.response, at: last.at, ...(last.first_choice ? { first_choice: last.first_choice } : {}) };
|
|
251
|
+
const missBy = OVERTURNS.has(last.response) ? "overturn" : asked ? "asked" : opts.preference === true && last.first_choice === "alternative" ? "blind" : undefined;
|
|
252
|
+
return { final, miss: missBy !== undefined, ...(missBy ? { missBy } : {}), switched: last.response === "accept" && last.first_choice === "alternative" };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Whether a card was answered in chat (B-hswmx): its latest revision says so, or an answer on an earlier one was marked
|
|
256
|
+
* seen. Its choice wasn't made blind, so calibration leaves it out of every measure: the report's counts, the replay of
|
|
257
|
+
* past cards, and the slider proposals. It's in the record, and can become a rule, like any card.
|
|
258
|
+
*/
|
|
259
|
+
export function answeredInChat(c) {
|
|
260
|
+
return c.meta.answered_in_chat !== undefined || c.verdicts.some((v) => v.seen === true);
|
|
261
|
+
}
|
|
262
|
+
// ---------------------------------------------------------------- Baesian's own cards
|
|
263
|
+
/** The name Baesian files its own cards under: conflict, convention and slider cards. */
|
|
264
|
+
export const BAESIAN = "baesian";
|
|
265
|
+
/**
|
|
266
|
+
* Baesian's own cards: conflicts (D35), rule drafts (D110), convention cards (D161) and slider proposals (B-y6jn6). The
|
|
267
|
+
* gate always shows them, and the report, the replay and the proposals leave them out: they're not an agent's decisions.
|
|
268
|
+
*/
|
|
269
|
+
export function isOwnCard(meta) {
|
|
270
|
+
return meta.source_agent === BAESIAN || meta.rule !== undefined || meta.convention !== undefined || meta.pref_row !== undefined || meta.conflict_of !== undefined;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A card as `GET /api/state` gives it, as the replay reads it, or undefined for one it leaves out: Baesian's own,
|
|
274
|
+
* withdrawn ones, and those answered in chat (B-hswmx). So the client can replay any setting itself, a single row moved,
|
|
275
|
+
* say, as the server does for presets.
|
|
276
|
+
*/
|
|
277
|
+
export function replayCardOf(c) {
|
|
278
|
+
if (isOwnCard(c.meta) || c.gate?.source === "baesian" || c.status === "withdrawn" || answeredInChat(c))
|
|
279
|
+
return undefined;
|
|
280
|
+
return { id: c.meta.id, touches: c.meta.touches, reverse: c.meta.reverse, ...(c.gate?.asked ? { asked: c.gate.asked } : {}), source: c.gate?.source ?? "agent", outcome: outcomeOf(c.verdicts, { preference: c.meta.crux_preference }) };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* What a setting would have done to past cards, by re-running the rules over them (P14 section 4). Under "Today" the
|
|
284
|
+
* shadow record never shows a card as would-have-held, so this is how the client shows what a looser preset would
|
|
285
|
+
* hold. Cards in filing order; the audits are an expectation, since the draw needs a hash the client doesn't compute.
|
|
286
|
+
*/
|
|
287
|
+
export function replay(cards, settings) {
|
|
288
|
+
const empty = () => ({ cards: 0, shown: 0, held: 0, heldMissed: 0, audits: 0 });
|
|
289
|
+
const rows = {};
|
|
290
|
+
const total = empty();
|
|
291
|
+
const heldSoFar = new Map();
|
|
292
|
+
const held = [];
|
|
293
|
+
for (const c of cards) {
|
|
294
|
+
const d = route({ touches: c.touches, reverse: c.reverse, asked: c.asked, source: c.source, draw: 1, heldBefore: (row) => heldSoFar.get(row) ?? 0 }, settings);
|
|
295
|
+
const r = (rows[d.row] ??= empty());
|
|
296
|
+
for (const t of [r, total]) {
|
|
297
|
+
t.cards++;
|
|
298
|
+
if (d.outcome === "shown")
|
|
299
|
+
t.shown++;
|
|
300
|
+
else {
|
|
301
|
+
t.held++;
|
|
302
|
+
if (c.outcome.miss)
|
|
303
|
+
t.heldMissed++;
|
|
304
|
+
t.audits += d.burn_in ? 1 : d.p;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (d.outcome === "held") {
|
|
308
|
+
heldSoFar.set(d.row, (heldSoFar.get(d.row) ?? 0) + 1);
|
|
309
|
+
held.push(c.id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { rows, total, held };
|
|
313
|
+
}
|
|
314
|
+
/** The question every survey card asks, and its two answers. */
|
|
315
|
+
export const SURVEY_QUESTION = {
|
|
316
|
+
question: "Would you be fine not seeing a decision like this one, and checking a sample afterwards?",
|
|
317
|
+
see: "Show me ones like it",
|
|
318
|
+
fine: "Fine not seeing it",
|
|
319
|
+
};
|
|
320
|
+
export const SURVEY = [
|
|
321
|
+
{
|
|
322
|
+
row: "dependency",
|
|
323
|
+
title: "The date-parsing library",
|
|
324
|
+
question: "Which library should parse dates in the import step?",
|
|
325
|
+
doing: "writing the CSV import",
|
|
326
|
+
touches: ["dependency"],
|
|
327
|
+
reverse: "cheap",
|
|
328
|
+
proposal: "date-fns",
|
|
329
|
+
proposal_then: "a small runtime dependency is added to package.json",
|
|
330
|
+
alternative: "The platform's Intl API",
|
|
331
|
+
alternative_then: "no dependency, and a little more code to handle formats",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
row: "infra",
|
|
335
|
+
title: "Caching dependencies in CI",
|
|
336
|
+
question: "Should the CI workflow cache installed packages between runs?",
|
|
337
|
+
doing: "speeding up the test workflow",
|
|
338
|
+
touches: ["infra"],
|
|
339
|
+
reverse: "cheap",
|
|
340
|
+
proposal: "Cache them",
|
|
341
|
+
proposal_then: "the workflow restores packages from a cache keyed on the lockfile",
|
|
342
|
+
alternative: "Install fresh each run",
|
|
343
|
+
alternative_then: "runs stay slower, with nothing cached to go stale",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
row: "other",
|
|
347
|
+
title: "Retrying failed uploads",
|
|
348
|
+
question: "How should the uploader retry a failed upload?",
|
|
349
|
+
doing: "making uploads survive a flaky network",
|
|
350
|
+
touches: [],
|
|
351
|
+
reverse: "moderate",
|
|
352
|
+
proposal: "Exponential backoff, five tries",
|
|
353
|
+
proposal_then: "a failed upload is retried over about a minute, then reported",
|
|
354
|
+
alternative: "Fail at once",
|
|
355
|
+
alternative_then: "the user sees the failure straight away and retries by hand",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
row: "user_facing",
|
|
359
|
+
title: "The empty search page",
|
|
360
|
+
question: "What should search show when nothing matches?",
|
|
361
|
+
doing: "building the search page",
|
|
362
|
+
touches: ["user-facing"],
|
|
363
|
+
reverse: "cheap",
|
|
364
|
+
proposal: "Suggestions from recent searches",
|
|
365
|
+
proposal_then: "the empty page lists the user's last five searches",
|
|
366
|
+
alternative: "A plain message",
|
|
367
|
+
alternative_then: "the page says nothing matched, and nothing more",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
row: "schema",
|
|
371
|
+
title: "Storing tags on a run",
|
|
372
|
+
question: "How should a run's tags be stored?",
|
|
373
|
+
doing: "adding tags to runs",
|
|
374
|
+
touches: ["schema"],
|
|
375
|
+
reverse: "moderate",
|
|
376
|
+
proposal: "A tags table",
|
|
377
|
+
proposal_then: "a migration adds a table joining runs to tags",
|
|
378
|
+
alternative: "A JSON column on runs",
|
|
379
|
+
alternative_then: "a migration adds one column; tags can't be indexed",
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
row: "public_api",
|
|
383
|
+
title: "Paging the runs endpoint",
|
|
384
|
+
question: "How should the runs endpoint page its results?",
|
|
385
|
+
doing: "adding paging to the API",
|
|
386
|
+
touches: ["public-api"],
|
|
387
|
+
reverse: "moderate",
|
|
388
|
+
proposal: "Cursor tokens",
|
|
389
|
+
proposal_then: "clients pass an opaque cursor from the last page",
|
|
390
|
+
alternative: "Page numbers",
|
|
391
|
+
alternative_then: "clients pass page and size, and can jump to any page",
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
/**
|
|
395
|
+
* The preset closest to the survey's answers (B-5na1k): the one whose rows agree with the most answers, a "fine" agreeing
|
|
396
|
+
* with a row that holds cards back and a "see" with one that shows them. A tie goes to the stricter preset.
|
|
397
|
+
*/
|
|
398
|
+
export function suggestPreset(answers) {
|
|
399
|
+
const held = (p) => rank(p) < rank("tell-me");
|
|
400
|
+
let best = PRESETS[0];
|
|
401
|
+
let bestScore = -1;
|
|
402
|
+
for (const preset of PRESETS) {
|
|
403
|
+
const score = (answers.fine ?? []).filter((r) => held(preset.rows[r])).length + (answers.see ?? []).filter((r) => !held(preset.rows[r])).length;
|
|
404
|
+
if (score > bestScore) {
|
|
405
|
+
best = preset;
|
|
406
|
+
bestScore = score;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return best;
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=routing.js.map
|