cortad 0.4.2 → 0.4.3

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/lib/proof.mjs CHANGED
@@ -766,9 +766,20 @@ function problemsIn(ex, reply, find, frame, printedLine) {
766
766
  placed("leaked-markup", [...tags][0], `The reply carried markup from the prompt your app sent the model: ${listed(tags)}.`, written ?? call,
767
767
  written ? "" : " None of it is written in your code; this is the line that made the model call.");
768
768
  }
769
+ // A SLOT IS ONLY A SLOT WHERE A LINE OF THEIR CODE WRITES IT.
770
+ //
771
+ // Both branches now need `find(p)`. The first one asked only whether the same braces appear in the
772
+ // runtime prompt, which on a maths tutor they always do: LaTeX. `\text{kg}` in a reply and the same
773
+ // in the prompt reported an unfilled placeholder and addressed it to the line that made the model
774
+ // call, while holding the proof that no code of theirs writes the token. Measured on one customer's
775
+ // 64 replies: 17 of them carry a brace group this pattern matches, and `\begin{bmatrix}`,
776
+ // `\frac{dy}`, `{dx}` and `{T_y}` are not slots. Every `placeholder` problem this rule has ever
777
+ // recorded, 12 of them across six days, was that same false reading, and none was ever real.
769
778
  for (const [p] of said.matchAll(PLACEHOLDER)) {
770
- if (prompt.includes(norm(p))) placed("placeholder", p, `The reply carried "${p}", a placeholder still unfilled in the prompt sent to the model.`, find(p) ?? call);
771
- else if (!words.includes(norm(p)) && find(p)) add("placeholder", `The reply carried "${p}", a placeholder your code never filled.`, find(p));
779
+ const written = find(p);
780
+ if (!written) continue;
781
+ if (prompt.includes(norm(p))) placed("placeholder", p, `The reply carried "${p}", a placeholder still unfilled in the prompt sent to the model.`, written);
782
+ else if (!words.includes(norm(p))) add("placeholder", `The reply carried "${p}", a placeholder your code never filled.`, written);
772
783
  }
773
784
  const stopped = endedOnTool(ex);
774
785
  if (stopped) add("ended-on-tool", `The request ended right after ${stopped.join(", ")} ran: no model call came after it, so the reply holds no answer written from the tool's result.`, frame(generation(ex).at(-1)?.caller) ?? call);
package/lib/text.mjs CHANGED
@@ -99,12 +99,23 @@ const runsSaid = (left, allowed) => (left > 0 ? `${num(left)} of ${plural(allowe
99
99
  // After the findings: what was measured of the app's own promises, in its own words, then the checks
100
100
  // measured and why the rest were not, then how many trials kept every promise. Counts, never a score.
101
101
  // The replies that did not count sit before the count: a clean count beside 62 error replies read as a clean bill.
102
- // Their own eval files, as the run played them: one line for all, one per file.
102
+ // Their own eval files, as the run played them: one line for all, one per file. `open` are questions
103
+ // a file asks with nothing expected of the reply, played and read against the app's own rules; they
104
+ // are said apart because the file made no claim to pass or fail. `left` is what never became a
105
+ // conversation, by the reason, so a count of what was taken never stands alone.
103
106
  function evalLines(evals) {
104
107
  const t = evals?.total;
105
- if (!t || !t.played) return [];
106
- const said = (x) => `${num(x.passed)} of ${plural(x.played, "case")} passed${x.failed ? `, ${num(x.failed)} failed` : ""}${x.unclear ? `, ${num(x.unclear)} not settled` : ""}${x.cases > x.played ? `, ${num(x.cases - x.played)} not played` : ""}`;
107
- return [`Your own eval files: ${said(t)}.`, ...(evals.files ?? []).slice(0, 12).map((f) => ` ${f.file}: ${said(f)}.`)];
108
+ if (!t || (!t.played && !t.open)) return [];
109
+ const open = (x) => (x.open ? `${x.played ? ", and " : ""}${num(x.open)} more ${x.open === 1 ? "question it asks" : "questions it asks"} with no answer of its own, read against your rules` : "");
110
+ const said = (x) => (x.played
111
+ ? `${num(x.passed)} of ${plural(x.played, "case")} passed${x.failed ? `, ${num(x.failed)} failed` : ""}${x.unclear ? `, ${num(x.unclear)} not settled` : ""}${x.cases > x.played ? `, ${num(x.cases - x.played)} not played` : ""}${open(x)}`
112
+ : `${num(x.open)} ${x.open === 1 ? "question" : "questions"} played, read against your rules, since the ${x.open === 1 ? "file expects no answer of its own" : "files expect no answers of their own"}`);
113
+ const left = Object.entries(evals.left ?? {}).sort((a, b) => b[1] - a[1]);
114
+ return [
115
+ `Your own eval files: ${said(t)}.`,
116
+ ...(evals.files ?? []).slice(0, 12).map((f) => ` ${f.file}: ${said(f)}.`),
117
+ ...left.map(([why, n]) => ` ${num(n)} ${n === 1 ? "case" : "cases"} of yours was left out: ${why}.`.replace("cases of yours was", "cases of yours were")),
118
+ ];
108
119
  }
109
120
 
110
121
  function measuredLines(d) {
@@ -118,6 +129,10 @@ function measuredLines(d) {
118
129
  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.`);
119
130
  // The server's own sentence already says the count.
120
131
  if (d.promises && !d.promises.said && has(d.promises.decided)) lines.push(promiseLine(d));
132
+ // What the run decided, and what nothing breaking is worth on that many. The server writes the
133
+ // sentence (simulate/report-pack.ts sensitivityOf) because the arithmetic belongs beside the
134
+ // numbers it is made of; printing it here is what stops a count standing with no denominator.
135
+ if (d.sensitivity?.said) lines.push(clip(d.sensitivity.said, 400));
121
136
  return lines;
122
137
  }
123
138
 
@@ -440,6 +455,13 @@ function findingBlock(f, unseen = false) {
440
455
  const lines = [`${f.id} ${f.asks}`, shareLine(f)].filter(Boolean);
441
456
  if (f.sentAway?.said) lines.push(` Of them, ${f.sentAway.said}.`);
442
457
  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)}.`);
458
+ // The same conversation, played again from nothing. A failure that did not come back is one roll of
459
+ // the app's dice, and it was listed beside one that came back twice with nothing to tell them apart.
460
+ if (f.cameBack) {
461
+ lines.push(f.cameBack.again === 0
462
+ ? ` Played over again: ${plural(f.cameBack.played, "conversation")} of its own, word for word from nothing, and it did not fail again in any of them. Worth less of your time than one that came back.`
463
+ : ` Played over again: ${plural(f.cameBack.played, "conversation")} of its own, word for word from nothing, and it failed again in ${num(f.cameBack.again)}.`);
464
+ }
443
465
  // Chased because production broke it first: real people met this one.
444
466
  if (f.production) lines.push(` In production: ${plural(f.production, "conversation")} with real users broke this in the last 30 days.`);
445
467
  const logs = { shown: new Set(), room: 30 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them. No dependencies.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"