bareloop 0.38.0 → 0.39.0
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/CHANGELOG.md +32 -0
- package/bareloop.context.md +26 -15
- package/package.json +3 -3
- package/src/authorflow.js +3 -2
- package/src/authoring.js +62 -3
- package/src/authorjob.js +37 -24
- package/src/authorreadout.js +449 -2
- package/src/authorrun.js +21 -4
- package/src/calibrate.js +84 -10
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +10 -4
- package/src/job.js +1 -1
- package/src/judged.js +322 -28
- package/src/kinds.js +13 -1
- package/src/panel/authorroutes.js +2 -6
- package/src/panel/authorsession.js +61 -45
- package/src/panel/index.html +123 -242
- package/src/planrun.js +3 -2
- package/src/providers.js +27 -0
- package/src/trend.js +1 -0
- package/types/authoring.d.ts +24 -1
- package/types/authorjob.d.ts +17 -10
- package/types/authorreadout.d.ts +106 -0
- package/types/calibrate.d.ts +4 -1
- package/types/declaredclose.d.ts +2 -1
- package/types/judged.d.ts +21 -2
- package/types/panel/authorsession.d.ts +15 -1
- package/types/providers.d.ts +7 -0
- package/types/trend.d.ts +1 -0
package/src/authorreadout.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
import { scrubRaw } from './text.js';
|
|
27
27
|
import { redactSecrets } from './validate.js';
|
|
28
28
|
import { judgedStages } from './kinds.js';
|
|
29
|
+
import { CALIBRATION_SIZE } from './judged.js';
|
|
30
|
+
import { INJECTION_LOCATE_BATTERY } from './calibrate.js';
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* @param {{goal?: string|null, closeDecl?: any}} spec the RESOLVED spec — the bytes
|
|
@@ -142,6 +144,36 @@ export function answeredQuestionLines(confirmed) {
|
|
|
142
144
|
return lines;
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/** a quote for a human line: one line, cut at `max` with the cut VISIBLE
|
|
148
|
+
* @param {unknown} q @param {number} [max] @returns {string} */
|
|
149
|
+
function shortQuote(q, max = 80) {
|
|
150
|
+
const one = String(q ?? '').replace(/\s+/g, ' ').trim();
|
|
151
|
+
return one.length > max ? `${one.slice(0, max)}…[truncated, ${one.length} chars]` : one;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* ONE plain line saying WHY a graded case reds, read off its own `diag` (F192):
|
|
156
|
+
* the first red's rule, function, `why` and quoted text — and how many more the
|
|
157
|
+
* record holds. A case with no reds says why it has none (no facts: each
|
|
158
|
+
* attempt's axis and cause). Null when the row predates `diag`.
|
|
159
|
+
* @param {any} row a `graded` or `injection.styles` row
|
|
160
|
+
* @returns {string|null}
|
|
161
|
+
*/
|
|
162
|
+
function diagLine(row) {
|
|
163
|
+
const d = row?.diag;
|
|
164
|
+
if (!d) return null;
|
|
165
|
+
const first = d.reds?.[0];
|
|
166
|
+
if (first) {
|
|
167
|
+
const more = d.reds.length > 1 ? ` (+${d.reds.length - 1} more red(s) in signing.json)` : '';
|
|
168
|
+
return ` why: ${first.rule} · ${first.fn} — ${first.why}; quote: ${first.quote === null ? '(none)' : `"${shortQuote(first.quote)}"`}${more}`;
|
|
169
|
+
}
|
|
170
|
+
const failed = (d.attempts ?? []).filter((/** @type {any} */ a) => !a.ok);
|
|
171
|
+
if (failed.length) {
|
|
172
|
+
return ` why: no facts — ${failed.map((/** @type {any} */ a) => `attempt ${a.attempt} [${a.axis}] ${shortQuote(a.detail, 120)}`).join('; ')}`;
|
|
173
|
+
}
|
|
174
|
+
return d.reason ? ` why: ${shortQuote(d.reason, 120)}` : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
145
177
|
/**
|
|
146
178
|
* THE CALIBRATION GATE'S OWN READOUT — the one gate that spends money, and the
|
|
147
179
|
* one whose rows a signer has to read case by case.
|
|
@@ -170,14 +202,46 @@ export function calibrationLines(calibration) {
|
|
|
170
202
|
if (calibration.casualty) {
|
|
171
203
|
lines.push(` CASUALTY on ${calibration.casualty.kind} "${calibration.casualty.at}" [${calibration.casualty.axis}] `
|
|
172
204
|
+ '— a broken judge is no evidence about the set');
|
|
205
|
+
const cl = diagLine(calibration.casualty);
|
|
206
|
+
if (cl) lines.push(cl);
|
|
207
|
+
}
|
|
208
|
+
for (const g of graded.filter((/** @type {any} */ x) => !x.ok)) {
|
|
209
|
+
lines.push(` WRONG ${g.id}: ${g.detail}`);
|
|
210
|
+
const l = diagLine(g);
|
|
211
|
+
if (l) lines.push(l);
|
|
212
|
+
}
|
|
213
|
+
for (const s of styles.filter((/** @type {any} */ x) => !x.resisted)) {
|
|
214
|
+
lines.push(` LEAK ${s.style}: ${s.detail}`);
|
|
215
|
+
const l = diagLine(s);
|
|
216
|
+
if (l) lines.push(l);
|
|
173
217
|
}
|
|
174
|
-
for (const g of graded.filter((/** @type {any} */ x) => !x.ok)) lines.push(` WRONG ${g.id}: ${g.detail}`);
|
|
175
|
-
for (const s of styles.filter((/** @type {any} */ x) => !x.resisted)) lines.push(` LEAK ${s.style}: ${s.detail}`);
|
|
176
218
|
lines.push(` certified card ${String(calibration.cardHash ?? 'unknown').slice(0, 12)} cases `
|
|
177
219
|
+ `${String(calibration.casesHash ?? 'unknown').slice(0, 12)} set ${String(calibration.setHash ?? 'unknown').slice(0, 12)}`);
|
|
178
220
|
return lines;
|
|
179
221
|
}
|
|
180
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The calibration gate's real result as one line a person reads next to the step (hamr, 2026-10-09: the panel
|
|
225
|
+
* showed `calibrating ✓` over a gate that graded 8 of 10 and let 2 attack styles leak). PASS IS THE GATE'S OWN:
|
|
226
|
+
* `ok` is `calibration.ok` as `runCalibration` decided it (every case right as a SET of reds, every style
|
|
227
|
+
* resisted), never recomputed here, so there is no second spelling of the floor. The counts are read from the
|
|
228
|
+
* record (`graded[].ok` is `compareExpectation`, which compares reds as sets); the denominators are the live
|
|
229
|
+
* constants (CALIBRATION_SIZE, the battery length), so a gate that stopped part-way reads "x of 10", not "x of x".
|
|
230
|
+
* @param {any} calibration `signing.gates.calibration`
|
|
231
|
+
* @returns {{ok: boolean, note: string}|null} null when the gate was not reached
|
|
232
|
+
*/
|
|
233
|
+
export function calibrationSummary(calibration) {
|
|
234
|
+
if (!calibration || typeof calibration !== 'object') return null;
|
|
235
|
+
const graded = Array.isArray(calibration.graded) ? calibration.graded : [];
|
|
236
|
+
const styles = Array.isArray(calibration.injection?.styles) ? calibration.injection.styles : [];
|
|
237
|
+
const right = graded.filter((/** @type {any} */ g) => g?.ok === true).length;
|
|
238
|
+
const resisted = styles.filter((/** @type {any} */ x) => x?.resisted === true).length;
|
|
239
|
+
return {
|
|
240
|
+
ok: calibration.ok === true,
|
|
241
|
+
note: `${right} of ${CALIBRATION_SIZE} practice cases graded right · ${resisted} of ${INJECTION_LOCATE_BATTERY.length} attack tests resisted`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
181
245
|
/**
|
|
182
246
|
* THE AUTHORING CEILING, parsed from `--budget`. Housed here for the same reason
|
|
183
247
|
* the readout above is: the runner is a script, and a rule no test can reach is a
|
|
@@ -321,3 +385,386 @@ export function crashRecord(err) {
|
|
|
321
385
|
}),
|
|
322
386
|
};
|
|
323
387
|
}
|
|
388
|
+
|
|
389
|
+
// ── THE PLAIN REASON FOR A REFUSED RUBRIC PROPOSAL ──────────────────────────
|
|
390
|
+
//
|
|
391
|
+
// A rubric draft that stopped at `proposal-invalid` / `rubric-invalid` used to
|
|
392
|
+
// say only that code, and the technical reds behind it were lost to the person
|
|
393
|
+
// (live, panel session smv0j8bp2pbyn, 2026-10-09; hamr: "A user should get a
|
|
394
|
+
// reason that they can understand and do something about"). ONE owner: the
|
|
395
|
+
// panel session and the CLI both print `proposalStopText`, so there is one
|
|
396
|
+
// spelling and two doors. Every sentence is written HERE, by code — the model
|
|
397
|
+
// never words a reason — and the structured red travels beside it so the record
|
|
398
|
+
// loses nothing.
|
|
399
|
+
|
|
400
|
+
/** the longest case name / quoted piece a plain sentence will carry */
|
|
401
|
+
const PLAIN_QUOTE_MAX = 60;
|
|
402
|
+
/** the most bullets a message lists before it says how many more there are */
|
|
403
|
+
const PLAIN_MAX_BULLETS = 8;
|
|
404
|
+
/** the most reds one drafting-log record carries, and the longest string in any of them */
|
|
405
|
+
const RECORD_MAX_REDS = 40;
|
|
406
|
+
const RECORD_STR_MAX = 600;
|
|
407
|
+
|
|
408
|
+
/** model-derived text in a person-facing line: scrubbed, one line, bounded
|
|
409
|
+
* @param {unknown} s @returns {string} */
|
|
410
|
+
function safeText(s) {
|
|
411
|
+
const one = redactSecrets(String(s ?? '')).replace(/\s+/g, ' ').trim();
|
|
412
|
+
return one.length > PLAIN_QUOTE_MAX ? `${one.slice(0, PLAIN_QUOTE_MAX)}…` : one;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @typedef {{code: string, path: string, detail: string, [k: string]: any}} PlainRed
|
|
417
|
+
* @typedef {{kind: string, sentence: string, red: PlainRed}} PlainReason
|
|
418
|
+
*/
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The ONE table of plain kinds → sentence builders. A builder gets the red, the
|
|
422
|
+
* name of the case it lands on (when it lands on one) and the match of the
|
|
423
|
+
* regex that picked it. `unmapped` is the only kind the table does not list.
|
|
424
|
+
* Order matters: the first matching entry wins.
|
|
425
|
+
* @type {{kind: string, when: (r: PlainRed) => boolean, say: (r: PlainRed, c: string, m: RegExpMatchArray|null) => string}[]}
|
|
426
|
+
*/
|
|
427
|
+
const PLAIN_TABLE = [
|
|
428
|
+
// ── the rubric card (validateCard, surfaced as code invalid-value at path "card") ──
|
|
429
|
+
{ kind: 'card-not-object', when: (r) => r.code === 'invalid-value' && /^the card must be an object/.test(r.detail),
|
|
430
|
+
say: () => 'The model did not hand back a rubric in the expected shape.' },
|
|
431
|
+
{ kind: 'card-empty', when: (r) => r.code === 'invalid-value' && /^the card must carry a non-empty/.test(r.detail),
|
|
432
|
+
say: () => 'The model\'s rubric has no rules in it.' },
|
|
433
|
+
{ kind: 'card-line-bad', when: (r) => r.code === 'invalid-value' && /^card item \d+: must be an object/.test(r.detail),
|
|
434
|
+
say: (r) => `Rubric line ${cardLineNo(r)} is not a proper line.` },
|
|
435
|
+
{ kind: 'card-line-no-rule', when: (r) => r.code === 'invalid-value' && /^card item \d+: needs a `rule`/.test(r.detail),
|
|
436
|
+
say: (r) => `Rubric line ${cardLineNo(r)} does not pick which rule it uses.` },
|
|
437
|
+
{ kind: 'card-line-unknown-rule', when: (r) => r.code === 'invalid-value' && /^card item \d+: `.*` is not a rule this arbiter implements/.test(r.detail),
|
|
438
|
+
say: (r, _c, _m) => `Rubric line ${cardLineNo(r)} names a rule bareloop does not have (${safeText(/`(.*?)` is not a rule/.exec(r.detail)?.[1] ?? '')}).` },
|
|
439
|
+
{ kind: 'card-line-repeat', when: (r) => r.code === 'invalid-value' && /^card item \d+: `.*` is named twice/.test(r.detail),
|
|
440
|
+
say: (r) => `Rubric line ${cardLineNo(r)} uses a rule that an earlier line already uses.` },
|
|
441
|
+
{ kind: 'card-line-no-text', when: (r) => r.code === 'invalid-value' && /^card item \d+: needs `text`/.test(r.detail),
|
|
442
|
+
say: (r) => `Rubric line ${cardLineNo(r)} has no wording.` },
|
|
443
|
+
{ kind: 'proposal-not-object', when: (r) => r.code === 'invalid-value' && r.path === 'proposal',
|
|
444
|
+
say: () => 'The model\'s answer was not a rubric plus practice cases.' },
|
|
445
|
+
// ── the practice-case set as a whole ──
|
|
446
|
+
{ kind: 'set-no-rules', when: (r) => r.code === 'calibration-cardless',
|
|
447
|
+
say: () => 'The rubric has no rules, so the practice cases had nothing to be checked against.' },
|
|
448
|
+
{ kind: 'set-not-list', when: (r) => r.code === 'calibration-size' && !('declared' in r),
|
|
449
|
+
say: () => `The practice cases did not come back as a list of ${CALIBRATION_SIZE}.` },
|
|
450
|
+
{ kind: 'set-size', when: (r) => r.code === 'calibration-size',
|
|
451
|
+
say: (r) => `The model gave ${r.declared} practice case${r.declared === 1 ? '' : 's'}; exactly ${r.required ?? CALIBRATION_SIZE} are needed.` },
|
|
452
|
+
{ kind: 'set-one-sided', when: (r) => r.code === 'calibration-polarity',
|
|
453
|
+
say: (r) => `The practice cases were ${r.passes} pass and ${r.reds} fail; the set needs at least one of each, or it cannot tell a working judge from one that always gives the same answer.` },
|
|
454
|
+
// ── one practice case ──
|
|
455
|
+
{ kind: 'case-not-object', when: (r) => r.code === 'calibration-case' && /^calibration\.cases\[\d+\]$/.test(r.path),
|
|
456
|
+
say: (_r, c) => `${c} is not a proper case.` },
|
|
457
|
+
{ kind: 'case-name-repeat', when: (r) => r.code === 'calibration-case' && /\.id$/.test(r.path) && /declared twice/.test(r.detail),
|
|
458
|
+
say: (_r, c) => `Two practice cases share the name ${c}.` },
|
|
459
|
+
{ kind: 'case-name-bad', when: (r) => r.code === 'calibration-case' && /\.id$/.test(r.path),
|
|
460
|
+
say: (_r, c) => `${c} has no usable name (a short name in lowercase letters, digits and hyphens is needed).` },
|
|
461
|
+
{ kind: 'case-code-repeat', when: (r) => r.code === 'calibration-case' && /\.artifact$/.test(r.path) && /used twice/.test(r.detail),
|
|
462
|
+
say: (_r, c) => `${c} reuses the same sample code as another case.` },
|
|
463
|
+
{ kind: 'case-code-missing', when: (r) => r.code === 'calibration-case' && /\.artifact$/.test(r.path),
|
|
464
|
+
say: (_r, c) => `${c} has no sample code to look at.` },
|
|
465
|
+
{ kind: 'case-impossible-no-doc', when: (r) => r.code === 'calibration-case' && /\.expect\.reds$/.test(r.path) && /expects has-doc red on/.test(r.detail),
|
|
466
|
+
say: (r, c) => {
|
|
467
|
+
const fn = safeText(/expects has-doc red on (\S+?),/.exec(r.detail)?.[1] ?? 'a function');
|
|
468
|
+
return `${c} says "fail: no doc comment" on ${fn}, but ${fn} has a doc comment right above it.`;
|
|
469
|
+
} },
|
|
470
|
+
{ kind: 'case-pass-lists-fails', when: (r) => r.code === 'calibration-case' && /\.expect\.reds$/.test(r.path) && /^a PASS case expects no reds/.test(r.detail),
|
|
471
|
+
say: (_r, c) => `${c} is marked as a pass but also lists things that fail.` },
|
|
472
|
+
{ kind: 'case-fail-lists-nothing', when: (r) => r.code === 'calibration-case' && /\.expect\.reds$/.test(r.path) && /^a RED case names WHICH/.test(r.detail),
|
|
473
|
+
say: (_r, c) => `${c} is marked as a fail but does not say which rule or function fails.` },
|
|
474
|
+
{ kind: 'case-fails-not-list', when: (r) => r.code === 'calibration-case' && /\.expect\.reds$/.test(r.path),
|
|
475
|
+
say: (_r, c) => `${c} does not give its expected failures as a list.` },
|
|
476
|
+
{ kind: 'case-no-expected', when: (r) => r.code === 'calibration-case' && /\.expect$/.test(r.path),
|
|
477
|
+
say: (_r, c) => `${c} has no expected result.` },
|
|
478
|
+
{ kind: 'case-verdict-bad', when: (r) => r.code === 'calibration-case' && /\.expect\.verdict$/.test(r.path),
|
|
479
|
+
say: (_r, c) => `${c} expects a result that is neither pass nor fail.` },
|
|
480
|
+
{ kind: 'case-fail-malformed', when: (r) => r.code === 'calibration-case' && /\.reds\[\d+\]$/.test(r.path) && /^an expected red is/.test(r.detail),
|
|
481
|
+
say: (_r, c) => `${c} has an expected failure that is not written as a rule and a function.` },
|
|
482
|
+
{ kind: 'case-fail-repeat', when: (r) => r.code === 'calibration-case' && /\.reds\[\d+\]$/.test(r.path),
|
|
483
|
+
say: (_r, c) => `${c} lists the same failure twice.` },
|
|
484
|
+
{ kind: 'case-fail-rule-foreign', when: (r) => r.code === 'calibration-case' && /\.rule$/.test(r.path) && typeof r.rule === 'string',
|
|
485
|
+
say: (r, c) => `${c} expects a failure from the rule "${safeText(r.rule)}", which this rubric does not use.` },
|
|
486
|
+
{ kind: 'case-fail-rule-missing', when: (r) => r.code === 'calibration-case' && /\.rule$/.test(r.path),
|
|
487
|
+
say: (_r, c) => `${c} has an expected failure with no rule named.` },
|
|
488
|
+
{ kind: 'case-fail-fn-missing', when: (r) => r.code === 'calibration-case' && /\.fn$/.test(r.path),
|
|
489
|
+
say: (_r, c) => `${c} has an expected failure with no function named.` },
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
/** @param {PlainRed} r the 1-based rubric line a card red lands on @returns {number} */
|
|
493
|
+
function cardLineNo(r) {
|
|
494
|
+
return Number(/^card item (\d+):/.exec(r.detail)?.[1] ?? -1) + 1;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Every plain kind the table can emit — the list the test enumerates against.
|
|
499
|
+
* @returns {string[]}
|
|
500
|
+
*/
|
|
501
|
+
export function plainKinds() {
|
|
502
|
+
return PLAIN_TABLE.map((e) => e.kind);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Map the technical reds of a refused proposal to plain sentences, one per red.
|
|
507
|
+
* An unknown code is NEVER dropped: it becomes a generic sentence that still
|
|
508
|
+
* quotes the code and path, under the kind `unmapped`.
|
|
509
|
+
* @param {PlainRed[]} reds
|
|
510
|
+
* @param {{cases?: any}} [o] the proposed cases, to name a case by its own id
|
|
511
|
+
* @returns {PlainReason[]}
|
|
512
|
+
*/
|
|
513
|
+
export function plainReasons(reds, { cases = null } = {}) {
|
|
514
|
+
return (Array.isArray(reds) ? reds : []).map((red) => {
|
|
515
|
+
const r = /** @type {PlainRed} */ ({ ...red, code: String(red?.code ?? ''), path: String(red?.path ?? ''), detail: String(red?.detail ?? '') });
|
|
516
|
+
const idx = /^calibration\.cases\[(\d+)\]/.exec(r.path)?.[1];
|
|
517
|
+
const given = idx !== undefined && Array.isArray(cases) ? cases[Number(idx)]?.id : undefined;
|
|
518
|
+
const caseName = idx === undefined ? 'A case'
|
|
519
|
+
: typeof given === 'string' && given.trim() !== '' ? `Case "${safeText(given)}"` : `Case ${Number(idx) + 1}`;
|
|
520
|
+
const hit = PLAIN_TABLE.find((e) => e.when(r));
|
|
521
|
+
if (hit) return { kind: hit.kind, sentence: hit.say(r, caseName, null), red: r };
|
|
522
|
+
return {
|
|
523
|
+
kind: 'unmapped',
|
|
524
|
+
sentence: `The model's proposal failed a check that has no plain wording yet ("${safeText(r.code)}" at ${safeText(r.path)}).`,
|
|
525
|
+
red: r,
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** @param {number} n @returns {string} the panel's money spelling: 2 decimals, "<$0.01" under a cent */
|
|
531
|
+
function moneyText(n) {
|
|
532
|
+
return n > 0 && n < 0.005 ? '<$0.01' : `$${n.toFixed(2)}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The money clause: the measured figure, "at least" when a call was unpriced,
|
|
537
|
+
* and `null` when there is no honest figure (the caller then says unknown or
|
|
538
|
+
* drops the clause — never a zero).
|
|
539
|
+
* @param {{knownUsd?: number|null, spendComplete?: boolean|null}|null|undefined} spend
|
|
540
|
+
* @returns {string|null}
|
|
541
|
+
*/
|
|
542
|
+
function spendText(spend) {
|
|
543
|
+
if (!spend || typeof spend.knownUsd !== 'number' || !Number.isFinite(spend.knownUsd)) return null;
|
|
544
|
+
return `${spend.spendComplete === false ? 'at least ' : ''}${moneyText(spend.knownUsd)}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** the stops this module explains in plain words */
|
|
548
|
+
export const PLAIN_PROPOSAL_STOPS = Object.freeze(['proposal-invalid', 'rubric-invalid']);
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* THE MESSAGE a person reads when the rubric proposal was refused. Both doors
|
|
552
|
+
* (the panel's failed step and the CLI) print exactly this.
|
|
553
|
+
* @param {{stop: string, reds: PlainRed[], cases?: any, spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null,
|
|
554
|
+
* source?: 'proposal'|'signer'}} o
|
|
555
|
+
* @returns {string}
|
|
556
|
+
*/
|
|
557
|
+
export function proposalStopText({ stop, reds, cases = null, spend = null, source = 'proposal' }) {
|
|
558
|
+
const reasons = plainReasons(reds, { cases });
|
|
559
|
+
const unique = [...new Set(reasons.map((x) => x.sentence))];
|
|
560
|
+
const shown = unique.slice(0, PLAIN_MAX_BULLETS);
|
|
561
|
+
const onlyCases = reasons.length > 0 && reasons.every((x) => x.kind.startsWith('case-') || x.kind.startsWith('set-'));
|
|
562
|
+
const spent = spendText(spend);
|
|
563
|
+
const head = stop === 'rubric-invalid'
|
|
564
|
+
? 'The rubric and practice cases failed the last check before signing, so nothing was signed.'
|
|
565
|
+
: `The model's ${onlyCases ? 'practice cases for your rubric' : 'rubric and practice cases'} didn't hold up, so we stopped before grading anything.`;
|
|
566
|
+
const lines = [`${head} Spent so far: ${spent ?? 'unknown'}.`, 'What was wrong:'];
|
|
567
|
+
for (const s of shown) lines.push(` • ${s}`);
|
|
568
|
+
if (unique.length > shown.length) lines.push(` • …and ${unique.length - shown.length} more.`);
|
|
569
|
+
lines.push(source === 'signer' ? 'This one is from the edit made before signing.' : 'This is the model\'s mistake, not yours.');
|
|
570
|
+
lines.push(`What to do: draft again${spent ? ` (about ${spent} to reach here)` : ''}.`);
|
|
571
|
+
lines.push('If it fails the same way twice, make your PASS/FAIL examples more concrete.');
|
|
572
|
+
return lines.join('\n');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── THE SIGNING GATES' REFUSAL, in plain words (hamr, 2026-10-09) ──────────────────────────
|
|
576
|
+
//
|
|
577
|
+
// `prepareSigning` (src/authorjob.js) can refuse with a list of reds from four gates. The panel used to say
|
|
578
|
+
// `signing gates failed — calibration-miswrite, calibration-miswrite, calibration-miswrite, injection-leak`.
|
|
579
|
+
// Same principle as the proposal text above ("a user should get a reason that they can understand and do
|
|
580
|
+
// something about"): one owner, written by code, the structured reds travel beside it in the drafting log.
|
|
581
|
+
//
|
|
582
|
+
// WHOSE FAULT. A calibration-miswrite is NOT claimed to be the model's mistake: the practice case was written by
|
|
583
|
+
// the model, but the check can be the one that is wrong (live, 2026-10-09: two of three misses were our rule).
|
|
584
|
+
// The text says the practice case and the check DISAGREE, and leaves it at that.
|
|
585
|
+
//
|
|
586
|
+
// EVERY RED CODE REACHABLE AT THE REFUSAL, enumerated from source:
|
|
587
|
+
// gate 1a/1b (the declaration; validateJob + validateCloseDecl): DECLARATION_CODES below, plus seed-unreadable
|
|
588
|
+
// and listing-unreadable (authorjob.js)
|
|
589
|
+
// gate 2 (precheck): broken-close
|
|
590
|
+
// gate 4 (calibration; calibrate.js + authorjob.js calibrationGate): calibration-missing, no-judge-seam,
|
|
591
|
+
// cap-halt, pricing-red, provider-red, artifact-red (the casualty axes), calibration-miswrite,
|
|
592
|
+
// injection-leak, and the invalid-set reds (invalid-value at `card`, calibration-case / -size / -polarity /
|
|
593
|
+
// -cardless / -orphan) which the proposal table above already words
|
|
594
|
+
// gate 3 refuses with NO red at all (refusal only: the close already passes at the seed)
|
|
595
|
+
// An unknown code is never dropped: it becomes a generic sentence that quotes the code.
|
|
596
|
+
|
|
597
|
+
/** codes the declaration gates (validateJob, validateCloseDecl) raise: one plain sentence covers the family */
|
|
598
|
+
export const DECLARATION_CODES = Object.freeze([
|
|
599
|
+
'bounds', 'class-absent', 'class-battery-locked', 'class-ceiling', 'cmd-denied', 'duplicate-kind', 'duplicate-name',
|
|
600
|
+
'env-ownership-absent', 'genre-env-missing', 'genre-env-ungrounded', 'genre-owned-env', 'guard-missing',
|
|
601
|
+
'guard-weakened', 'guards-absent', 'human-stage-not-last', 'human-stage-offered', 'invalid-value', 'judged-outside-write-scope', 'judged-stage-order',
|
|
602
|
+
'listing-absent', 'listing-conflict', 'locked-kind', 'missing-field', 'missing-required', 'one-population',
|
|
603
|
+
'path-not-in-listing', 'secret-literal', 'unknown-field', 'unknown-kind',
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
/** the rulebook rules in the words of a person: what the rule flags */
|
|
607
|
+
const RULE_FLAGS = Object.freeze({
|
|
608
|
+
'has-doc': 'there is no doc comment',
|
|
609
|
+
'says-what': 'the doc only repeats the function\'s name',
|
|
610
|
+
params: 'a parameter is missing from the doc, or the doc names one that does not exist',
|
|
611
|
+
returns: 'the return value is not documented',
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
/** @param {unknown} rule @returns {string} */
|
|
615
|
+
const ruleFlag = (rule) => (typeof rule === 'string' && Object.hasOwn(RULE_FLAGS, rule)
|
|
616
|
+
? /** @type {Record<string,string>} */ (RULE_FLAGS)[rule] : `the rule "${safeText(rule)}" applies`);
|
|
617
|
+
|
|
618
|
+
/** "says-what · initCache; params · initCache" to [{rule, fn}] @param {string} list */
|
|
619
|
+
const ruleFns = (list) => list.split(';').map((x) => x.trim()).filter(Boolean).map((x) => {
|
|
620
|
+
const [rule, fn] = x.split('·').map((t) => t.trim());
|
|
621
|
+
return { rule, fn: fn ?? '' };
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* A calibration-miswrite red as plain sentences: what the practice case expected against what the check found,
|
|
626
|
+
* read off the red's own detail (the compareExpectation pieces calibrate.js writes).
|
|
627
|
+
* @param {PlainRed} r @returns {string[]}
|
|
628
|
+
*/
|
|
629
|
+
function miswriteSentences(r) {
|
|
630
|
+
const who = `Case "${safeText(r.caseId ?? /^case "(.*?)" was graded/.exec(r.detail)?.[1] ?? '')}"`;
|
|
631
|
+
const missing = /expected red\(s\) the pipe did not raise: (.*?)(?: — |\. The floor)/.exec(r.detail)?.[1];
|
|
632
|
+
const extra = /red\(s\) the pipe raised that nobody signed: (.*?)(?: — |\. The floor)/.exec(r.detail)?.[1];
|
|
633
|
+
/** @type {string[]} */
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const { rule, fn } of missing ? ruleFns(missing) : []) {
|
|
636
|
+
out.push(`${who}${fn ? ` (${safeText(fn)})` : ''}: the practice case says ${ruleFlag(rule)}, but the check passed it.`);
|
|
637
|
+
}
|
|
638
|
+
for (const { rule, fn } of extra ? ruleFns(extra) : []) {
|
|
639
|
+
out.push(`${who}${fn ? ` (${safeText(fn)})` : ''}: the check says ${ruleFlag(rule)}, but the practice case did not expect that.`);
|
|
640
|
+
}
|
|
641
|
+
if (out.length === 0) {
|
|
642
|
+
const m = /signed "(\w+)", the pipe rendered "(\w+)"/.exec(r.detail);
|
|
643
|
+
out.push(m
|
|
644
|
+
? `${who}: the practice case says ${m[1] === 'red' ? 'fail' : 'pass'}, but the check said ${m[2] === 'red' ? 'fail' : 'pass'}.`
|
|
645
|
+
: `${who}: the practice case and the check disagree.`);
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* @type {{kind: string, when: (r: PlainRed) => boolean, say: (r: PlainRed) => string[], todo: string}[]}
|
|
652
|
+
* Order matters; the first match wins. `todo` is the one thing the person can do.
|
|
653
|
+
*/
|
|
654
|
+
const SIGNING_TABLE = [
|
|
655
|
+
{ kind: 'miswrite', when: (r) => r.code === 'calibration-miswrite', say: miswriteSentences, todo: 'draft again' },
|
|
656
|
+
{ kind: 'injection-leak', when: (r) => r.code === 'injection-leak',
|
|
657
|
+
say: (r) => [`A test file tried to trick the judge ("${safeText(r.style ?? '')}"), and what the judge pointed at changed, so the judge could not be shown to resist it.`],
|
|
658
|
+
todo: 'draft again' },
|
|
659
|
+
{ kind: 'calibration-missing', when: (r) => r.code === 'calibration-missing',
|
|
660
|
+
say: () => ['The rubric came without practice cases, so the check was never tried out.'], todo: 'draft again' },
|
|
661
|
+
{ kind: 'no-judge', when: (r) => r.code === 'no-judge-seam',
|
|
662
|
+
say: () => ['No judge model was set up to try out the rubric, so it was never graded.'], todo: 'check the judge model and its key in Settings, then try again' },
|
|
663
|
+
{ kind: 'cap-halt', when: (r) => r.code === 'cap-halt',
|
|
664
|
+
say: () => ['The spending cap ran out while the practice cases were being graded. Nothing says the rubric is wrong.'], todo: 'raise the cap, then draft again' },
|
|
665
|
+
{ kind: 'pricing-red', when: (r) => r.code === 'pricing-red',
|
|
666
|
+
say: () => ['A model call could not be priced, so the cap could not be applied; bareloop stopped rather than spend blind.'], todo: 'set the price for this model in Settings, then try again' },
|
|
667
|
+
{ kind: 'provider-red', when: (r) => r.code === 'provider-red',
|
|
668
|
+
say: () => ['The judge model could not be reached or was cut off while the practice cases were graded. Nothing says the rubric is wrong.'], todo: 'try again in a few minutes' },
|
|
669
|
+
{ kind: 'artifact-red', when: (r) => r.code === 'artifact-red',
|
|
670
|
+
say: () => ['The judge model answered with something bareloop could not read while the practice cases were graded. Nothing says the rubric is wrong.'], todo: 'try again' },
|
|
671
|
+
{ kind: 'broken-close', when: (r) => r.code === 'broken-close',
|
|
672
|
+
say: (r) => [`A check of the close (${safeText(r.path.split('.').pop() ?? '')}) cannot run on this repository, so nothing it reported could be trusted.`],
|
|
673
|
+
todo: 'check that the project\'s tools (its install, build and test commands) run, then try again' },
|
|
674
|
+
{ kind: 'seed-unreadable', when: (r) => r.code === 'seed-unreadable',
|
|
675
|
+
say: () => ['bareloop could not read the repository\'s starting commit.'], todo: 'check that the source is a git repository with at least one commit' },
|
|
676
|
+
{ kind: 'listing-unreadable', when: (r) => r.code === 'listing-unreadable',
|
|
677
|
+
say: () => ['bareloop could not list the files of the repository\'s starting commit.'], todo: 'check that the source is a git repository with at least one commit' },
|
|
678
|
+
{ kind: 'judged-outside-fence', when: (r) => r.code === 'judged-outside-write-scope',
|
|
679
|
+
say: (r) => [`The rubric check looks at ${listText(r.outside)}, but the job may only edit ${listText(r.fence)} — so the run could never fix what it finds.`],
|
|
680
|
+
todo: 'draft again' },
|
|
681
|
+
{ kind: 'declaration', when: (r) => DECLARATION_CODES.includes(r.code) && !(r.code === 'invalid-value' && r.path === 'card'),
|
|
682
|
+
say: (r) => [`The close the model wrote was refused before anything ran ("${safeText(r.code)}" at ${safeText(r.path)}).`], todo: 'draft again' },
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
/** a red's string list as plain text, each entry scrubbed and bounded
|
|
686
|
+
* @param {unknown} v @returns {string} */
|
|
687
|
+
const listText = (v) => {
|
|
688
|
+
const items = (Array.isArray(v) ? v : []).filter((x) => typeof x === 'string' && x !== '').slice(0, 4).map((x) => safeText(x));
|
|
689
|
+
return items.length ? items.join(', ') : 'files outside the edit area';
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/** the sentence when the gates refuse with no red at all (the close already passes on the untouched repository) */
|
|
693
|
+
const SEED_GREEN_SENTENCE = 'Nothing in the close fails on the repository as it is, so there is nothing for a run to do.';
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Plain sentences for the reds of a refused signing, one or more per red. Card and practice-case reds reuse the
|
|
697
|
+
* proposal table; an unknown code is a generic sentence that quotes the code.
|
|
698
|
+
* @param {PlainRed[]} reds @param {{cases?: any}} [o]
|
|
699
|
+
* @returns {{kind: string, sentence: string, todo: string, red: PlainRed}[]}
|
|
700
|
+
*/
|
|
701
|
+
export function signingReasons(reds, { cases = null } = {}) {
|
|
702
|
+
/** @type {{kind: string, sentence: string, todo: string, red: PlainRed}[]} */
|
|
703
|
+
const out = [];
|
|
704
|
+
for (const red of Array.isArray(reds) ? reds : []) {
|
|
705
|
+
const r = /** @type {PlainRed} */ ({ ...red, code: String(red?.code ?? ''), path: String(red?.path ?? ''), detail: String(red?.detail ?? '') });
|
|
706
|
+
const hit = SIGNING_TABLE.find((e) => e.when(r));
|
|
707
|
+
if (hit) { for (const sentence of hit.say(r)) out.push({ kind: hit.kind, sentence, todo: hit.todo, red: r }); continue; }
|
|
708
|
+
const [pr] = plainReasons([r], { cases });
|
|
709
|
+
if (pr && pr.kind !== 'unmapped') { out.push({ kind: pr.kind, sentence: pr.sentence, todo: 'draft again', red: r }); continue; }
|
|
710
|
+
out.push({
|
|
711
|
+
kind: 'unmapped',
|
|
712
|
+
sentence: `A check before signing failed and has no plain wording yet ("${safeText(r.code)}" at ${safeText(r.path)}).`,
|
|
713
|
+
todo: 'draft again; if it fails the same way, report the code above',
|
|
714
|
+
red: r,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** every plain kind `signingReasons` can emit besides the proposal table's — what the test enumerates
|
|
721
|
+
* @returns {string[]} */
|
|
722
|
+
export function signingKinds() {
|
|
723
|
+
return [...SIGNING_TABLE.map((e) => e.kind), 'seed-green', 'unmapped'];
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* THE MESSAGE a person reads when the signing gates refuse. The panel's failed step and the CLI print exactly this.
|
|
728
|
+
* @param {{reds: PlainRed[], refusal?: {detail?: string}|null, cases?: any,
|
|
729
|
+
* spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null}} o
|
|
730
|
+
* @returns {string}
|
|
731
|
+
*/
|
|
732
|
+
export function signingStopText({ reds, refusal = null, cases = null, spend = null }) {
|
|
733
|
+
const reasons = signingReasons(reds, { cases });
|
|
734
|
+
if (reasons.length === 0) {
|
|
735
|
+
reasons.push({ kind: 'seed-green', sentence: SEED_GREEN_SENTENCE, todo: 'check that the close measures the right thing, or describe a sharper problem',
|
|
736
|
+
red: { code: '', path: '', detail: String(refusal?.detail ?? '') } });
|
|
737
|
+
}
|
|
738
|
+
const unique = [...new Set(reasons.map((x) => x.sentence))];
|
|
739
|
+
const shown = unique.slice(0, PLAIN_MAX_BULLETS);
|
|
740
|
+
const spent = spendText(spend);
|
|
741
|
+
const lines = [`The checks before signing did not all pass, so nothing was signed. Spent so far: ${spent ?? 'unknown'}.`, 'What was wrong:'];
|
|
742
|
+
for (const s of shown) lines.push(` • ${s}`);
|
|
743
|
+
if (unique.length > shown.length) lines.push(` • …and ${unique.length - shown.length} more.`);
|
|
744
|
+
if (reasons.some((x) => x.kind === 'miswrite' || x.kind === 'injection-leak')) {
|
|
745
|
+
lines.push('The practice cases and the check disagree; either one can be the one that is wrong, so this is not necessarily your doing or the model\'s.');
|
|
746
|
+
}
|
|
747
|
+
lines.push(`What to do: ${reasons[0].todo}${reasons[0].todo === 'draft again' && spent ? ` (about ${spent} to reach here)` : ''}.`);
|
|
748
|
+
return lines.join('\n');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* The reds as the drafting log keeps them: every structured field, the plain
|
|
753
|
+
* sentence beside it, every string secret-scrubbed and bounded, the list capped.
|
|
754
|
+
* @param {PlainRed[]} reds @param {{cases?: any, signing?: boolean}} [o] `signing`: the reds are the signing gates', worded by signingReasons
|
|
755
|
+
* @returns {Record<string, any>[]}
|
|
756
|
+
*/
|
|
757
|
+
export function redsRecord(reds, { cases = null, signing = false } = {}) {
|
|
758
|
+
/** @param {unknown} s */
|
|
759
|
+
const cut = (s) => { const t = redactSecrets(String(s ?? '')); return t.length > RECORD_STR_MAX ? `${t.slice(0, RECORD_STR_MAX)}…` : t; };
|
|
760
|
+
return (signing ? signingReasons(reds, { cases }) : plainReasons(reds, { cases })).slice(0, RECORD_MAX_REDS).map(({ kind, sentence, red }) => {
|
|
761
|
+
/** @type {Record<string, any>} */
|
|
762
|
+
const out = { kind, plain: sentence };
|
|
763
|
+
for (const [k, v] of Object.entries(red)) {
|
|
764
|
+
if (typeof v === 'string') out[k] = cut(v);
|
|
765
|
+
else if (typeof v === 'number' || typeof v === 'boolean' || v === null) out[k] = v;
|
|
766
|
+
else if (Array.isArray(v)) out[k] = v.slice(0, 10).map((x) => (typeof x === 'string' ? cut(x) : x));
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
});
|
|
770
|
+
}
|
package/src/authorrun.js
CHANGED
|
@@ -128,7 +128,7 @@ import { readSourceManifest, missingDependencies, nonRepoSourceMessage } from '.
|
|
|
128
128
|
import { tallyCalls } from './text.js';
|
|
129
129
|
import {
|
|
130
130
|
declarationLines, rubricLines, calibrationLines, parseCeiling, ceilingLine, crashRecord, phaseLine,
|
|
131
|
-
openQuestionLines, answeredQuestionLines, fellBackLines,
|
|
131
|
+
openQuestionLines, answeredQuestionLines, fellBackLines, PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText,
|
|
132
132
|
} from './authorreadout.js';
|
|
133
133
|
|
|
134
134
|
/** Thrown to unwind `main` to an exit code without ever calling
|
|
@@ -978,6 +978,15 @@ export async function main(argv, deps = {}) {
|
|
|
978
978
|
if (r.red) out(` red: ${JSON.stringify(r.red)}`);
|
|
979
979
|
for (const e of refusalEvents(r)) emit(e.type, e);
|
|
980
980
|
}
|
|
981
|
+
// a refused rubric proposal reads in plain words FIRST (the same text the panel shows), the technical reds after
|
|
982
|
+
if (PLAIN_PROPOSAL_STOPS.includes(String(authored.stop))) {
|
|
983
|
+
out(`\n${proposalStopText({
|
|
984
|
+
stop: String(authored.stop), reds: authored.reds ?? [],
|
|
985
|
+
cases: authored.judged?.signed?.cases ?? authored.judged?.proposal?.proposal?.cases ?? null,
|
|
986
|
+
spend: authored.cost ? { knownUsd: authored.cost.knownUsd ?? null, spendComplete: authored.cost.spendComplete ?? null } : null,
|
|
987
|
+
source: authored.judged?.source === 'signer' ? 'signer' : 'proposal',
|
|
988
|
+
})}`);
|
|
989
|
+
}
|
|
981
990
|
for (const red of authored.reds ?? []) {
|
|
982
991
|
out(`\nRED ${red.code} at ${red.path}\n${red.detail}`);
|
|
983
992
|
emit('job-red', red);
|
|
@@ -1181,16 +1190,16 @@ export async function main(argv, deps = {}) {
|
|
|
1181
1190
|
: (g.seedVerdict.ok
|
|
1182
1191
|
? `PASS — work red at seed: ${g.seedVerdict.workRed.join(', ')}`
|
|
1183
1192
|
: (g.seedVerdict.satisfiedBy === 'calibration'
|
|
1184
|
-
? 'PASS via the CALIBRATION gate — this close
|
|
1193
|
+
? 'PASS via the CALIBRATION gate — this close carries a judged stage'
|
|
1185
1194
|
: 'FAIL — no work stage is red at the seed'))}`);
|
|
1186
1195
|
if (g.seedVerdict) {
|
|
1187
1196
|
out(` red at seed: ${g.seedVerdict.redAtSeed.join(', ') || '(none)'}`);
|
|
1188
1197
|
out(` green at seed: ${g.seedVerdict.greenAtSeed.join(', ') || '(none)'}`);
|
|
1189
|
-
// ruling 3: a judged
|
|
1198
|
+
// ruling 3 (widened 2026-10-09): a close with a judged stage clears gate 3 on its calibration instead,
|
|
1190
1199
|
// and the surface SAYS which proof carried it rather than leaving a reader
|
|
1191
1200
|
// to wonder why an empty workRed list passed
|
|
1192
1201
|
if (g.seedVerdict.satisfiedBy === 'calibration') {
|
|
1193
|
-
out(' a judged stage skips the seed read (ruling 8), so this close\'s proof that
|
|
1202
|
+
out(' a judged stage skips the seed read (ruling 8), so this close\'s proof that its judged stage CAN fail is');
|
|
1194
1203
|
out(' the graded calibration set below — signed cases it must red as well as ones it must pass.');
|
|
1195
1204
|
}
|
|
1196
1205
|
}
|
|
@@ -1207,6 +1216,14 @@ export async function main(argv, deps = {}) {
|
|
|
1207
1216
|
if ((signing.guards ?? []).length) { out('\nguard stages at the seed'); for (const r of signing.guards) row(r); }
|
|
1208
1217
|
if ((signing.stops ?? []).length) { out('\nstages that could NOT RUN (a broken instrument is a casualty, never a verdict)'); for (const r of signing.stops) row(r); }
|
|
1209
1218
|
|
|
1219
|
+
// a refused signing reads in plain words FIRST (the same text the panel shows), the technical reds after
|
|
1220
|
+
if (!signing.ok) {
|
|
1221
|
+
const t = costSoFar();
|
|
1222
|
+
out(`\n${signingStopText({
|
|
1223
|
+
reds: signing.reds ?? [], refusal: signing.refusal,
|
|
1224
|
+
spend: { knownUsd: t.knownUsd ?? null, spendComplete: t.spendComplete ?? null },
|
|
1225
|
+
})}`);
|
|
1226
|
+
}
|
|
1210
1227
|
if (signing.refusal) {
|
|
1211
1228
|
const r = signing.refusal;
|
|
1212
1229
|
out(`\nREFUSED (${r.kind}) verb=${r.verb ?? 'none'} path=${r.path}`);
|