bareloop 0.37.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 +53 -0
- package/bareloop.context.md +48 -26
- 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/legs.js +15 -0
- package/src/panel/authorroutes.js +9 -6
- package/src/panel/authorsession.js +66 -49
- package/src/panel/index.html +321 -355
- package/src/panel/server.js +33 -13
- package/src/panel/status.js +5 -4
- package/src/planrun.js +4 -3
- package/src/providers.js +27 -0
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- 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/legs.d.ts +12 -0
- package/types/panel/authorsession.d.ts +15 -1
- package/types/panel/server.d.ts +5 -4
- package/types/panel/status.d.ts +1 -1
- package/types/providers.d.ts +7 -0
- package/types/ralph.d.ts +6 -1
- package/types/trend.d.ts +1 -0
package/src/calibrate.js
CHANGED
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
import { createHash } from 'node:crypto';
|
|
62
62
|
import {
|
|
63
63
|
LOCATE_LABEL, LOCATE_AXES, CALIBRATION_SIZE,
|
|
64
|
-
runLocate, decide, expectedOf, validateCard, validateCalibrationSet,
|
|
64
|
+
runLocate, decide, expectedOf, validateCard, validateCalibrationSet, docQuoteInBlock,
|
|
65
65
|
} from './judged.js';
|
|
66
66
|
import { JUDGE_ATTEMPTS } from './kinds.js';
|
|
67
|
-
import { tallyCalls } from './text.js';
|
|
67
|
+
import { tallyCalls, scrubRaw } from './text.js';
|
|
68
68
|
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
69
69
|
|
|
70
70
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -280,20 +280,87 @@ export function factsResist(facts, gold, artifactText) {
|
|
|
280
280
|
if (gotNames.join(',') !== wantNames.join(',')) {
|
|
281
281
|
return { ok: false, why: `the located function list moved: expected ${wantNames.join(', ')}, got ${gotNames.join(', ') || '(none)'}` };
|
|
282
282
|
}
|
|
283
|
-
const lines = new Set(artifactText.split('\n').map((l) => l.trim()).filter(Boolean));
|
|
284
283
|
for (const w of want) {
|
|
285
284
|
const f = got.find((/** @type {any} */ x) => String(x?.name ?? '') === w.name);
|
|
286
285
|
const q = typeof f?.docQuote === 'string' && f.docQuote.trim() !== '' ? f.docQuote : null;
|
|
287
286
|
if (!w.hasDoc && q !== null) {
|
|
288
287
|
return { ok: false, why: `${w.name} has no JSDoc block and the extraction reported one` };
|
|
289
288
|
}
|
|
290
|
-
|
|
289
|
+
// ONE OWNER of "this quote points at the real doc block" (docQuoteInBlock, the same call
|
|
290
|
+
// `has-doc`'s decide() makes). The judge routinely quotes a worded line inside the block
|
|
291
|
+
// rather than the `/**` opener, and since ruling A (0f27db5) that is a good pointer; the
|
|
292
|
+
// declaration it names must be this function's own, so a block above another one still leaks.
|
|
293
|
+
const decl = typeof f?.declarationQuote === 'string' ? f.declarationQuote : null;
|
|
294
|
+
if (w.hasDoc && (q === null || decl === null || !decl.includes(w.name) || !docQuoteInBlock(artifactText, decl, q))) {
|
|
291
295
|
return { ok: false, why: `${w.name} IS documented and the extraction did not report its real JSDoc opener` };
|
|
292
296
|
}
|
|
293
297
|
}
|
|
294
298
|
return { ok: true, why: null };
|
|
295
299
|
}
|
|
296
300
|
|
|
301
|
+
// ── THE DIAGNOSTIC RECORD (F192) ────────────────────────────────────────────
|
|
302
|
+
//
|
|
303
|
+
// A refusal must be diagnosable from its OWN record. `expectedOf` drops `why` and
|
|
304
|
+
// `quote` on purpose (a signer cannot predict the judge's wording, so grading must
|
|
305
|
+
// not pin it), and the judge's raw locate facts used to be written nowhere — so a
|
|
306
|
+
// `has-doc` red could not be told apart as "no docQuote", "not a /** opener" or
|
|
307
|
+
// "a quoted line is not in the artifact". `diag` is a NEW field beside the graded
|
|
308
|
+
// ones: nothing here is read by grading, the compare, the hashes or any verdict.
|
|
309
|
+
// Every string passes through `scrubRaw` — the ONE secret inventory plus the one
|
|
310
|
+
// announced bound — so a quote that carries a key is masked and a long one says
|
|
311
|
+
// how much was withheld rather than silently clipping.
|
|
312
|
+
|
|
313
|
+
/** the per-string cap on anything the diagnostic keeps (bytes, scrubbed) */
|
|
314
|
+
export const DIAG_STRING_CAP = 400;
|
|
315
|
+
/** the most elements of one array the diagnostic keeps before it says it stopped */
|
|
316
|
+
const DIAG_ARRAY_CAP = 32;
|
|
317
|
+
|
|
318
|
+
/** @param {unknown} v @returns {string} */
|
|
319
|
+
const diagText = (v) => scrubRaw({ label: 'diag', attempt: 0, text: v, cap: DIAG_STRING_CAP }).text;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* A facts tree, deep-copied with every string scrubbed and bounded. Depth is
|
|
323
|
+
* bounded too: facts are a shallow list of functions, and anything deeper is not
|
|
324
|
+
* a shape this pipe reads.
|
|
325
|
+
* @param {any} v @param {number} [depth] @returns {any}
|
|
326
|
+
*/
|
|
327
|
+
function diagFacts(v, depth = 0) {
|
|
328
|
+
if (typeof v === 'string') return diagText(v);
|
|
329
|
+
if (v === null || typeof v !== 'object') return v;
|
|
330
|
+
if (depth >= 4) return diagText(JSON.stringify(v));
|
|
331
|
+
if (Array.isArray(v)) {
|
|
332
|
+
const kept = v.slice(0, DIAG_ARRAY_CAP).map((x) => diagFacts(x, depth + 1));
|
|
333
|
+
if (v.length > DIAG_ARRAY_CAP) kept.push(`[${v.length - DIAG_ARRAY_CAP} more element(s) withheld — the cap is ${DIAG_ARRAY_CAP} per array]`);
|
|
334
|
+
return kept;
|
|
335
|
+
}
|
|
336
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, diagFacts(x, depth + 1)]));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* What one graded artifact's record KEEPS about how it was judged: every locate
|
|
341
|
+
* attempt's axis and cause, the judge's facts as returned, and the decision's
|
|
342
|
+
* full reds per rule.
|
|
343
|
+
* @param {{diagTries: any[], facts: any, decision: any}} r
|
|
344
|
+
*/
|
|
345
|
+
function diagOf({ diagTries, facts, decision }) {
|
|
346
|
+
/** @type {{rule: string, fn: string, why: string, quote: string|null}[]} */
|
|
347
|
+
const reds = [];
|
|
348
|
+
for (const item of decision?.items ?? []) {
|
|
349
|
+
for (const x of item?.reds ?? []) {
|
|
350
|
+
reds.push({
|
|
351
|
+
rule: String(item?.rule ?? ''), fn: String(x?.fn ?? ''), why: diagText(x?.why),
|
|
352
|
+
quote: typeof x?.quote === 'string' ? diagText(x.quote) : null,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
attempts: diagTries,
|
|
358
|
+
facts: facts === null || facts === undefined ? null : diagFacts(facts),
|
|
359
|
+
reds,
|
|
360
|
+
reason: typeof decision?.reason === 'string' ? diagText(decision.reason) : null,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
297
364
|
// ── THE GATE ────────────────────────────────────────────────────────────────
|
|
298
365
|
|
|
299
366
|
/**
|
|
@@ -320,6 +387,10 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
320
387
|
let last = null;
|
|
321
388
|
/** @type {any[]} */
|
|
322
389
|
const tries = [];
|
|
390
|
+
/** the diagnostic twin of `tries` (F192): the same attempts, with the cause text
|
|
391
|
+
* `tries` never carried. A separate array so `casualty.attempts` is unchanged.
|
|
392
|
+
* @type {any[]} */
|
|
393
|
+
const diagTries = [];
|
|
323
394
|
// TIGHTEN-ONLY, FLOOR 1 — the same clamp shape every bound in this pipeline
|
|
324
395
|
// carries. A caller may buy fewer retries than the ladder allows and never
|
|
325
396
|
// more; ZERO is not a cheaper gate, it is a gate that grades nothing and has
|
|
@@ -327,7 +398,7 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
327
398
|
const bound = Math.max(1, Math.min(Math.trunc(Number(attempts)) || 0, JUDGE_ATTEMPTS));
|
|
328
399
|
for (let attempt = 1; attempt <= bound; attempt += 1) {
|
|
329
400
|
const halt = capStop();
|
|
330
|
-
if (halt) return { ok: false, budget: halt, tries, red: null, facts: null, decision: null };
|
|
401
|
+
if (halt) return { ok: false, budget: halt, tries, diagTries, red: null, facts: null, decision: null };
|
|
331
402
|
last = await runLocate({
|
|
332
403
|
artifactText,
|
|
333
404
|
card,
|
|
@@ -348,11 +419,12 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
348
419
|
}),
|
|
349
420
|
});
|
|
350
421
|
tries.push({ attempt, ok: last.ok, axis: last.red?.axis ?? null, costUsd: last.costUsd });
|
|
422
|
+
diagTries.push({ attempt, ok: last.ok, axis: last.red?.axis ?? null, detail: last.red ? diagText(last.red.detail) : null });
|
|
351
423
|
if (last.ok) break;
|
|
352
424
|
if (last.red.axis === LOCATE_AXES.PRICING) break;
|
|
353
425
|
}
|
|
354
|
-
if (!last.ok) return { ok: false, budget: null, tries, red: last.red, facts: null, decision: null };
|
|
355
|
-
return { ok: true, budget: null, tries, red: null, facts: last.facts, decision: decide(last.facts, card, { artifactText }) };
|
|
426
|
+
if (!last.ok) return { ok: false, budget: null, tries, diagTries, red: last.red, facts: null, decision: null };
|
|
427
|
+
return { ok: true, budget: null, tries, diagTries, red: null, facts: last.facts, decision: decide(last.facts, card, { artifactText }) };
|
|
356
428
|
}
|
|
357
429
|
|
|
358
430
|
/**
|
|
@@ -393,7 +465,7 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
393
465
|
* onCost?: (c: any) => void, attempts?: number, battery?: any[], batteryCard?: any,
|
|
394
466
|
* capStop?: () => 'cap-halt'|'pricing-red'|null}} o
|
|
395
467
|
* @returns {Promise<{ok: boolean, stop: string|null, casualty: any,
|
|
396
|
-
* graded: {id: string, ok: boolean, got: any, want: any, detail: string|null}[],
|
|
468
|
+
* graded: {id: string, ok: boolean, got: any, want: any, detail: string|null, diag: any}[],
|
|
397
469
|
* injection: {styles: any[], allResisted: boolean, leaks: string[]},
|
|
398
470
|
* failures: string[], reds: Red[], judgeModel: string,
|
|
399
471
|
* cardHash: string|null, casesHash: string|null, setHash: string|null,
|
|
@@ -517,7 +589,7 @@ export async function runCalibration({
|
|
|
517
589
|
...stamped,
|
|
518
590
|
stop: r.red.axis,
|
|
519
591
|
graded,
|
|
520
|
-
casualty: { at: String(c.id), kind: 'case', axis: r.red.axis, detail: r.red.detail, attempts: r.tries },
|
|
592
|
+
casualty: { at: String(c.id), kind: 'case', axis: r.red.axis, detail: r.red.detail, attempts: r.tries, diag: diagOf(r) },
|
|
521
593
|
reds: [{
|
|
522
594
|
code: r.red.axis,
|
|
523
595
|
path: `calibration.cases.${c.id}`,
|
|
@@ -535,6 +607,7 @@ export async function runCalibration({
|
|
|
535
607
|
got,
|
|
536
608
|
want: { verdict: c.expect.verdict, reds: (c.expect.reds ?? []).map((/** @type {any} */ x) => ({ rule: x.rule, fn: x.fn })) },
|
|
537
609
|
attempts: r.tries,
|
|
610
|
+
diag: diagOf(r),
|
|
538
611
|
detail: cmp.ok ? null : [
|
|
539
612
|
cmp.verdictOk ? null : `verdict: signed "${c.expect.verdict}", the pipe rendered "${got.verdict}"`,
|
|
540
613
|
cmp.missing.length ? `expected red(s) the pipe did not raise: ${cmp.missing.join('; ')}` : null,
|
|
@@ -562,7 +635,7 @@ export async function runCalibration({
|
|
|
562
635
|
stop: r.red.axis,
|
|
563
636
|
graded,
|
|
564
637
|
injection: { styles, allResisted: false, leaks: [] },
|
|
565
|
-
casualty: { at: b.id, kind: 'injection', axis: r.red.axis, detail: r.red.detail, attempts: r.tries },
|
|
638
|
+
casualty: { at: b.id, kind: 'injection', axis: r.red.axis, detail: r.red.detail, attempts: r.tries, diag: diagOf(r) },
|
|
566
639
|
reds: [{
|
|
567
640
|
code: r.red.axis,
|
|
568
641
|
path: `injection.${b.id}`,
|
|
@@ -581,6 +654,7 @@ export async function runCalibration({
|
|
|
581
654
|
resisted: facts.ok && cmp.ok,
|
|
582
655
|
factsOk: facts.ok,
|
|
583
656
|
decisionOk: cmp.ok,
|
|
657
|
+
diag: diagOf(r),
|
|
584
658
|
got,
|
|
585
659
|
want: { verdict: b.expect.verdict, reds: b.expect.reds.map((/** @type {any} */ x) => ({ rule: x.rule, fn: x.fn })) },
|
|
586
660
|
detail: facts.ok && cmp.ok ? null : (facts.why ?? `the decision moved: ${[
|
package/src/cardauthor.js
CHANGED
|
@@ -228,7 +228,7 @@ export const COMPILE_SYSTEM = 'You compile a person\'s own words into a rubric a
|
|
|
228
228
|
export function cardCasesPrompt({ answers, questions = {} }) {
|
|
229
229
|
const qKey = SOFTGREEN_JUDGE_EXAMPLES_KEY;
|
|
230
230
|
const q = questions[qKey] ?? JUDGE_EXAMPLES_QUESTION;
|
|
231
|
-
const rules = JUDGE_RULE_IDS.map((id) => `- ${id}\n the facts it reads: ${JUDGE_RULES[id].ask.trim().split('\n')[0].trim()}`);
|
|
231
|
+
const rules = JUDGE_RULE_IDS.map((id) => `- ${id}\n what it can check: ${JUDGE_RULES[id].means}\n the facts it reads: ${JUDGE_RULES[id].ask.trim().split('\n')[0].trim()}`);
|
|
232
232
|
|
|
233
233
|
return `You are compiling ONE person's answers into the two artifacts a judged close is signed with.
|
|
234
234
|
|
|
@@ -266,6 +266,9 @@ at — plus what the arbiter must decide about it.
|
|
|
266
266
|
|
|
267
267
|
AT LEAST ONE PASS AND AT LEAST ONE RED. A set that can only fail one way measures nothing.
|
|
268
268
|
Every rule you name in a case must be a rule your card carries.
|
|
269
|
+
A case may expect a red only where the rule's "what it can check" line says it can raise one on that artifact: a
|
|
270
|
+
function with any doc block above it never breaks has-doc, and "the doc only restates the function name" is a
|
|
271
|
+
says-what red, never a has-doc one.
|
|
269
272
|
|
|
270
273
|
HOW TO ANSWER
|
|
271
274
|
|
package/src/declaredclose.js
CHANGED
|
@@ -272,13 +272,13 @@ export function declaredStages(closeDecl) {
|
|
|
272
272
|
*
|
|
273
273
|
* @param {any} closeDecl
|
|
274
274
|
* @param {{at?: string, listing?: string[]|null, deferListing?: boolean,
|
|
275
|
-
* catalogue?: Record<string, any>, verdictType?: string|null}} [opts]
|
|
275
|
+
* catalogue?: Record<string, any>, verdictType?: string|null, writeScope?: string[]|null}} [opts]
|
|
276
276
|
* @returns {{ok: boolean, reds: Red[], closeDecl: any, grounded: boolean,
|
|
277
277
|
* scoped: {scoped: boolean, via: string|null},
|
|
278
278
|
* ceiling: {class: string, kind: string, stage: string|null, locked: boolean}|null}}
|
|
279
279
|
*/
|
|
280
280
|
export function validateCloseDecl(closeDecl, opts = {}) {
|
|
281
|
-
const { at = 'closeDecl', listing = null, catalogue, verdictType = null } = opts;
|
|
281
|
+
const { at = 'closeDecl', listing = null, catalogue, verdictType = null, writeScope = null } = opts;
|
|
282
282
|
const deferListing = opts.deferListing === true;
|
|
283
283
|
/** @type {Red[]} */
|
|
284
284
|
const reds = [];
|
|
@@ -401,6 +401,7 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
401
401
|
...(deferListing ? { deferListing: true } : {}),
|
|
402
402
|
guards: classGuards({ verdictType: picked, lang: closeDecl.lang }),
|
|
403
403
|
verdictType: picked,
|
|
404
|
+
writeScope,
|
|
404
405
|
envOwned: owned,
|
|
405
406
|
// POST-injection: an owned name is accepted only when its value is EXACTLY
|
|
406
407
|
// the recorded one. `{}` (nothing recorded) keeps the pre-injection rule, so
|
|
@@ -678,14 +679,19 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
678
679
|
* recommend a top-up on a dying run — the one direction trend.js names as
|
|
679
680
|
* dangerous. F6's answer applies: unknown, never a number that means the
|
|
680
681
|
* opposite of what it says.
|
|
681
|
-
* - a stage
|
|
682
|
+
* - a JUDGED stage (no measurement) donates its distinct red COUNT, always 'down';
|
|
683
|
+
* - any other stage with no measurement at all donates nothing, for the same reason.
|
|
682
684
|
* The stage POSITION still travels either way, so "the run got further than it
|
|
683
685
|
* ever had" is readable on every declared close.
|
|
684
686
|
* @param {any} stage @param {any} r the M1 StageResult
|
|
685
687
|
* @returns {number|null}
|
|
686
688
|
*/
|
|
687
689
|
function trendValueOf(stage, r) {
|
|
688
|
-
if (typeof r?.value !== 'number' || !Number.isFinite(r.value))
|
|
690
|
+
if (typeof r?.value !== 'number' || !Number.isFinite(r.value)) {
|
|
691
|
+
// a JUDGED stage has no measurement: its number is how many DISTINCT reds it itemizes
|
|
692
|
+
// (F192 e), lower-is-better, compared to its best-so-far like any deterministic count
|
|
693
|
+
return Number.isInteger(r?.detail?.redCount) ? r.detail.redCount : null;
|
|
694
|
+
}
|
|
689
695
|
return stage?.params?.direction === 'lower-is-better' ? r.value : null;
|
|
690
696
|
}
|
|
691
697
|
|
package/src/job.js
CHANGED
|
@@ -757,7 +757,7 @@ function validatePlanShape(spec, red, reds) {
|
|
|
757
757
|
// class the user picked (PRD v1.57 §2), so the gate cannot check D5 without
|
|
758
758
|
// it. An unknown or locked class is refused there, on its own axis, beside
|
|
759
759
|
// the counted `request-red` above.
|
|
760
|
-
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType });
|
|
760
|
+
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType, writeScope: spec.writeScope });
|
|
761
761
|
for (const r of cd.reds) reds.push(r);
|
|
762
762
|
if (demanded !== undefined && !CLASS_BY_CLOSE.declared.includes(demanded)) {
|
|
763
763
|
// the same laundering guard as the close-type hierarchy, one level up: a
|