phaedo-mcp 0.3.21 → 0.3.22
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/hooks/pre-ship.mjs +7 -1
- package/package.json +1 -1
- package/precedent.js +106 -3
- package/server.js +18 -2
- package/server.json +2 -2
- package/weekly.js +22 -2
package/hooks/pre-ship.mjs
CHANGED
|
@@ -506,6 +506,7 @@ export async function realDeps({ agentId = AGENT_ID, statePath } = {}) {
|
|
|
506
506
|
const { resolveConsultation, consultDrivers, withRevision, checkAuthorization } = await import('../consult.js');
|
|
507
507
|
const { buildReceipt, appendReceipt, receiptsCapFromEnv, readReceipts } = await import('../receipts.js');
|
|
508
508
|
const { sourceOf } = await import('../fingerprint-source.js');
|
|
509
|
+
const { precedentAdjustment } = await import('../precedent.js');
|
|
509
510
|
const { defaultStateDir } = await import('../cache.js');
|
|
510
511
|
const stateDir = defaultStateDir();
|
|
511
512
|
const quiet = () => {};
|
|
@@ -531,9 +532,14 @@ export async function realDeps({ agentId = AGENT_ID, statePath } = {}) {
|
|
|
531
532
|
async consult(request) {
|
|
532
533
|
const vd = await loadConsultVd(MCP_DIR, { preferCache: true, warn: quiet, onRead: (plain) => { readSource = sourceOf(plain); } });
|
|
533
534
|
consultedVd = vd;
|
|
534
|
-
|
|
535
|
+
let response = await resolveConsultation(vd, request, opts);
|
|
535
536
|
let drivers = [];
|
|
536
537
|
try { drivers = withRevision(consultDrivers(vd, request, opts), response); } catch { /* provenance is non-critical */ }
|
|
538
|
+
// Your past decisions in this situation move the confidence only (precedent.js Phase 3).
|
|
539
|
+
try {
|
|
540
|
+
const adj = precedentAdjustment(await readReceipts(stateDir), { request, drivers, response });
|
|
541
|
+
if (adj) { response = { ...response, confidence: adj.confidence }; drivers = [adj.driver, ...drivers]; }
|
|
542
|
+
} catch { /* precedent is non-critical */ }
|
|
537
543
|
// The plain read's provenance — `sourceOf(vd)` on the overlay always says "live", which is
|
|
538
544
|
// also why the 14-day refresh nudge below never fired before 0.3.17.
|
|
539
545
|
return { response, drivers, source: readSource };
|
package/package.json
CHANGED
package/precedent.js
CHANGED
|
@@ -6,8 +6,21 @@
|
|
|
6
6
|
// derived view that would say so — over the §10.6 receipt store, never a stored artifact.
|
|
7
7
|
//
|
|
8
8
|
// PURE: (receipts, opts) -> report. No I/O, `now` injected — the agreement.js discipline.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// Readout: `npm run precedent`.
|
|
10
|
+
//
|
|
11
|
+
// PHASE 3 (2026-11-03, Randy: "Yes, all four" — design doc §9 decisions 2–5). A consult now reads
|
|
12
|
+
// the precedent for its own situation, and it moves the answer's CONFIDENCE only:
|
|
13
|
+
// - the key is CONSULT_FIELDS: domain × kind × reversible × magnitude × amount band × binding.
|
|
14
|
+
// The amount bands are the subject's own edges; size/touched stay out until most receipts
|
|
15
|
+
// carry them (0009 Decided 1). On the reference store this groups exactly as the coarse key;
|
|
16
|
+
// - N = 5 within 180 days (the defaults below);
|
|
17
|
+
// - the SIGNAL never changes — not escalate → decisive, never anything → autoproceed (rail 4);
|
|
18
|
+
// - confidence is RAISED only once rejections are measured for the shape (MIN_SUPPORT marked
|
|
19
|
+
// decisions since "no" capture shipped, NO_CAPTURE_SINCE). Before that, "no" is undercounted
|
|
20
|
+
// and every shape looks agreeable, so precedent may only LOWER confidence (0009 Decided 3);
|
|
21
|
+
// - the counts reach NO agent. Disclosure is scoped to agents the subject allowed (decision 2),
|
|
22
|
+
// and in this version that set is empty — the counts stay on the device, in the receipt's
|
|
23
|
+
// `precedent` driver. Only the moved confidence crosses, a field every answer already has.
|
|
11
24
|
//
|
|
12
25
|
// The SHAPE is the situation key — banded features only, a closed vocabulary per field, so
|
|
13
26
|
// nothing a receipt holds on-device (summary, exact amount, counterparty, ids, previews) can
|
|
@@ -42,6 +55,17 @@ export const DEFAULT_WINDOW_DAYS = 180;
|
|
|
42
55
|
export const MIN_SUPPORT = 5;
|
|
43
56
|
export const BATCH_MIN = 3; // this many marks inside BATCH_WINDOW_MS = a burst
|
|
44
57
|
export const PREFLIGHT_VIA = 'phaedo_check_authorization';
|
|
58
|
+
// The key a consult matches on (Phase 3): every field but size/touched.
|
|
59
|
+
export const CONSULT_FIELDS = Object.freeze(['domain', 'kind', 'reversible', 'magnitude', 'amount_band', 'binding']);
|
|
60
|
+
// Claude Code's ship hook began recording a denied prompt here (phaedo-mcp 0.3.14). Before it,
|
|
61
|
+
// a "no" reached the store only if the agent remembered to record one.
|
|
62
|
+
export const NO_CAPTURE_SINCE = Date.parse('2026-10-27T00:00:00Z');
|
|
63
|
+
export const RECENT_MISS_DAYS = 30;
|
|
64
|
+
const RAISE_AT = 0.9; // this share of your outcomes agree with the answer → more sure
|
|
65
|
+
const LOWER_BELOW = 0.6; // under this share → less sure
|
|
66
|
+
const RAISE_BY = 0.1;
|
|
67
|
+
const LOWER_FACTOR = 0.75;
|
|
68
|
+
const CEILING = 0.95; // precedent alone never makes an inferred answer certain
|
|
45
69
|
|
|
46
70
|
const DAY = 24 * 60 * 60 * 1000;
|
|
47
71
|
const OUTCOMES = new Set(['approved', 'rejected', 'modified']);
|
|
@@ -173,6 +197,9 @@ export function precedentFor(receipts, shape, {
|
|
|
173
197
|
if (main.length < minSupport) return null;
|
|
174
198
|
const pre = match.filter(({ r }) => r.via === PREFLIGHT_VIA);
|
|
175
199
|
const { observed, user_actions, oracle_agreement, most_recent_divergence_days_ago } = tally(main, now);
|
|
200
|
+
// The bias disclosed beside the figure (0009 Decided 3): enough of these decisions postdate
|
|
201
|
+
// "no" capture that a rejection rate is measured, not missing.
|
|
202
|
+
const rejections_measured = main.filter(({ t }) => t >= NO_CAPTURE_SINCE).length >= minSupport;
|
|
176
203
|
return {
|
|
177
204
|
shape: projectShape(shape, fields),
|
|
178
205
|
shape_revision: SHAPE_REVISION,
|
|
@@ -181,10 +208,46 @@ export function precedentFor(receipts, shape, {
|
|
|
181
208
|
oracle_agreement,
|
|
182
209
|
window_days: windowDays,
|
|
183
210
|
most_recent_divergence_days_ago,
|
|
211
|
+
rejections_measured,
|
|
184
212
|
preflight: { observed: pre.length, user_actions: tally(pre, now).user_actions },
|
|
185
213
|
};
|
|
186
214
|
}
|
|
187
215
|
|
|
216
|
+
const round2 = (x) => Math.round(x * 100) / 100;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* How the subject's past decisions in this situation bear on a consult's answer (Phase 3).
|
|
220
|
+
* `request` is the consult request, `drivers` its provenance (the amount band rides there),
|
|
221
|
+
* `response` the answer after every guard. Returns null when precedent has nothing to say —
|
|
222
|
+
* an undecisive signal, no confidence, or under minimum support — else
|
|
223
|
+
* { confidence, effect: 'raised' | 'lowered' | 'held', driver }
|
|
224
|
+
* where `driver` is the on-device receipt entry: `precedent`, value `<effect>:<agreeing>/<observed>`.
|
|
225
|
+
* Never touches the signal (rail 4). Pure.
|
|
226
|
+
*/
|
|
227
|
+
export function precedentAdjustment(receipts, { request, drivers, response } = {}, opts = {}) {
|
|
228
|
+
const signal = response?.signal;
|
|
229
|
+
if (!DECISIVE.includes(signal) || typeof response.confidence !== 'number') return null;
|
|
230
|
+
const p = precedentFor(receipts, shapeOf({ request, drivers }), { ...opts, fields: CONSULT_FIELDS });
|
|
231
|
+
if (!p) return null;
|
|
232
|
+
const agreeing = signal === 'decline' ? p.user_actions.rejected : p.user_actions.approved;
|
|
233
|
+
const share = agreeing / p.observed;
|
|
234
|
+
const recentMiss = p.most_recent_divergence_days_ago !== null && p.most_recent_divergence_days_ago <= RECENT_MISS_DAYS;
|
|
235
|
+
let effect = 'held';
|
|
236
|
+
let confidence = response.confidence;
|
|
237
|
+
if (share < LOWER_BELOW || recentMiss) {
|
|
238
|
+
effect = 'lowered';
|
|
239
|
+
confidence = round2(confidence * LOWER_FACTOR);
|
|
240
|
+
} else if (share >= RAISE_AT && p.rejections_measured && confidence < CEILING) {
|
|
241
|
+
effect = 'raised';
|
|
242
|
+
confidence = round2(Math.min(CEILING, confidence + RAISE_BY));
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
confidence,
|
|
246
|
+
effect,
|
|
247
|
+
driver: { cue: 'precedent', value: `${effect}:${agreeing}/${p.observed}`, confidence: null, basis: 'observed' },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
188
251
|
/**
|
|
189
252
|
* The divergence episodes — the only thing 0009's principle says is worth keeping: receipts
|
|
190
253
|
* where a decisive prediction did not match what the subject did. Banded, newest first.
|
|
@@ -236,6 +299,46 @@ export function precedentTable(receipts, {
|
|
|
236
299
|
};
|
|
237
300
|
}
|
|
238
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Did precedent help? (Randy, 2026-11-05: "improve what we built".) Over the receipts it touched
|
|
304
|
+
* since `since`: how often it made an answer less sure, more sure or left it, and — among those
|
|
305
|
+
* the subject has since marked — how often that was right. Lowering was right when the subject
|
|
306
|
+
* decided against the answer; raising was right when they decided with it. Pure.
|
|
307
|
+
*/
|
|
308
|
+
export function precedentCheck(receipts, { now = Date.now(), since = 0 } = {}) {
|
|
309
|
+
const out = {
|
|
310
|
+
lowered: { n: 0, marked: 0, right: 0 },
|
|
311
|
+
raised: { n: 0, marked: 0, right: 0 },
|
|
312
|
+
held: { n: 0, marked: 0, right: 0 },
|
|
313
|
+
};
|
|
314
|
+
for (const r of Array.isArray(receipts) ? receipts : []) {
|
|
315
|
+
const t = Date.parse(r?.created_at);
|
|
316
|
+
if (!Number.isFinite(t) || t < since || t > now) continue;
|
|
317
|
+
const d = Array.isArray(r?.drivers) ? r.drivers.find((x) => x?.cue === 'precedent') : null;
|
|
318
|
+
const effect = typeof d?.value === 'string' ? d.value.split(':')[0] : null;
|
|
319
|
+
if (!out[effect]) continue;
|
|
320
|
+
out[effect].n++;
|
|
321
|
+
if (!OUTCOMES.has(r?.user_action) || !DECISIVE.includes(r?.response?.signal)) continue;
|
|
322
|
+
out[effect].marked++;
|
|
323
|
+
const sided = agrees(r.response.signal, r.user_action);
|
|
324
|
+
if (effect === 'lowered' ? !sided : sided) out[effect].right++;
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function formatCheck(c) {
|
|
330
|
+
const total = c.lowered.n + c.raised.n + c.held.n;
|
|
331
|
+
if (!total) return 'Your past decisions have not touched an answer yet.';
|
|
332
|
+
const line = (label, x, rightWord) => x.n
|
|
333
|
+
? ` ${label} ${x.n}× — ${x.marked ? `${x.right} of ${x.marked} you decided were ${rightWord}` : 'none decided yet'}`
|
|
334
|
+
: null;
|
|
335
|
+
return ['How your past decisions changed answers:',
|
|
336
|
+
line('Less sure', c.lowered, 'right to doubt'),
|
|
337
|
+
line('More sure', c.raised, 'right to trust'),
|
|
338
|
+
line('Unchanged', c.held, 'as answered'),
|
|
339
|
+
].filter(Boolean).join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
239
342
|
const show = (shape) => Object.entries(shape).filter(([, v]) => v !== null && v !== 'none' && v !== '(none)').map(([k, v]) => `${k}=${v}`).join(' ');
|
|
240
343
|
|
|
241
344
|
export function formatTable(t) {
|
|
@@ -263,5 +366,5 @@ if (isMainModule(import.meta.url)) {
|
|
|
263
366
|
const { defaultStateDir } = await import('./cache.js');
|
|
264
367
|
const receipts = await readReceipts(defaultStateDir()).catch(() => []);
|
|
265
368
|
const t = precedentTable(receipts, { windowDays: num('--days', DEFAULT_WINDOW_DAYS), minSupport: num('--min', MIN_SUPPORT), fields: fields.length ? fields : SHAPE_FIELDS });
|
|
266
|
-
process.stdout.write(formatTable(t) + '\n');
|
|
369
|
+
process.stdout.write(formatTable(t) + '\n\n' + formatCheck(precedentCheck(receipts)) + '\n');
|
|
267
370
|
}
|
package/server.js
CHANGED
|
@@ -32,6 +32,7 @@ import { resolveConsultation, checkAuthorization, standingRuleRelaxes, CONSULTAT
|
|
|
32
32
|
import { defaultStateDir, readCacheTime } from './cache.js';
|
|
33
33
|
import { ruleLeeway, RULE_FRESH_DAYS } from './lib/rule-leeway.js';
|
|
34
34
|
import { standingRuleDriver } from './weekly.js';
|
|
35
|
+
import { precedentAdjustment } from './precedent.js';
|
|
35
36
|
import { buildConsultOpts, loadConsultVd } from './consult-runtime.js';
|
|
36
37
|
import { buildReceipt, appendReceipt, receiptsCapFromEnv, readReceipts, markOutcome, USER_ACTIONS } from './receipts.js';
|
|
37
38
|
|
|
@@ -105,6 +106,19 @@ async function guardFacts(vd, req, ruleDecided = ruleRelaxes(vd, req)) {
|
|
|
105
106
|
|
|
106
107
|
// The receipt's note that one of the subject's standing rules let this go ahead (weekly.js counts
|
|
107
108
|
// it for the phone). First in the list, since buildReceipt keeps only the first 16 drivers.
|
|
109
|
+
// Situational precedent (precedent.js Phase 3): the subject's past decisions in this situation
|
|
110
|
+
// may move the answer's confidence, never its signal. The counts stay on the receipt (the
|
|
111
|
+
// `precedent` driver); no agent is allowed them yet. Best-effort: a failure leaves the answer as is.
|
|
112
|
+
async function withPrecedent(req, response, drivers) {
|
|
113
|
+
try {
|
|
114
|
+
const adj = precedentAdjustment(await readReceipts(defaultStateDir()), { request: req, drivers, response });
|
|
115
|
+
if (!adj) return { response, drivers };
|
|
116
|
+
return { response: { ...response, confidence: adj.confidence }, drivers: [adj.driver, ...drivers] };
|
|
117
|
+
} catch {
|
|
118
|
+
return { response, drivers };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
function ruleAnsweredDriver(vd, req, response) {
|
|
109
123
|
if (response?.signal !== 'proceed') return [];
|
|
110
124
|
try { return standingRuleDriver(checkAuthorization(vd, req, CONSULT_OPTS), response); } catch { return []; }
|
|
@@ -382,11 +396,12 @@ server.registerTool(
|
|
|
382
396
|
// profile with no downgrade and no provenance. That is the 2026-07-25 device defect (a
|
|
383
397
|
// revoked $500 grant honoured from a 22h cache) still live on the paths that matter most.
|
|
384
398
|
const raw = await resolveConsultation(vd, args || {}, CONSULT_OPTS);
|
|
385
|
-
|
|
399
|
+
let response = withStaleAuthorityGuard(raw, vd, await guardFacts(vd, args || {}));
|
|
386
400
|
// Provenance for the local audit receipt only — the cues that drove this
|
|
387
401
|
// (never in `response`; §10.4). Best-effort: a failure must not block the answer.
|
|
388
402
|
let drivers = [];
|
|
389
403
|
try { drivers = withRevision(consultDrivers(vd, args || {}, CONSULT_OPTS), raw); } catch { /* provenance is non-critical */ }
|
|
404
|
+
({ response, drivers } = await withPrecedent(args || {}, response, drivers));
|
|
390
405
|
drivers = [...ruleAnsweredDriver(vd, args || {}, response), ...drivers];
|
|
391
406
|
const receiptId = await emitReceipt('phaedo_consult', args, response, drivers); // every resolution, abstains included
|
|
392
407
|
if (receiptId) response.receipt_id = receiptId; // the handle for phaedo_record_outcome
|
|
@@ -600,9 +615,10 @@ server.registerTool(
|
|
|
600
615
|
// and then told the agent the signal was not blocking — i.e. "just proceed", and never
|
|
601
616
|
// wake the subject, on authority that may have been revoked hours earlier.
|
|
602
617
|
const raw = await resolveConsultation(vd, req, CONSULT_OPTS);
|
|
603
|
-
|
|
618
|
+
let response = withStaleAuthorityGuard(raw, vd, await guardFacts(vd, req));
|
|
604
619
|
let drivers = [];
|
|
605
620
|
try { drivers = withRevision(consultDrivers(vd, req, CONSULT_OPTS), raw); } catch { /* non-critical */ }
|
|
621
|
+
({ response, drivers } = await withPrecedent(req, response, drivers));
|
|
606
622
|
drivers = [...ruleAnsweredDriver(vd, req, response), ...drivers];
|
|
607
623
|
const escReceiptId = await emitReceipt('phaedo_escalate', req, response, drivers); // the decision being escalated
|
|
608
624
|
await maybeDepositReceiptsDigest(); // G1+G2 audit channel
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.phaedo-labs/phaedo-mcp",
|
|
4
4
|
"description": "Make AI apps answer your way, check with you before acting, and get approvals on your phone.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.22",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/phaedo-labs/phaedo-mcp",
|
|
8
8
|
"source": "github",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "phaedo-mcp",
|
|
16
|
-
"version": "0.3.
|
|
16
|
+
"version": "0.3.22",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|
package/weekly.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
// "Handled" is went_ahead + stopped. `by_rule` is the part of went_ahead that one of the subject's
|
|
14
14
|
// standing rules decided — the receipt carries a `standingRule` (server) or `standingGrant` (ship
|
|
15
15
|
// hook) driver saying so — which is how they see a rule they accepted actually being used.
|
|
16
|
+
// `less_sure` / `more_sure` count the answers the subject's own past decisions moved (precedent.js
|
|
17
|
+
// Phase 3: the receipt's `precedent` driver says `lowered:` / `raised:`), so they see that working
|
|
18
|
+
// too — without the counts themselves, which never leave this computer.
|
|
16
19
|
// Pure over the receipts list; the CLI reads the local store.
|
|
17
20
|
//
|
|
18
21
|
// npm run week
|
|
@@ -22,6 +25,7 @@ import { defaultStateDir } from './cache.js';
|
|
|
22
25
|
import { isMainModule } from './lib/is-main.js';
|
|
23
26
|
|
|
24
27
|
export const WEEK_DAYS = 7;
|
|
28
|
+
export const SUMMARY_MAX = 240; // mobile/src/lib/receiptsSync.ts parseWeek
|
|
25
29
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
26
30
|
const BUCKET = {
|
|
27
31
|
proceed: 'went_ahead', proceed_with_note: 'went_ahead',
|
|
@@ -45,15 +49,25 @@ export function standingRuleDriver(auth, response) {
|
|
|
45
49
|
? [{ cue: 'standingRule', value: 'answered', confidence: 1, basis: 'authored' }] : [];
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/** What the subject's past decisions did to this answer: 'lowered' | 'raised' | 'held' | null. */
|
|
53
|
+
export function precedentEffect(r) {
|
|
54
|
+
const d = Array.isArray(r?.drivers) ? r.drivers.find((x) => x?.cue === 'precedent') : null;
|
|
55
|
+
const effect = typeof d?.value === 'string' ? d.value.split(':')[0] : null;
|
|
56
|
+
return effect === 'lowered' || effect === 'raised' || effect === 'held' ? effect : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
export function weeklyHandled(receipts, { now = Date.now(), days = WEEK_DAYS } = {}) {
|
|
49
60
|
const since = now - days * DAY_MS;
|
|
50
|
-
const out = { days, went_ahead: 0, by_rule: 0, stopped: 0, came_to_you: 0, unsure: 0 };
|
|
61
|
+
const out = { days, went_ahead: 0, by_rule: 0, stopped: 0, came_to_you: 0, unsure: 0, less_sure: 0, more_sure: 0 };
|
|
51
62
|
for (const r of Array.isArray(receipts) ? receipts : []) {
|
|
52
63
|
const t = Date.parse(r?.created_at);
|
|
53
64
|
if (!Number.isFinite(t) || t < since || t > now) continue;
|
|
54
65
|
const b = BUCKET[r?.response?.signal];
|
|
55
66
|
if (b) out[b]++;
|
|
56
67
|
if (b === 'went_ahead' && answeredByRule(r)) out.by_rule++;
|
|
68
|
+
const p = precedentEffect(r);
|
|
69
|
+
if (p === 'lowered') out.less_sure++;
|
|
70
|
+
else if (p === 'raised') out.more_sure++;
|
|
57
71
|
}
|
|
58
72
|
out.handled = out.went_ahead + out.stopped;
|
|
59
73
|
out.total = out.handled + out.came_to_you + out.unsure;
|
|
@@ -61,6 +75,7 @@ export function weeklyHandled(receipts, { now = Date.now(), days = WEEK_DAYS } =
|
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
78
|
+
const times = (n) => (n === 1 ? 'once' : n === 2 ? 'twice' : `${n} times`);
|
|
64
79
|
|
|
65
80
|
/** One plain sentence (two at most) for a person. */
|
|
66
81
|
export function formatWeekly(w) {
|
|
@@ -74,7 +89,12 @@ export function formatWeekly(w) {
|
|
|
74
89
|
const asked = w.came_to_you ? ` and brought ${w.came_to_you} to you` : '';
|
|
75
90
|
const rule = w.by_rule ? ` Your standing rule decided ${w.handled === 1 ? 'it' : `${w.by_rule === 1 ? 'one' : w.by_rule} of them`}.` : '';
|
|
76
91
|
const unsure = w.unsure ? ` ${w.unsure === 1 ? 'One more was' : `${w.unsure} more were`} too unclear to call.` : '';
|
|
77
|
-
|
|
92
|
+
const sure = [w.less_sure ? `less sure ${times(w.less_sure)}` : '', w.more_sure ? `surer ${times(w.more_sure)}` : '']
|
|
93
|
+
.filter(Boolean).join(', ');
|
|
94
|
+
const past = sure ? ` Past choices made it ${sure}.` : '';
|
|
95
|
+
// The phone shows SUMMARY_MAX characters; this sentence is the one to drop rather than be cut.
|
|
96
|
+
const full = `${handled}${asked}.${rule}${past}${unsure}`;
|
|
97
|
+
return full.length <= SUMMARY_MAX ? full : `${handled}${asked}.${rule}${unsure}`;
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
if (isMainModule(import.meta.url)) {
|