cortad 0.3.1-rc.9 → 0.3.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/README.md +8 -5
- package/lib/app-port.mjs +9 -0
- package/lib/auto-reach.mjs +88 -0
- package/lib/backing.mjs +230 -0
- package/lib/calls.mjs +23 -0
- package/lib/cli.mjs +115 -30
- package/lib/contact-text.mjs +167 -0
- package/lib/data.mjs +16 -22
- package/lib/dissection-text.mjs +232 -0
- package/lib/document.mjs +75 -0
- package/lib/env-secrets.mjs +48 -0
- package/lib/failed-text.mjs +36 -0
- package/lib/fresh.mjs +33 -12
- package/lib/health.mjs +61 -0
- package/lib/held.mjs +67 -0
- package/lib/home.mjs +55 -17
- package/lib/hosted.mjs +201 -0
- package/lib/identities.mjs +1 -1
- package/lib/keeping.mjs +162 -0
- package/lib/ledger.mjs +20 -0
- package/lib/lock.mjs +6 -1
- package/lib/mcp.mjs +52 -18
- package/lib/mint.mjs +179 -38
- package/lib/mongo.mjs +74 -0
- package/lib/numbers-text.mjs +68 -0
- package/lib/pg.mjs +118 -0
- package/lib/proc.mjs +100 -0
- package/lib/proof.mjs +1014 -0
- package/lib/proxy.mjs +184 -0
- package/lib/pyhook/cortad_sites.py +218 -0
- package/lib/pyhook/cortad_tied.py +291 -0
- package/lib/pyhook/sitecustomize.py +1175 -166
- package/lib/qdrant.mjs +47 -0
- package/lib/reach.mjs +223 -0
- package/lib/read-text.mjs +78 -24
- package/lib/redis.mjs +130 -0
- package/lib/register.mjs +36 -27
- package/lib/replay.mjs +464 -34
- package/lib/runner.mjs +85 -0
- package/lib/service.mjs +25 -0
- package/lib/spec.mjs +7 -4
- package/lib/start.mjs +33 -2
- package/lib/stick.mjs +65 -25
- package/lib/stores.mjs +347 -0
- package/lib/switches.mjs +36 -0
- package/lib/tether.mjs +9 -0
- package/lib/text.mjs +314 -179
- package/lib/tied.cjs +217 -0
- package/lib/trace.cjs +386 -514
- package/lib/verbs.mjs +155 -38
- package/lib/wire.cjs +882 -0
- package/lib/words.mjs +2 -0
- package/lib/writes.mjs +2 -2
- package/local.mjs +661 -297
- package/package.json +31 -1
- package/skill/SKILL.md +49 -42
- package/skill/references/results.md +279 -102
package/lib/text.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { contactLines } from "./contact-text.mjs";
|
|
2
|
+
import { costSaid, dissectionLines, verifyOnceAPage } from "./dissection-text.mjs";
|
|
3
|
+
import { failedLines, heldLines } from "./failed-text.mjs";
|
|
1
4
|
import { readLines } from "./read-text.mjs";
|
|
2
5
|
import { at, clip, has, num, PAGE_CHARS, pageOf, plural, SIDE, upper } from "./words.mjs";
|
|
3
6
|
|
|
@@ -6,71 +9,121 @@ import { at, clip, has, num, PAGE_CHARS, pageOf, plural, SIDE, upper } from "./w
|
|
|
6
9
|
// out prints nothing, so the text follows the API as it grows.
|
|
7
10
|
|
|
8
11
|
const DONE = new Set(["succeeded", "failed", "canceled"]);
|
|
9
|
-
const UNIT = { sweeps: ["run", "runs"], runs: ["run", "runs"], trials: ["verify
|
|
12
|
+
const UNIT = { sweeps: ["run", "runs"], runs: ["run", "runs"], trials: ["verify conversation", "verify conversations"] };
|
|
10
13
|
export const STARTING = "Starting your app for the run. Call run_status; it answers as soon as the run has an id.";
|
|
14
|
+
// The runner's own record (lib/runner.mjs): an app it could not start, and one that answers while
|
|
15
|
+
// the server cannot play it yet.
|
|
16
|
+
export const notStartedText = (r) => [upper(r.error), r.said ? `Its last lines:\n${r.said}` : null, "Nothing ran."].filter(Boolean).join("\n");
|
|
17
|
+
// The server's reason it cannot play an app the runner has up: what it has found of the app's AI
|
|
18
|
+
// endpoints, or the request it asks for. It names the port it saw, so it stands in for the runner's
|
|
19
|
+
// line rather than beside a copy of it. The server's other words on an app it cannot play yet ("Your
|
|
20
|
+
// app is starting.") are its guess at what the runner already knows, and are not said.
|
|
21
|
+
const doorsSaid = (r, app) => (app && app.state !== "ready-to-test" && app.port === r.port && app.said) || null;
|
|
22
|
+
export const answeringText = (r, app) => doorsSaid(r, app) ?? `Your app answers on port ${r.port}.`;
|
|
23
|
+
export const upWaitingText = (r, app) => `${answeringText(r, app)}\nCall run_status; it answers as soon as the run has an id.`;
|
|
11
24
|
|
|
12
|
-
const signed = (x) => (Math.round(x) > 0 ? `+${num(x)}` : num(x));
|
|
13
25
|
const units = (n, unit) => { const [one, many] = UNIT[unit] ?? [unit, unit]; return `${num(n)} ${n === 1 ? one : many}`; };
|
|
14
26
|
const kn = (x) => (has(x?.k) ? `${num(x.k)} of ${num(x.n)}` : num(x));
|
|
15
|
-
const interval = (ci) => (ci && has(ci.low) ? `, interval ${num(ci.low)} to ${num(ci.high)}` : "");
|
|
16
27
|
|
|
17
28
|
export const finished = (run) => Boolean(run) && (run.finished ?? DONE.has(run.status));
|
|
18
29
|
|
|
19
30
|
export function statusText(d) {
|
|
20
31
|
const lines = [];
|
|
21
|
-
if (d.repository?.name) lines.push(`
|
|
32
|
+
if (d.repository?.name) lines.push(`Repository: ${d.repository.name}.`);
|
|
33
|
+
// What changed since the last status call, first: the read landing, endpoints proven, a run.
|
|
34
|
+
if (d.news?.length) lines.push("New since your last call:", ...d.news.map((n) => ` ${n}`));
|
|
22
35
|
const plan = planLine(d.plan);
|
|
23
36
|
if (plan) lines.push(plan);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
const receipts = receiptsSaid(d.runner?.receipts, d.contact);
|
|
38
|
+
lines.push(...appLines(d, receipts));
|
|
39
|
+
lines.push(...contactLines(d.contact, d.app?.data, receipts));
|
|
40
|
+
// While the read runs its lists are zeros ("Journeys (0)", "Questions: 0; 0 asked..."): it is said
|
|
41
|
+
// once, and the lists come when it lands (the server's news says so).
|
|
42
|
+
// The quote says it when it is said (it names the read with what the run does about it).
|
|
43
|
+
if (d.read?.complete === false) { if (!d.contact?.ready?.reading || !d.contact.ready.trials) lines.push("Your code is still being read."); }
|
|
44
|
+
else if (d.read) lines.push(...readLines(d.read));
|
|
45
|
+
if (d.run) lines.push(...runLines(d.run, "latest "));
|
|
30
46
|
// Trials the read wrote after this run chose its own: the one count that is not the run's.
|
|
31
47
|
const since = (d.read?.trials?.written ?? 0) - (d.run?.trials?.written ?? Infinity);
|
|
32
|
-
if (since > 0 && d.run?.kind !== "verify") lines.push(`${plural(since, "more
|
|
33
|
-
|
|
48
|
+
if (since > 0 && d.run?.kind !== "verify") lines.push(`${plural(since, "more conversation")} written since this run; the next run chooses from them too.`);
|
|
49
|
+
// Production comes up after the first run (skill: The second run and production).
|
|
50
|
+
if (d.field && d.run) lines.push(`Production: ${d.field.connected ? "connected" : "not connected"}.`);
|
|
51
|
+
if (d.next) lines.push(d.next);
|
|
34
52
|
if (d.links?.lab && (d.read || d.run)) lines.push(`For the person: ${d.links.lab} shows this in the browser.`);
|
|
35
53
|
if (d.run && !finished(d.run)) lines.push(`next: run_status ${d.run.jobId}`);
|
|
36
54
|
return lines.join("\n");
|
|
37
55
|
}
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
// A request to `door` ("POST /chat") was answered in words and no model call came through the local proxy.
|
|
58
|
+
export const unseenText = (door) => `A request to ${door} was answered in words and no model call came through the proxy. If that endpoint uses a model, your app's model calls do not go through OPENAI_BASE_URL (or ANTHROPIC_BASE_URL or GOOGLE_GEMINI_BASE_URL): have it take its model's address from that setting, then save.`;
|
|
59
|
+
// A request to `door` the app answered at once with no model call inside it (lib/replay.mjs): its
|
|
60
|
+
// answer may come from a part of the app Cortad did not start, or from a model call Cortad cannot tie
|
|
61
|
+
// to it, or there is no AI behind it. `followed`: a model call came after it that carried none of its
|
|
62
|
+
// words.
|
|
63
|
+
export const receiptText = (door, followed) => (followed
|
|
64
|
+
? `${door} answered your request at once, with no model call inside it; a model call came after it, but its prompt carried none of the words your request sent, so Cortad cannot tell it was this request's answer. That gap is on Cortad's side, and a run leaves this endpoint out.\nFor the person: ${door} answers later in a way Cortad cannot match to the message that asked, so a run leaves it out for now; that gap is on Cortad's side.`
|
|
65
|
+
: `${door} answered your request at once with no model call inside it, and no model call followed in any process Cortad started. If a worker in another process writes its answer, Cortad does not see that worker, so a run leaves this endpoint out for now; that gap is on Cortad's side. If this endpoint has no AI behind it, nothing is missing.\nFor the person: ${door} answered with no AI call Cortad could see; if its AI runs in a separate worker, a run leaves it out for now, and that gap is on Cortad's side.`);
|
|
66
|
+
// Only a door the read lists as one no request has reached, and no request has proven since: a sign-up
|
|
67
|
+
// or a guard's reply to a request a later one proved is no worker's answer.
|
|
68
|
+
const receiptsSaid = (receipts = [], c) => {
|
|
69
|
+
const proven = new Set((c?.proven ?? []).map((v) => v.door));
|
|
70
|
+
const unreached = new Set((c?.notCalled ?? []).map((m) => `${m.method} ${m.path}`));
|
|
71
|
+
return receipts.filter((x) => unreached.has(x.door) && !proven.has(x.door));
|
|
72
|
+
};
|
|
73
|
+
// An app seen through the local proxy (lib/proxy.mjs): where to send, and what is not known.
|
|
74
|
+
const proxiedLine = (r) => `Model calls are seen through a local proxy, so the line of code that made each call is not known. Send requests to port ${r.port}; requests sent straight to your app on port ${r.proxied} are not seen.${r.unseen ? ` ${unseenText(r.unseen)}` : ""}`;
|
|
75
|
+
|
|
76
|
+
// The app as the runner holding it up on this machine says it is. The server's word stands only
|
|
77
|
+
// where no runner is here, and beside it when the server is asking for a request at the app's door.
|
|
78
|
+
function appLines(d, receipts) {
|
|
79
|
+
const r = d.runner;
|
|
80
|
+
if (!r) return d.app?.said ? [`App: ${d.app.said}`] : [];
|
|
81
|
+
if (r.state === "up") return [`App: ${doorsSaid(r, d.app) ?? `answering on port ${r.port}.`}`, ...(r.proxied ? [proxiedLine(r)] : []), ...receipts.map((x) => receiptText(x.door, x.followed))];
|
|
82
|
+
return [r.state === "failed" ? `App: ${r.error}` : "App: starting."];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// What this month has used of the plan, and what the plan above it costs and buys, in the numbers
|
|
86
|
+
// checkout sells.
|
|
87
|
+
export function planLine(p) {
|
|
40
88
|
if (!p?.name) return null;
|
|
41
89
|
const parts = [];
|
|
42
|
-
if (has(p.runsLeft) && has(p.runsAllowed)) parts.push(
|
|
43
|
-
if (has(p.verifyTrialsLeft)
|
|
44
|
-
|
|
90
|
+
if (has(p.runsLeft) && has(p.runsAllowed)) parts.push(runsSaid(p.runsLeft, p.runsAllowed));
|
|
91
|
+
if (has(p.verifyTrialsLeft)) parts.push(`${plural(p.verifyTrialsLeft, "verify conversation")} left`);
|
|
92
|
+
const head = parts.length ? `${p.name}: ${parts.join("; ")}.` : `Plan: ${p.name}.`;
|
|
93
|
+
const n = p.next;
|
|
94
|
+
return n ? `${head} ${n.name} $${num(n.monthlyUsd)} a month: runs and reruns included, ${num(n.productionReplies)} production replies read.` : head;
|
|
45
95
|
}
|
|
46
96
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
97
|
+
const runsSaid = (left, allowed) => (left > 0 ? `${num(left)} of ${plural(allowed, "run")} left this month`
|
|
98
|
+
: allowed === 1 ? "this month's 1 run is used" : `all ${plural(allowed, "run")} of this month are used`);
|
|
99
|
+
|
|
100
|
+
// After the findings: what was measured of the app's own promises, in its own words, then the checks
|
|
101
|
+
// measured and why the rest were not, then how many trials kept every promise. Counts, never a score.
|
|
102
|
+
// The replies that did not count sit before the count: a clean count beside 62 error replies read as a clean bill.
|
|
51
103
|
function measuredLines(d) {
|
|
104
|
+
const lines = [...promiseLines(d.promises), ...failedLines(d.failedReplies)];
|
|
52
105
|
const of = has(d.questionsAsked) && has(d.questionsOf) && d.questionsOf > 0;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const groups = (d.notMeasured?.groups ?? []).filter((g) => g.count > 0 && g.said);
|
|
56
|
-
if (groups.length) lines.push(`${
|
|
106
|
+
if (of) lines.push(`${num(d.questionsAsked)} of ${plural(d.questionsOf, "check")} measured.`);
|
|
107
|
+
// The dissection says what was not measured in its own line.
|
|
108
|
+
const groups = d.dissection ? [] : (d.notMeasured?.groups ?? []).filter((g) => g.count > 0 && g.said);
|
|
109
|
+
if (groups.length) lines.push(`${plural(d.notMeasured.total, "check")} with no reply to read: ${groups.map((g) => clip(g.said, 240)).join("; ")}.`);
|
|
110
|
+
// An answer from before failedReplies says its failures here, never folded into a number.
|
|
111
|
+
if (!d.failedReplies && d.replies?.crashed > 0) lines.push(`${num(d.replies.crashed)} of ${plural(d.replies.total, "reply", "replies")} were failures and are not counted.`);
|
|
112
|
+
// The server's own sentence already says the count.
|
|
113
|
+
if (d.promises && !d.promises.said && has(d.promises.decided)) lines.push(promiseLine(d));
|
|
57
114
|
return lines;
|
|
58
115
|
}
|
|
59
116
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// 89 error replies unsaid, read as a clean bill.
|
|
64
|
-
const crashed = d.replies && d.replies.crashed > 0 ? `; ${num(d.replies.crashed)} of ${plural(d.replies.total, "reply", "replies")} were failures and are not in it` : "";
|
|
65
|
-
return `${floor ? "provisional score" : "score"} ${num(d.score)} of 100${of ? ` on those ${num(d.questionsAsked)}` : ""}${interval(d.ci)}${floor ? `, from ${plural(d.decided, "decided reading")}: under the 22-reading floor, so not a verdict yet` : ""}${crashed}`;
|
|
117
|
+
function promiseLines(p) {
|
|
118
|
+
if (!p?.said) return [];
|
|
119
|
+
return [p.said, ...(p.measured ?? []).map((m) => ` "${clip(m.words, 200)}"${m.at ? ` (${m.at})` : ""}: failed in ${num(m.failed)} of ${plural(m.of, "conversation")}.`)];
|
|
66
120
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const haltLine = (h) => `${h.side === "ours" ? `Stopped ${SIDE.ours}` : "Your app stopped answering"} after ${plural(h.after, "turn")}${h.why ? `: ${clip(h.why, 240)}` : ""}. The score covers the turns before that.`;
|
|
121
|
+
|
|
122
|
+
const promiseLine = (d) => `${upper(`${num(d.promises.held)} of the ${plural(d.promises.decided, "conversation")}`)} tested against your app's own rules broke none.`;
|
|
123
|
+
|
|
124
|
+
const haltLine = (h) => `${h.side === "ours" ? `Stopped ${SIDE.ours}` : "Your app stopped answering"} after ${plural(h.after, "reply", "replies")}${h.why ? `: ${clip(h.why, 240)}` : ""}. The numbers cover the replies before that.`;
|
|
72
125
|
// The pace a slow app was played at, and the replies our own wait cut off: both said as ours. A
|
|
73
|
-
// verify says the second
|
|
126
|
+
// verify says the second among the replays it could not read.
|
|
74
127
|
const waitLines = (d) => [d.pace?.said, d.kind === "verify" ? null : d.waited?.said].filter(Boolean);
|
|
75
128
|
const LAYER_WORDS = {
|
|
76
129
|
retrieval: "what the store returned behind the reply, not how the reply was written",
|
|
@@ -83,81 +136,81 @@ const LAYER_WORDS = {
|
|
|
83
136
|
|
|
84
137
|
function runLines(d, prefix = "") {
|
|
85
138
|
const done = finished(d);
|
|
86
|
-
const played = d.of ? `${num(d.played ?? 0)} of ${plural(d.of, "
|
|
87
|
-
|
|
139
|
+
const played = d.of ? `${num(d.played ?? 0)} of ${plural(d.of, "conversation")} played` : "no conversation played yet";
|
|
140
|
+
// A run is finished once its numbers land, while its job may still be writing the Lab's report.
|
|
141
|
+
const state = d.status === "failed" || d.status === "canceled" ? d.status : done ? "finished" : d.status && d.status !== "succeeded" ? d.status : "running";
|
|
142
|
+
const lines = [`${upper(`${prefix}${d.kind === "verify" ? "verify" : "run"}`)} ${d.jobId}: ${state}, ${played}.`];
|
|
88
143
|
if (d.tested) lines.push(testedLine(d.tested));
|
|
144
|
+
// What a run after a change plays and why; a finished one says it inside its dissection.
|
|
145
|
+
if (d.scope && !d.dissection?.change) lines.push(d.scope);
|
|
146
|
+
// Each endpoint's first conversation, played alone before the others: whether it held and why not.
|
|
147
|
+
lines.push(...(d.first ?? []));
|
|
89
148
|
// Between two counts, what the run is doing now: four minutes of "0 of 15" read as nothing happening.
|
|
90
149
|
if (!done && d.now?.doing) lines.push(d.now.doing);
|
|
91
150
|
lines.push(...waitLines(d));
|
|
92
|
-
// A
|
|
93
|
-
|
|
94
|
-
|
|
151
|
+
// A field the app sets for its model that went out as the agent's own request had it.
|
|
152
|
+
lines.push(...(d.recordedValues ?? []));
|
|
153
|
+
// A finished run leads with its dissection: what only running the app showed, then what a code
|
|
154
|
+
// review would also find, then what was measured of the app's promises. Status and an answer from
|
|
155
|
+
// before the dissection say the run's counts. A verify is a move and says only the move.
|
|
156
|
+
if (done && d.kind !== "verify" && d.dissection) {
|
|
157
|
+
lines.push(...dissectionLines(d.dissection));
|
|
158
|
+
if (has(d.findings)) lines.push(findingsCount(d.findings, false));
|
|
159
|
+
lines.push(...measuredLines(d));
|
|
160
|
+
} else if (done && d.kind !== "verify") {
|
|
161
|
+
if (d.top) lines.push("Top finding:", findingBlock(d.top));
|
|
162
|
+
if (has(d.findings)) lines.push(findingsCount(d.findings, Boolean(d.top)));
|
|
163
|
+
if (d.model) lines.push(`Your app answered on ${d.model}, as its own model calls show.`);
|
|
164
|
+
const cost = costSaid(d);
|
|
165
|
+
if (cost) lines.push(cost);
|
|
166
|
+
lines.push(...measuredLines(d));
|
|
167
|
+
}
|
|
95
168
|
if (done && d.blocked) lines.push(d.blocked);
|
|
96
169
|
if (done && d.notCounted) lines.push(d.notCounted);
|
|
97
170
|
if (done && d.halted) lines.push(haltLine(d.halted));
|
|
98
|
-
if (done && has(d.readingsHeld) && has(d.readings) && d.readings > 0) lines.push(`The question readings held in ${num(d.readingsHeld)} of ${num(d.readings)}, ${Math.round((100 * d.readingsHeld) / d.readings)}%; the score is the case checks' pass rate on the checks measured.`);
|
|
99
|
-
if (done && has(d.findings)) lines.push(`${plural(d.findings, "finding")}.`);
|
|
100
171
|
const trials = trialsLine(d);
|
|
101
172
|
if (trials) lines.push(trials);
|
|
102
|
-
// Every row, or six and the rest in one line, so the rows always sum to the held-back count.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (heldRows.length > 6) lines.push(`Held back: ${plural(heldRows.slice(6).reduce((n, h) => n + h.trials, 0), "more trial")} for ${plural(heldRows.length - 6, "other reason")}.`);
|
|
106
|
-
const reads = readingsLine(d);
|
|
107
|
-
if (reads) lines.push(reads);
|
|
173
|
+
// Every row, or six and the rest in one line, so the rows always sum to the held-back count. A
|
|
174
|
+
// finished run says them with the replies that did not count, above the counts.
|
|
175
|
+
if (!(done && (d.failedReplies || d.dissection))) lines.push(...heldLines(d.heldBack));
|
|
108
176
|
if (d.stopped) lines.push(stoppedLine(d));
|
|
109
177
|
for (const f of d.faults ?? []) lines.push(`Fault${SIDE[f.side] ? ` ${SIDE[f.side]}` : ""}: ${f.what}${f.fix ? ` ${f.fix}` : ""}`);
|
|
110
178
|
for (const s of d.data ?? []) lines.push(`Data: ${s}`);
|
|
111
|
-
|
|
179
|
+
// A run the stop line already explains is not said twice as an error.
|
|
180
|
+
if (d.error && !(d.stopped && d.error.startsWith(d.stopped.why))) lines.push(`Error: ${d.error}`);
|
|
112
181
|
if (d.verify) lines.push(...verifyLines(d.verify));
|
|
113
182
|
return lines;
|
|
114
183
|
}
|
|
115
184
|
|
|
185
|
+
// Settled: failed in enough trials to say it fails at least one visit in five; findings lists those first.
|
|
186
|
+
const findingsCount = (n, shown) => `${shown ? `${plural(n, "finding")} in all` : plural(n, "finding")}.`;
|
|
187
|
+
|
|
116
188
|
// The run's trials in one sentence that adds up: written = chosen + held back + not planned, and
|
|
117
189
|
// chosen = played + not played. Three counts from three places never met before.
|
|
118
190
|
function trialsLine(d) {
|
|
119
191
|
const t = d.trials;
|
|
120
192
|
// A verify replays the trials it names, so it has no written count of its own to add up.
|
|
121
193
|
if (!t || !has(t.written) || d.kind === "verify") return null;
|
|
122
|
-
const parts = [`${num(t.chosen)} chosen for this run`, t.heldBack && `${num(t.heldBack)}
|
|
194
|
+
const parts = [`${num(t.chosen)} chosen for this run`, t.heldBack && `${num(t.heldBack)} set aside`, t.notPlanned && `${num(t.notPlanned)} not chosen this time`].filter(Boolean);
|
|
123
195
|
const rest = t.notPlayed ? `; ${num(t.played)} played, ${num(t.notPlayed)} ${finished(d) ? "not played" : "still to play"}` : "";
|
|
124
|
-
return `
|
|
196
|
+
return `Conversations: ${num(t.written)} written: ${parts.join(", ")}${rest}.`;
|
|
125
197
|
}
|
|
126
198
|
|
|
127
|
-
// A
|
|
128
|
-
|
|
129
|
-
if (!has(d.readings) && !has(d.questionsAsked)) return null;
|
|
130
|
-
const r = has(d.readings) ? plural(d.readings, "reading") : null;
|
|
131
|
-
// The line above says why the rest were not measured; without it, how many there are.
|
|
132
|
-
const never = !d.notMeasured && has(d.questionsOf) && d.questionsOf > d.questionsAsked;
|
|
133
|
-
const q = has(d.questionsAsked) ? `${plural(d.questionsAsked, "question")}${never ? ` (${num(d.questionsOf - d.questionsAsked)} of the ${num(d.questionsOf)} in the set never came up)` : ""}` : null;
|
|
134
|
-
const split = [has(d.decided) && `${num(d.decided)} decided`, has(d.unclear) && `${num(d.unclear)} unclear`].filter(Boolean).join(", ");
|
|
135
|
-
return `${r && q ? `${r} of ${q}` : r ?? `${q} asked`}${split ? `: ${split}` : ""}.`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// A run that stopped before its last trial says where; one that stopped after it is a note.
|
|
199
|
+
// A run that stopped before its last trial says where; one that stopped after it is a note. A stop
|
|
200
|
+
// before the first turn has no turn to name, and a reason written as a sentence keeps one full stop.
|
|
139
201
|
function stoppedLine(d) {
|
|
140
202
|
const s = d.stopped;
|
|
141
|
-
|
|
203
|
+
// A stop before the trials, on the endpoints' first conversations, names each one instead of a count.
|
|
204
|
+
// `after` counts the replies of the whole run, never one conversation's turn.
|
|
205
|
+
const turn = s.after > 0 && !s.first ? ` after ${plural(s.after, "reply", "replies")}` : "";
|
|
206
|
+
const why = String(s.why).replace(/\.$/, "");
|
|
142
207
|
const fix = s.fix ? ` ${s.fix}` : "";
|
|
143
|
-
if (d.of && d.played < d.of) return `Stopped at ${num(d.played)} of ${plural(d.of, "
|
|
144
|
-
return `${upper(
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const moveText = (m) => `held ${kn(m.before)} readings before, ${kn(m.after)} after; ${m.moved}, ${signed(m.point)} points${interval(m)}${m.insideNoise ? ", inside the noise" : ""}.`;
|
|
148
|
-
|
|
149
|
-
// Where the replies did not pair, the question's rate on the same trials before and after, with the
|
|
150
|
-
// trials the verify played new in the situation and the readings nobody could settle said apart.
|
|
151
|
-
function rateText(r) {
|
|
152
|
-
const unclear = [r.before.unclear && `${num(r.before.unclear)} unclear before`, r.after.unclear && `${num(r.after.unclear)} unclear after`].filter(Boolean).map((u) => `, ${u}`).join("");
|
|
153
|
-
const over = r.trials?.widened ? `, over ${plural(r.trials.replayed, "trial")} replayed and ${num(r.trials.widened)} new` : "";
|
|
154
|
-
const verdict = r.thin ? `too few to state a rate (${num(r.after.settled)} of the 22 it needs)` : `${r.moved}, ${signed(r.point)} points${interval(r)}${r.insideNoise ? ", inside the noise" : ""}`;
|
|
155
|
-
return `failed on ${num(r.before.failed)} of ${num(r.before.settled)} settled readings before, ${num(r.after.failed)} of ${num(r.after.settled)} after${unclear}${over}; ${verdict}.`;
|
|
208
|
+
if (d.of && d.played < d.of) return `Stopped at ${num(d.played)} of ${plural(d.of, "conversation")}${SIDE[s.side] ? `, ${SIDE[s.side]}` : ""}: ${why}${turn}.${fix}`;
|
|
209
|
+
return `${upper(why)}${turn}${SIDE[s.side] ? `, ${SIDE[s.side]}` : ""}.${fix}`;
|
|
156
210
|
}
|
|
157
|
-
const sideText = (m) => (m.rate ? rateText(m.rate) : m.unavailable ? null : moveText(m));
|
|
158
211
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
212
|
+
// A verify is its verdict on the finding's own trials, in the server's words: what came back broken,
|
|
213
|
+
// gone, less often, stayed or cannot tell yet with the counts behind it, and the held-out line.
|
|
161
214
|
function verifyLines(v) {
|
|
162
215
|
// An access verify is a re-knock, not a trial move: the routes side by side, then the count. Only
|
|
163
216
|
// a route knocked and answered both times is compared; one the app no longer has or did not answer
|
|
@@ -165,25 +218,20 @@ function verifyLines(v) {
|
|
|
165
218
|
if (v.access) {
|
|
166
219
|
const lines = [`Verify of ${v.findingId}.`];
|
|
167
220
|
for (const r of v.routes ?? []) {
|
|
168
|
-
|
|
221
|
+
// A 2xx handed the data over; a 400, 404 or 422 reached the handler's own checks with no sign-in asked.
|
|
222
|
+
const open = (status, when) => (status >= 200 && status < 300 ? `${when === "then" ? "answered" : "now answers"} ${status} with no sign-in` : `${when === "then" ? "reached its own checks" : "now reaches its own checks"} with no sign-in (HTTP ${status})`);
|
|
223
|
+
const then = r.wasOpen ? open(r.before, "then") : `refused (${r.before})`;
|
|
169
224
|
const now =
|
|
170
|
-
r.state === "answered" ? (r.nowOpen ?
|
|
225
|
+
r.state === "answered" ? (r.nowOpen ? open(r.after, "now") : `now refuses (${r.after})`)
|
|
171
226
|
: r.state === "gone" ? "your app no longer has this route"
|
|
172
227
|
: r.state === "no-answer" ? "your app did not answer this time"
|
|
173
|
-
: "was not
|
|
228
|
+
: "was not sent again";
|
|
174
229
|
lines.push(` ${r.method} ${r.route}: ${then}, ${now}.`);
|
|
175
230
|
}
|
|
176
231
|
if (v.said) lines.push(v.said);
|
|
177
232
|
return lines;
|
|
178
233
|
}
|
|
179
|
-
|
|
180
|
-
const visible = v.visible && sideText(v.visible);
|
|
181
|
-
if (visible) lines.push(`Visible trials: ${visible}`);
|
|
182
|
-
if (v.holdout) lines.push(`Held-out trials: ${sideText(v.holdout) ?? `verification unavailable: ${v.holdout.unavailable}.`}`);
|
|
183
|
-
else if (v.holdout === null) lines.push("Held-out trials: no pair for this question.");
|
|
184
|
-
if (v.overfit) lines.push("Overfit: the visible trials moved and the held-out trials did not.");
|
|
185
|
-
if (v.said) lines.push(v.said);
|
|
186
|
-
return lines;
|
|
234
|
+
return [`Verify of ${v.findingId}${v.file ? ` at ${at(v.file, v.line)}` : ""}.`, ...(v.said ? [v.said] : [])];
|
|
187
235
|
}
|
|
188
236
|
|
|
189
237
|
export function nextCall(d) {
|
|
@@ -191,11 +239,43 @@ export function nextCall(d) {
|
|
|
191
239
|
return d.kind === "verify" || d.findings > 0 ? "findings" : "status";
|
|
192
240
|
}
|
|
193
241
|
|
|
242
|
+
// One line for the person while a run plays: conversations done of the total, whose side any failed
|
|
243
|
+
// reply is on, and the minutes left at the pace measured so far. Cost is measured only at the end.
|
|
244
|
+
export function progressLine(d, now = Date.now()) {
|
|
245
|
+
if (!d.of) return "For the person: the run has started and is choosing its conversations; none has played yet.";
|
|
246
|
+
const parts = [`${num(d.played ?? 0)} of ${plural(d.of, "conversation")} done.`];
|
|
247
|
+
const total = d.replies?.total ?? d.waited?.of;
|
|
248
|
+
const crashed = d.replies?.crashed ?? 0;
|
|
249
|
+
const ours = d.replies?.ours ?? d.waited?.replies ?? 0;
|
|
250
|
+
const refused = d.replies?.refused ?? 0;
|
|
251
|
+
if (total > 0 && !crashed && !ours && !refused) parts.push("No reply has failed so far.");
|
|
252
|
+
if (total > 0 && crashed) parts.push(`${num(crashed)} of ${plural(total, "reply", "replies")} so far were errors from your app.`);
|
|
253
|
+
if (total > 0 && refused) parts.push(`${num(refused)} of ${plural(total, "reply", "replies")} so far were refused by your app.`);
|
|
254
|
+
if (total > 0 && ours) parts.push(`${num(ours)} of ${plural(total, "reply", "replies")} so far failed on Cortad's side, not your app's.`);
|
|
255
|
+
const left = Date.parse(d.pace?.endsAt ?? "") - now;
|
|
256
|
+
if (left > 0) parts.push(`About ${plural(Math.ceil(left / 60_000), "minute")} left at the pace your app has answered so far.`);
|
|
257
|
+
return `For the person: ${parts.join(" ")}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
194
260
|
export function runText(d) {
|
|
195
261
|
const lines = runLines(d);
|
|
262
|
+
// A verify's verdict says its own round; a count of its replays read as the round being over.
|
|
263
|
+
if (!finished(d) && d.kind !== "verify") lines.push(progressLine(d));
|
|
264
|
+
const plan = finished(d) ? planLine(d.plan) : null;
|
|
265
|
+
if (plan) lines.push(plan);
|
|
266
|
+
const offer = productionOffer(d);
|
|
267
|
+
if (offer) lines.push(offer);
|
|
196
268
|
if (finished(d) && d.url) lines.push(`For the person: the report is at ${d.url}`);
|
|
197
269
|
lines.push(`next: ${nextCall(d)}`);
|
|
198
|
-
return lines.join("\n");
|
|
270
|
+
return verifyOnceAPage(lines.join("\n"));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Production is offered to the person once a fix is verified gone, and gone or less often on the
|
|
274
|
+
// conversations kept back, while it is not connected.
|
|
275
|
+
function productionOffer(d) {
|
|
276
|
+
const v = d.verify;
|
|
277
|
+
if (d.kind !== "verify" || !finished(d) || v?.verdict !== "gone" || !["gone", "less often"].includes(v.heldOut?.moved) || d.field?.connected !== false) return null;
|
|
278
|
+
return `For the person: the failure in ${v.findingId} is gone on its replays. Connect production to see what your real users are doing as it happens and where your AI still lets them down; field_connect has the steps.`;
|
|
199
279
|
}
|
|
200
280
|
|
|
201
281
|
export function startedText(d, kind, findingId, said = []) {
|
|
@@ -217,8 +297,14 @@ export function testedLine(t) {
|
|
|
217
297
|
return `App started ${started}${t.reloads ? " and reloads itself on save" : ""}; your last change was at ${clock(t.change.at)}.`;
|
|
218
298
|
}
|
|
219
299
|
|
|
220
|
-
|
|
221
|
-
|
|
300
|
+
// `runner`: the app's state as the process holding it up wrote it; `app`: the server's. An app that
|
|
301
|
+
// already answers is said to: an agent read "still starting" beside "your app is answering".
|
|
302
|
+
export function waitingText(p, now, limitMs, runner, app) {
|
|
303
|
+
const kind = p.kind === "verify" ? "verify" : "run";
|
|
304
|
+
const waited = `${num((now - Date.parse(p.startedAt)) / 1000)} of up to ${num(limitMs / 1000)} seconds`;
|
|
305
|
+
const head = runner?.state === "up" ? `${answeringText(runner, app)}\nThe ${kind} has waited ${waited}.` : `Your app is still starting for the ${kind}: ${waited}.`;
|
|
306
|
+
return `${head}\nFor the person: the run has not started yet, after ${waited}.\nnext: run_status pending`;
|
|
307
|
+
}
|
|
222
308
|
|
|
223
309
|
// A spent plan: what the last run found, what was fixed since, what the next run would play.
|
|
224
310
|
export function refusedText(d) {
|
|
@@ -232,20 +318,29 @@ export function refusedText(d) {
|
|
|
232
318
|
return lines.join("\n");
|
|
233
319
|
}
|
|
234
320
|
|
|
321
|
+
// The conversations saved to test a fix, unseen by the fixer: a fix whose own
|
|
322
|
+
// replays are gone can still have made the failure worse there.
|
|
323
|
+
const KEPT_MOVE = { gone: "the failure is gone on", "less often": "the failure shows less often on", worse: "the fix made the failure worse on" };
|
|
324
|
+
function keptSaid(h) {
|
|
325
|
+
if (!h?.before?.trials || !h?.after?.trials) return "";
|
|
326
|
+
const counts = `${num(h.before.failed)} of ${num(h.before.trials)} failed before, ${num(h.after.failed)} of ${num(h.after.trials)} after`;
|
|
327
|
+
return KEPT_MOVE[h.moved] ? `; ${KEPT_MOVE[h.moved]} the conversations saved to test the fix (${counts})` : `; on the conversations saved to test the fix, ${counts}, not beyond chance`;
|
|
328
|
+
}
|
|
329
|
+
|
|
235
330
|
function ledgerLines(l) {
|
|
236
331
|
const lines = [];
|
|
237
332
|
const r = l.lastRun;
|
|
238
333
|
if (r) {
|
|
239
|
-
const parts = [has(r.
|
|
334
|
+
const parts = [has(r.findings) && plural(r.findings, "finding"), r.of && `${num(r.played ?? 0)} of ${plural(r.of, "conversation")} played`].filter(Boolean);
|
|
240
335
|
lines.push(`Last run ${r.jobId}: ${parts.join(", ")}.`);
|
|
241
336
|
}
|
|
242
337
|
if (l.fixed) {
|
|
243
338
|
lines.push(`${plural(l.fixed.length, "fix", "fixes")} verified since that run${l.fixed.length ? ":" : "."}`);
|
|
244
339
|
// A fix on a finding with no file (a crash, a standard) is named by its id, never "undefined".
|
|
245
|
-
for (const f of l.fixed) lines.push(` ${f.path ? `${at(f.path, f.line)} (${f.findingId})` : f.findingId}:
|
|
340
|
+
for (const f of l.fixed) lines.push(` ${f.path ? `${at(f.path, f.line)} (${f.findingId})` : f.findingId}: ${f.verdict}; ${num(f.after.failed)} of ${plural(f.after.replays, "replay")} failed, against ${num(f.before.failed)} of ${num(f.before.trials)} before${keptSaid(f.heldOut)}.`);
|
|
246
341
|
}
|
|
247
342
|
const n = l.nextRun;
|
|
248
|
-
if (n) lines.push(`The next run would play ${[plural(n.trials, "
|
|
343
|
+
if (n) lines.push(`The next run would play ${[plural(n.trials, "conversation"), has(n.heldOut) && `${num(n.heldOut)} saved to test fixes`, has(n.newFromChanges) && `${num(n.newFromChanges)} new from the changes`].filter(Boolean).join(", ")}.`);
|
|
249
344
|
const p = l.plan;
|
|
250
345
|
if (p) lines.push(`The ${p.name} plan, $${p.monthlyUsd} a month${has(p.allowed) ? `, includes ${units(p.allowed, p.unit)}` : ""}.`);
|
|
251
346
|
if (l.field === null) lines.push("Production: not connected.");
|
|
@@ -269,58 +364,51 @@ export function linesOf(d) {
|
|
|
269
364
|
export const findingsText = (d, page = 1) => pageOf(findingsPages(d), page, (n) => `findings with page ${n}`);
|
|
270
365
|
|
|
271
366
|
function findingsHead(d) {
|
|
272
|
-
if (!d.runId) return
|
|
367
|
+
if (!d.runId) return d.why ?? "No run has finished on this repository yet.";
|
|
273
368
|
const lines = [`Run ${d.runId}: ${plural(d.findings?.length ?? 0, "finding")}.`];
|
|
369
|
+
const cost = costSaid(d);
|
|
370
|
+
if (cost) lines.push(cost);
|
|
274
371
|
if (d.tested) lines.push(testedLine(d.tested));
|
|
275
|
-
lines.
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// After the findings: what the app's promises measured and how the run went.
|
|
376
|
+
function findingsTail(d) {
|
|
377
|
+
const lines = measuredLines(d);
|
|
276
378
|
if (d.blocked) lines.push(d.blocked);
|
|
277
379
|
if (d.halted) lines.push(haltLine(d.halted));
|
|
278
380
|
lines.push(...waitLines(d));
|
|
279
|
-
const reads = readingsLine(d);
|
|
280
|
-
if (reads) lines.push(reads);
|
|
281
381
|
if (d.baseRunId) lines.push(`Compared with run ${d.baseRunId}.`);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
382
|
+
// With no run to read, the head already said why.
|
|
383
|
+
for (const s of [d.read, d.heldBack, d.findings?.length || !d.runId ? null : d.why]) if (s) lines.push(s);
|
|
384
|
+
// A finding on conversations saved to test a fix: what failed and where, so the rule can be fixed;
|
|
385
|
+
// its inputs and conversations stay unseen, and verify replays it all the same.
|
|
386
|
+
for (const k of d.keptBack ?? []) lines.push(`${k.id}, on conversations saved to test the fix: "${clip(k.asks, 160)}"${k.file ? ` at ${at(k.file, k.line)}` : ""}, ${k.trials ? `failed in ${num(k.trials.failed)} of ${plural(k.trials.of, "conversation")}` : `passed in ${kn(k.rate)}${has(k.reply) ? ` at reply ${num(k.reply)}` : ""}`}${k.quote ? `; the reply said "${clip(k.quote, 120)}"` : ""}. Its inputs are not shown; verify ${k.id} replays them.`);
|
|
286
387
|
return lines.join("\n");
|
|
287
388
|
}
|
|
288
389
|
|
|
289
|
-
// Whole findings
|
|
290
|
-
//
|
|
390
|
+
// Whole findings in the order the server ranks them (settled first, the app's own promises first),
|
|
391
|
+
// then the tail, packed into pages that stay under PAGE_CHARS.
|
|
291
392
|
function findingsPages(d, budget = PAGE_CHARS - 60) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const groups = linesOf(d).map((g) => ({ label: `${plural(g.findingIds.length, "finding")} at ${at(g.path, g.line)}`, placed: true, items: g.findingIds.map((id) => byId.get(id)).filter(Boolean) }));
|
|
296
|
-
const grouped = new Set(groups.flatMap((g) => g.items.map((f) => f.id)));
|
|
297
|
-
const rest = d.findings.filter((f) => !grouped.has(f.id));
|
|
298
|
-
if (rest.length) groups.push({ label: `${plural(rest.length, "finding")} without a line`, placed: false, items: rest });
|
|
299
|
-
|
|
393
|
+
// Unrecorded is not unused (dissection-text.mjs): no tool is said never to have run.
|
|
394
|
+
const unseen = (d.tools ?? d.dissection?.tools)?.recorded === false;
|
|
395
|
+
const blocks = [...(d.findings ?? []).map((f) => findingBlock(f, unseen)), findingsTail(d)].filter(Boolean);
|
|
300
396
|
const pages = [];
|
|
301
|
-
let page =
|
|
397
|
+
let page = findingsHead(d);
|
|
302
398
|
let filled = false;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
page = `Run ${d.runId}, findings continued.`;
|
|
311
|
-
filled = false;
|
|
312
|
-
open = null;
|
|
313
|
-
}
|
|
314
|
-
page += `${label()}\n${block}`;
|
|
315
|
-
filled = true;
|
|
316
|
-
open = g;
|
|
317
|
-
});
|
|
399
|
+
for (const block of blocks) {
|
|
400
|
+
if (filled && page.length + block.length + 2 > budget) {
|
|
401
|
+
pages.push(page);
|
|
402
|
+
page = `Run ${d.runId}, findings continued.`;
|
|
403
|
+
}
|
|
404
|
+
page += `\n\n${block}`;
|
|
405
|
+
filled = true;
|
|
318
406
|
}
|
|
319
407
|
pages.push(page);
|
|
320
|
-
return pages;
|
|
408
|
+
return pages.map(verifyOnceAPage);
|
|
321
409
|
}
|
|
322
410
|
|
|
323
|
-
function findingBlock(f,
|
|
411
|
+
function findingBlock(f, unseen = false) {
|
|
324
412
|
// A gate the run knocked open: the route, the address, the request sent and the status it answered
|
|
325
413
|
// with no sign-in. Its own words, then how to prove the fix.
|
|
326
414
|
if (f.kind === "access-open") {
|
|
@@ -332,57 +420,104 @@ function findingBlock(f, placed) {
|
|
|
332
420
|
lines.push(` Replay: verify ${f.id}`);
|
|
333
421
|
return lines.join("\n");
|
|
334
422
|
}
|
|
335
|
-
|
|
336
|
-
|
|
423
|
+
// What broke, in how many trials, the exchange it broke in, and the line; then the rest.
|
|
424
|
+
const lines = [`${f.id} ${f.asks}`, shareLine(f)].filter(Boolean);
|
|
425
|
+
if (f.sentAway?.said) lines.push(` Of them, ${f.sentAway.said}.`);
|
|
426
|
+
if (f.baseline) lines.push(` Played again: ${plural(f.baseline.trials, "new conversation")} with the same ask in other words; it failed again in ${num(f.baseline.failed)}.`);
|
|
427
|
+
// Chased because production broke it first: real people met this one.
|
|
428
|
+
if (f.production) lines.push(` In production: ${plural(f.production, "conversation")} with real users broke this in the last 30 days.`);
|
|
429
|
+
const logs = { shown: new Set(), room: 30 };
|
|
430
|
+
const [first, ...more] = (f.quotes ?? []).slice(0, 3);
|
|
431
|
+
if (first) lines.push(...quoteLines(first, f, logs));
|
|
432
|
+
if (f.file) lines.push(` At ${at(f.file, f.line)}${f.addressed ? `: ${f.addressed}` : ""}`);
|
|
337
433
|
if (f.criteria) lines.push(` Criteria: ${clip(f.criteria, 600)}`);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (f.
|
|
434
|
+
// The endpoint is said with the rate where there is one.
|
|
435
|
+
if (f.door && !f.trials && !f.rate) lines.push(` Endpoint: ${f.door}`);
|
|
436
|
+
else if (!f.door && f.doors?.length) lines.push(` Endpoints: ${f.doors.join(", ")}`);
|
|
437
|
+
const where = besides(f.where, f.door);
|
|
438
|
+
if (where) lines.push(` Where: ${where}`);
|
|
341
439
|
if (f.fix) lines.push(` Fix: ${f.fix}`);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
lines.push(` Same cause in ${plural(f.alsoIn.length, "more place")}, the same reply sentence in each:`);
|
|
345
|
-
for (const a of f.alsoIn.slice(0, 8)) lines.push(` ${a.where}: ${clip(a.asks, 160)}, broke in ${num(a.rate.n - a.rate.k)} of ${plural(a.rate.n, "reply", "replies")}`);
|
|
346
|
-
if (f.alsoIn.length > 8) lines.push(` and ${num(f.alsoIn.length - 8)} more.`);
|
|
347
|
-
}
|
|
440
|
+
if (f.sentAway?.neverRan?.length && !unseen) lines.push(` Needed by these asks and never run: ${f.sentAway.neverRan.join(", ")}.`);
|
|
441
|
+
lines.push(...alsoLines(f));
|
|
348
442
|
if (f.layer && LAYER_WORDS[f.layer]) lines.push(` Layer: ${f.layer}, ${LAYER_WORDS[f.layer]}.`);
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
// that asks whether the passages MISS the point left an agent unable to tell pass from fail.
|
|
352
|
-
if (r) lines.push(` Broke in ${num(r.n - r.k)} of ${plural(r.n, "reply", "replies")}${r.n ? `, ${Math.round((100 * (r.n - r.k)) / r.n)}%` : ""}${has(r.lo) ? `, interval ${Math.round((1 - r.hi) * 100)}% to ${Math.round((1 - r.lo) * 100)}%` : ""}.`);
|
|
353
|
-
if (f.unsettled) lines.push(" Unsettled: under the 22-reading floor.");
|
|
354
|
-
const by = [f.decidedBy?.code && `code in ${plural(f.decidedBy.code, "reading")}`, f.decidedBy?.model && `a model in ${plural(f.decidedBy.model, "reading")}`].filter(Boolean);
|
|
443
|
+
const read = f.decidedBy?.reader ?? f.decidedBy?.model;
|
|
444
|
+
const by = [f.decidedBy?.code && `code on ${plural(f.decidedBy.code, "quoted reply", "quoted replies")}`, read && `a model on ${plural(read, "quoted reply", "quoted replies")}`].filter(Boolean);
|
|
355
445
|
if (by.length) lines.push(` Decided by ${by.join(", by ")}.`);
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
for (const q of (f.quotes ?? []).slice(0, 3)) {
|
|
359
|
-
const about = [typeof q.p === "number" && `confidence ${q.p.toFixed(2)}`, q.trialId && `trial ${q.trialId}`, q.door && `at ${q.door}`].filter(Boolean).join(", ");
|
|
360
|
-
// The request beside the reply: an agent handed "HTTP 502 happened" did the rest of the
|
|
361
|
-
// investigation itself.
|
|
362
|
-
if (q.asked) lines.push(` Asked: "${clip(q.asked, 300)}"`);
|
|
363
|
-
lines.push(` Reply ${q.reply}: "${clip(q.quote, 400)}"${about ? ` (${about})` : ""}`);
|
|
364
|
-
// What the reader checked the reply against: what their app retrieved for it, the start of each.
|
|
365
|
-
if (q.checked) lines.push(` ${q.checked}`);
|
|
366
|
-
for (const p of (q.passages ?? []).slice(0, 3)) lines.push(` Passage${p.source ? ` (${clip(p.source, 60)})` : ""}: "${clip(p.text, 240)}"`);
|
|
367
|
-
// What a developer reproduces it from: the request as sent, the body back, the app's own log.
|
|
368
|
-
if (q.request) lines.push(` Request: ${q.request.method} ${q.request.path}${q.request.body ? ` ${clip(q.request.body, 600)}` : ""}`);
|
|
369
|
-
if (has(q.response)) lines.push(` Response: ${q.response ? clip(q.response, 600) : "(empty body)"}`);
|
|
370
|
-
// A page stays under what a client keeps: the same log twice is said once, and a finding
|
|
371
|
-
// prints thirty log lines at most.
|
|
372
|
-
const log = (q.log ?? []).join("\n");
|
|
373
|
-
if (log && logsShown.has(log)) lines.push(" Log: the same lines as the reply above.");
|
|
374
|
-
else for (const line of (q.log ?? []).slice(0, logRoom)) { lines.push(` Log: ${clip(line, 240)}`); logRoom -= 1; }
|
|
375
|
-
if (log) logsShown.add(log);
|
|
376
|
-
if (q.at && q.at !== at(f.file, f.line)) lines.push(` At ${q.at}`);
|
|
377
|
-
}
|
|
446
|
+
for (const q of more) lines.push(...quoteLines(q, f, logs));
|
|
447
|
+
if (f.sentAway?.note) lines.push(` ${f.sentAway.note}`);
|
|
378
448
|
for (const line of (f.log ?? []).slice(0, 8)) lines.push(` Log: ${clip(line, 300)}`);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
//
|
|
382
|
-
|
|
449
|
+
// A finding read from the customer's own words has nothing a verify can replay; its plan says so.
|
|
450
|
+
if (f.replay?.trials) lines.push(` Replay: ${plural(f.replay.trials, "conversation")}, verify ${f.id}`);
|
|
451
|
+
// What a verify of it can show at most, read before the allowance is spent: an agent paid for two
|
|
452
|
+
// verifies of a finding resting on six readings, and neither could have left the noise.
|
|
453
|
+
if (f.replay?.plan) lines.push(` ${f.replay.plan}`);
|
|
383
454
|
return lines.join("\n");
|
|
384
455
|
}
|
|
385
456
|
|
|
457
|
+
const pct = (x) => `${Math.round(x * 100)}%`;
|
|
458
|
+
|
|
459
|
+
// The server's `where` names the surface, which a finding already says as its endpoint.
|
|
460
|
+
const besides = (where, door) => (door && where ? where.split(", ").filter((p) => p !== door).join(", ") : where);
|
|
461
|
+
|
|
462
|
+
// The break count in trials, the unit a finding forms in. A finding from before trials counted says
|
|
463
|
+
// its replies, whichever way the question is phrased: "held in 21 of 33" under a question that asks
|
|
464
|
+
// whether the passages MISS the point left an agent unable to tell pass from fail.
|
|
465
|
+
function shareLine(f) {
|
|
466
|
+
const atDoor = f.door ? ` at ${f.door}` : "";
|
|
467
|
+
const t = f.trials;
|
|
468
|
+
if (t) return ` Failed in ${num(t.failed)} of ${plural(t.of, "conversation")}${atDoor}: ${pct(t.failed / t.of)} (${pct(t.lo)} to ${pct(t.hi)}).`;
|
|
469
|
+
const r = f.rate;
|
|
470
|
+
if (!r) return "";
|
|
471
|
+
const range = has(r.lo) ? ` (${pct(1 - r.hi)} to ${pct(1 - r.lo)})` : "";
|
|
472
|
+
return ` Failed on ${num(r.n - r.k)} of ${plural(r.n, "reply", "replies")}${atDoor}${r.n ? `: ${pct((r.n - r.k) / r.n)}${range}` : ""}.`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// One quoted break: what the trial sent, the reply, the sentence it broke on, what the reply was
|
|
476
|
+
// checked against, and what a developer reproduces it from: the request as sent, the body back, the
|
|
477
|
+
// app's own log. The same log twice is said once, and a finding prints thirty log lines at most.
|
|
478
|
+
function quoteLines(q, f, logs) {
|
|
479
|
+
const lines = [];
|
|
480
|
+
const about = [q.trialId && `conversation ${String(q.trialId).slice(0, 8)}`, q.door && `at ${q.door}`].filter(Boolean).join(", ");
|
|
481
|
+
if (q.asked) lines.push(` Sent: "${clip(q.asked, 300)}"`);
|
|
482
|
+
lines.push(` Reply ${q.reply}: "${clip(q.answer ?? q.quote, 400)}"${about ? ` (${about})` : ""}`);
|
|
483
|
+
if (q.answer && q.quote && q.quote !== q.answer) lines.push(` It broke on: "${clip(q.quote, 300)}"`);
|
|
484
|
+
// How sure the reading was that this reply broke the check, and who decided it.
|
|
485
|
+
if (q.after) lines.push(` Then the customer: "${clip(q.after, 300)}"`);
|
|
486
|
+
if (q.checked) lines.push(` ${q.checked}`);
|
|
487
|
+
for (const p of (q.passages ?? []).slice(0, 3)) lines.push(` Passage${p.source ? ` (${clip(p.source, 60)})` : ""}: "${clip(p.text, 240)}"`);
|
|
488
|
+
if (q.request) lines.push(` Request: ${q.request.method} ${q.request.path}${q.request.body ? ` ${clip(q.request.body, 600)}` : ""}`);
|
|
489
|
+
if (has(q.response)) lines.push(` Response: ${q.response ? clip(q.response, 600) : "(empty body)"}`);
|
|
490
|
+
const log = (q.log ?? []).join("\n");
|
|
491
|
+
if (log && logs.shown.has(log)) lines.push(" Log: the same lines as the reply above.");
|
|
492
|
+
else for (const line of (q.log ?? []).slice(0, logs.room)) { lines.push(` Log: ${clip(line, 240)}`); logs.room -= 1; }
|
|
493
|
+
if (log) logs.shown.add(log);
|
|
494
|
+
if (q.at && q.at !== at(f.file, f.line)) lines.push(` At ${q.at}`);
|
|
495
|
+
return lines;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// One question, one finding: the other situations it failed in are listed under it, and so are the
|
|
499
|
+
// other checks the same reply sentence broke, which are not findings of their own.
|
|
500
|
+
function alsoLines(f) {
|
|
501
|
+
const failed = (a) => (a.trials ? a.trials.failed : a.rate.n - a.rate.k);
|
|
502
|
+
const broke = (a) => (a.trials ? `failed in ${num(a.trials.failed)} of ${plural(a.trials.of, "conversation")}` : `failed on ${num(failed(a))} of ${plural(a.rate.n, "reply", "replies")}`);
|
|
503
|
+
const own = (a) => Boolean(f.questionId) && a.questionId === f.questionId;
|
|
504
|
+
// Only where it failed: a list of the groups it held in read as more failures.
|
|
505
|
+
const same = (f.alsoIn ?? []).filter((a) => own(a) && failed(a) > 0);
|
|
506
|
+
const other = (f.alsoIn ?? []).filter((a) => !own(a));
|
|
507
|
+
const lines = [];
|
|
508
|
+
if (same.length) {
|
|
509
|
+
lines.push(" Also failed:");
|
|
510
|
+
for (const a of same.slice(0, 8)) lines.push(` ${[a.door, besides(a.where, a.door)].filter(Boolean).join(", ")}: ${broke(a)}`);
|
|
511
|
+
if (same.length > 8) lines.push(` and ${num(same.length - 8)} more.`);
|
|
512
|
+
}
|
|
513
|
+
if (other.length) {
|
|
514
|
+
lines.push(` Same cause in ${plural(other.length, "more place")}, the same reply sentence in each:`);
|
|
515
|
+
for (const a of other.slice(0, 8)) lines.push(` ${a.where}: ${clip(a.asks, 160)}, ${broke(a)}`);
|
|
516
|
+
if (other.length > 8) lines.push(` and ${num(other.length - 8)} more.`);
|
|
517
|
+
}
|
|
518
|
+
return lines;
|
|
519
|
+
}
|
|
520
|
+
|
|
386
521
|
export function fieldText(d) {
|
|
387
522
|
const t = d.totals;
|
|
388
523
|
const pct = (k, n) => (n ? `${Math.round((100 * k) / n)}%` : "n/a");
|
|
@@ -390,11 +525,11 @@ export function fieldText(d) {
|
|
|
390
525
|
if (!t || !t.n) return [`No production conversations read in the last ${plural(d.days, "day")}.`, ...link].join("\n");
|
|
391
526
|
const lines = [
|
|
392
527
|
`Production, last ${plural(d.days, "day")}: ${plural(t.n, "conversation")}, ${num(t.read)} read.`,
|
|
393
|
-
`Rule checks
|
|
528
|
+
`Rule checks passed: ${pct(t.rulingsHeld, t.rulings)} of ${num(t.rulings)}${t.rulingsUnsure ? ` (${num(t.rulingsUnsure)} too close to call)` : ""}. Resolved ${pct(t.resolved, t.read)}, frustrated ${pct(t.frustrated, t.read)}, asked for a human ${pct(t.wantsHuman, t.read)}, unanswered ${pct(t.unanswered, t.read)}.`,
|
|
394
529
|
];
|
|
395
530
|
const broke = (d.rules ?? []).filter((r) => r.broke > 0).sort((a, b) => b.broke - a.broke).slice(0, 5);
|
|
396
531
|
if (broke.length) lines.push(`Rules broken most: ${broke.map((r) => `${r.id} (${num(r.broke)})`).join(", ")}.`);
|
|
397
|
-
if (d.journeys?.length) lines.push(`By journey: ${d.journeys.slice(0, 6).map((j) => `${j.value} ${plural(j.n, "conversation")}, ${pct(j.rulingsHeld, j.rulings)}
|
|
532
|
+
if (d.journeys?.length) lines.push(`By journey: ${d.journeys.slice(0, 6).map((j) => `${j.value} ${plural(j.n, "conversation")}, ${pct(j.rulingsHeld, j.rulings)} passed`).join("; ")}.`);
|
|
398
533
|
return [...lines, ...link].join("\n");
|
|
399
534
|
}
|
|
400
535
|
|