bareloop 0.10.0 → 0.12.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 +510 -0
- package/README.md +18 -7
- package/bareloop.context.md +456 -19
- package/package.json +3 -3
- package/src/authorflow.js +290 -49
- package/src/authoring.js +258 -46
- package/src/authorjob.js +390 -32
- package/src/authorscout.js +13 -1
- package/src/bridges.js +238 -11
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/clock.js +21 -3
- package/src/declaredclose.js +188 -13
- package/src/index.js +90 -7
- package/src/job.js +28 -9
- package/src/judged.js +765 -0
- package/src/kinds.js +578 -13
- package/src/ledger.js +24 -1
- package/src/planrun.js +517 -14
- package/src/ralph.js +9 -0
- package/src/reuse.js +514 -96
- package/src/reviewdoor.js +246 -0
- package/src/root.js +1 -1
- package/src/run.js +81 -8
- package/src/selection.js +23 -7
- package/types/authorflow.d.ts +233 -21
- package/types/authoring.d.ts +99 -39
- package/types/authorjob.d.ts +44 -6
- package/types/authorscout.d.ts +7 -1
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +93 -6
- package/types/index.d.ts +11 -7
- package/types/job.d.ts +22 -4
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +227 -8
- package/types/planrun.d.ts +73 -3
- package/types/reuse.d.ts +218 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +60 -2
package/src/authorjob.js
CHANGED
|
@@ -91,12 +91,17 @@
|
|
|
91
91
|
// in the stored declaration (forward-compat point 3), and the suite pins it.
|
|
92
92
|
|
|
93
93
|
import { jobSpecHash, validateJob } from './job.js';
|
|
94
|
-
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees } from './kinds.js';
|
|
94
|
+
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, SEED_EXEMPT_KINDS, JUDGED_FLOOR_KIND } from './kinds.js';
|
|
95
|
+
// SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
|
|
96
|
+
// and the gate that decides whether the ruler they describe is a ruler.
|
|
97
|
+
import { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts } from './cardauthor.js';
|
|
98
|
+
import { runCalibration } from './calibrate.js';
|
|
99
|
+
import { JUDGE_MODEL, CALIBRATION_SIZE } from './judged.js';
|
|
95
100
|
import {
|
|
96
101
|
GENRE_LANGUAGES, LOCKED_KINDS, TYPES_GENRE, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES,
|
|
97
102
|
} from './authoring.js';
|
|
98
|
-
import { QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose } from './authorflow.js';
|
|
99
|
-
import { runAuthorScout, buildSeedListing } from './authorscout.js';
|
|
103
|
+
import { QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook } from './authorflow.js';
|
|
104
|
+
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
100
105
|
import {
|
|
101
106
|
DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
|
|
102
107
|
} from './declaredclose.js';
|
|
@@ -366,23 +371,38 @@ function composerRefusal(reds) {
|
|
|
366
371
|
* verdictType?: string|null,
|
|
367
372
|
* questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
|
|
368
373
|
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
|
|
374
|
+
* onPhase?: (phase: string, data?: any) => void,
|
|
375
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
369
376
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
370
|
-
* authorFn?: Function, authorOpts?: object
|
|
377
|
+
* authorFn?: Function, authorOpts?: object,
|
|
378
|
+
* signerFix?: Function|any, proposeFn?: Function, compileOpts?: object}} o
|
|
371
379
|
* @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
|
|
372
|
-
* closeDecl: any, seedRef: string|null, authoring: any, reds: Red[],
|
|
373
|
-
* cost: any, interview: any}>}
|
|
380
|
+
* closeDecl: any, seedRef: string|null, authoring: any, judged: any, reds: Red[],
|
|
381
|
+
* stop: string|null, cost: any, interview: any}>}
|
|
374
382
|
*/
|
|
375
383
|
export async function authorCloseForJob({
|
|
376
384
|
answers, repoPath = null, lang, verdictType = null, questions = null,
|
|
377
385
|
generate, provider = null, seedRef = null, scout = null, listing = null,
|
|
378
386
|
ceilingUsd = null,
|
|
387
|
+
// THE PROGRESS SEAMS. This composition is the pipeline's long silence — a real
|
|
388
|
+
// survey, a real declaration ladder, and a real toolchain per close stage — and
|
|
389
|
+
// it emitted nothing until it returned, so an operator watching a live run
|
|
390
|
+
// could not tell work from a hang. The library REPORTS and the shell PRINTS
|
|
391
|
+
// (`runJob`'s `emit` → `runPlan`, the same shape): nothing here reads either
|
|
392
|
+
// callback back, and no decision in this file or below it depends on one.
|
|
393
|
+
onPhase = () => {}, onCall = () => {},
|
|
379
394
|
seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
|
|
380
395
|
authorFn = authorClose, authorOpts = {},
|
|
396
|
+
// SOFTGREEN modules 4+5. `signerFix` is the D5 seam: a function (or a value)
|
|
397
|
+
// carrying the SIGNER's corrected artifacts back. Absent means the proposal is
|
|
398
|
+
// signed as proposed, and `signJudgedArtifacts` records which of the two
|
|
399
|
+
// happened rather than leaving a reader to guess.
|
|
400
|
+
signerFix = null, proposeFn = proposeJudgedArtifacts, compileOpts = {},
|
|
381
401
|
}) {
|
|
382
402
|
/** @type {any} */
|
|
383
403
|
const base = {
|
|
384
404
|
ok: false, refusal: null, verdictType: null, closeDecl: null, seedRef: null,
|
|
385
|
-
authoring: null, reds: [], stop: null, cost: null, interview: null,
|
|
405
|
+
authoring: null, judged: null, reds: [], stop: null, cost: null, interview: null,
|
|
386
406
|
};
|
|
387
407
|
|
|
388
408
|
const interview = runInterview({ answers, verdictType, repoPath, questions });
|
|
@@ -425,6 +445,10 @@ export async function authorCloseForJob({
|
|
|
425
445
|
// D8 — the seed, READ rather than typed.
|
|
426
446
|
let seed = seedRef;
|
|
427
447
|
if (!isNonEmptyString(seed)) {
|
|
448
|
+
// Announced only on the path that actually READS one: a caller who handed a
|
|
449
|
+
// seedRef in never paid for a git call, and a phase line for work nobody did
|
|
450
|
+
// is the blind-instrument class, one line wide.
|
|
451
|
+
onPhase('seed', {});
|
|
428
452
|
const s = await seedFn(workdir);
|
|
429
453
|
if (s.stop !== null) {
|
|
430
454
|
// same git-stderr channel as `prepareSigning`'s, scrubbed at the same
|
|
@@ -445,15 +469,46 @@ export async function authorCloseForJob({
|
|
|
445
469
|
// form). `null` travels EXPLICITLY rather than being left off — unbounded is a
|
|
446
470
|
// stated operator choice, and an absent field read as falsy is the same state
|
|
447
471
|
// arrived at by accident.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
472
|
+
/** @type {any} */
|
|
473
|
+
let survey = scout;
|
|
474
|
+
// `== null` and not `!survey`: this is the `??` these two lines replaced, kept
|
|
475
|
+
// exactly — a caller-supplied survey is used whatever it is, and only an
|
|
476
|
+
// ABSENT one is paid for.
|
|
477
|
+
if (survey == null) {
|
|
478
|
+
// The scout is the pipeline's FIRST paid stage and its longest single
|
|
479
|
+
// silence: up to three ladder attempts, each a real toolable survey over a
|
|
480
|
+
// real tree. `attempts` is the ceiling the scout enforces on itself, so the
|
|
481
|
+
// reader knows how many the line could repeat for.
|
|
482
|
+
onPhase('scout', { attempts: SCOUT_ATTEMPTS });
|
|
483
|
+
survey = await scoutFn({ workdir, provider, ceilingUsd, onCall });
|
|
484
|
+
// …and the survey's own VERDICT, said the moment it lands. `state` is what
|
|
485
|
+
// every downstream refusal keys on (F59: absent is never "no facts needed"),
|
|
486
|
+
// and the fact count is the one number that says whether the ladder came
|
|
487
|
+
// back with anything to author from.
|
|
488
|
+
onPhase('scout-done', { state: survey?.state ?? null, facts: Object.keys(survey?.facts ?? {}).length });
|
|
489
|
+
}
|
|
490
|
+
/** @type {any} */
|
|
491
|
+
let seeds = listing;
|
|
492
|
+
if (seeds == null) {
|
|
493
|
+
onPhase('listing', {});
|
|
494
|
+
seeds = await listingFn({
|
|
495
|
+
workdir, seedRef: seed, sourcePaths: survey?.facts?.sourcePaths, testPaths: survey?.facts?.testPaths,
|
|
496
|
+
});
|
|
497
|
+
// the count is only knowable AFTER, so it rides the done line rather than
|
|
498
|
+
// being guessed onto the start one
|
|
499
|
+
onPhase('listing-done', { files: seeds?.files?.length ?? null, stop: seeds?.stop ?? null });
|
|
500
|
+
}
|
|
452
501
|
|
|
502
|
+
// The declaration ladder. The REVISION CAP is deliberately not quoted here:
|
|
503
|
+
// the tighten-only clamp lives in `authorClose` (and `authorOpts` may lower
|
|
504
|
+
// it), so a number spelled at this call site could disagree with the one the
|
|
505
|
+
// loop obeys — `author-call` carries the enforced cap instead, from where it
|
|
506
|
+
// is computed.
|
|
507
|
+
onPhase('author', {});
|
|
453
508
|
const authored = await authorFn({
|
|
454
509
|
workdir, seedRef: seed, lang, verdictType: picked,
|
|
455
510
|
answers: interview.answers, questions: questions ?? questionsFor(picked),
|
|
456
|
-
scout: survey, listing: seeds, generate, ceilingUsd, ...authorOpts,
|
|
511
|
+
scout: survey, listing: seeds, generate, ceilingUsd, onPhase, onCall, ...authorOpts,
|
|
457
512
|
});
|
|
458
513
|
|
|
459
514
|
if (!authored.ok) {
|
|
@@ -484,29 +539,111 @@ export async function authorCloseForJob({
|
|
|
484
539
|
// declaration above it), and stated here rather than re-derived. The remaining
|
|
485
540
|
// precondition (D9.3 — the close has work to do) is measured by
|
|
486
541
|
// `prepareSigning`, which refuses there.
|
|
542
|
+
/** @type {any} */
|
|
543
|
+
let closeDecl = {
|
|
544
|
+
genre: GENRE,
|
|
545
|
+
lang,
|
|
546
|
+
stages: authored.declaration.stages,
|
|
547
|
+
...(Object.keys(applied).length ? { genreEnv: { ...applied } } : {}),
|
|
548
|
+
// `notes` is the one FREE-TEXT field the model writes straight into the
|
|
549
|
+
// signed spec, and this is that field's persist boundary. Scrubbed here
|
|
550
|
+
// for the same reason `scrubRed` scrubs uniformly a few lines down: every
|
|
551
|
+
// other model-authored string that survives this call rides the ONE
|
|
552
|
+
// redactor, and a signed spec outlives the run that produced it.
|
|
553
|
+
// `redactSecrets` returns a non-matching string byte-identical, so a note
|
|
554
|
+
// carrying nothing hashes exactly as it did — the mask costs the honest
|
|
555
|
+
// case nothing, which is the fail-safe direction.
|
|
556
|
+
...(authored.declaration.notes?.length
|
|
557
|
+
? { notes: authored.declaration.notes.map((/** @type {any} */ n) => (typeof n === 'string' ? redactSecrets(n) : n)) }
|
|
558
|
+
: {}),
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// ── THE TWO SIGNED ARTIFACTS (softgreen module 4, wired here by module 5) ──
|
|
562
|
+
//
|
|
563
|
+
// A close that judges needs a rubric card and a calibration set, and until now
|
|
564
|
+
// nothing in the pipeline called the compile — module 4 shipped the machinery
|
|
565
|
+
// and said so. This is the call: Q6 and Q7 go to the compiler VERBATIM, the
|
|
566
|
+
// proposal is SHOWN (`onPhase`, and the driver prints it), the SIGNER's fix is
|
|
567
|
+
// what is stored, and both land inside the spec where the hash covers them.
|
|
568
|
+
//
|
|
569
|
+
// KEYED OFF THE DECLARATION, never off the picked class: a soft-green close
|
|
570
|
+
// that composed no judged stage needs no card, and a judged stage is a judged
|
|
571
|
+
// stage whatever the radio said. One judged stage is the composition law
|
|
572
|
+
// (AT_MOST_ONCE_KINDS), so the fold's own throw is unreachable here — it stays
|
|
573
|
+
// as the caller-error guard it is.
|
|
574
|
+
/** @type {any} */
|
|
575
|
+
let judged = null;
|
|
576
|
+
if ((closeDecl.stages ?? []).some((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND)) {
|
|
577
|
+
// ONE CEILING, ALL THREE PAID SEAMS. The declaration loop's own calls are
|
|
578
|
+
// ABSORBED before the compile is asked for, so the ceiling folds in prior
|
|
579
|
+
// spend and re-invoking a seam cannot silently widen it. `absorb` deliberately
|
|
580
|
+
// does not re-fire `onCall`, so nothing is reported twice.
|
|
581
|
+
const book = makeCostBook({ ceilingUsd, onCall });
|
|
582
|
+
book.absorb(authored.cost?.calls ?? []);
|
|
583
|
+
onPhase('rubric', { size: CALIBRATION_SIZE });
|
|
584
|
+
const proposed = await proposeFn({
|
|
585
|
+
answers: interview.answers,
|
|
586
|
+
questions: questions ?? questionsFor(picked),
|
|
587
|
+
generate,
|
|
588
|
+
book,
|
|
589
|
+
...compileOpts,
|
|
590
|
+
});
|
|
591
|
+
onPhase('rubric-done', { ok: proposed.ok, stop: proposed.stop, attempts: proposed.attempts });
|
|
592
|
+
if (!proposed.ok) {
|
|
593
|
+
return {
|
|
594
|
+
...base,
|
|
595
|
+
verdictType: picked, seedRef: seed, authoring: authored, judged: { proposal: proposed, signed: null },
|
|
596
|
+
reds: proposed.reds, stop: proposed.stop, cost: book.report(), interview,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// THE D5 MOMENT. `signerFix` is the seam a UI (or an operator's own editor)
|
|
601
|
+
// hands the fixed artifacts back through; ABSENT is a legal answer and it is
|
|
602
|
+
// recorded as one — `source: 'proposal'` says *nobody changed this*, which is
|
|
603
|
+
// a different fact from *a person read it and changed nothing*.
|
|
604
|
+
const fix = typeof signerFix === 'function' ? await signerFix(proposed.proposal) : signerFix;
|
|
605
|
+
const signed = signJudgedArtifacts({ proposal: proposed.proposal, fix });
|
|
606
|
+
judged = { proposal: proposed, signed, scrubbed: signed.scrubbed, source: signed.source };
|
|
607
|
+
if (!signed.ok) {
|
|
608
|
+
return {
|
|
609
|
+
...base,
|
|
610
|
+
verdictType: picked, seedRef: seed, authoring: authored, judged,
|
|
611
|
+
reds: signed.reds, stop: 'rubric-invalid', cost: book.report(), interview,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// THE SCRUB, ANNOUNCED. A signer signs a hash over the STORED bytes, so bytes
|
|
615
|
+
// the ONE inventory masked on the way in are said out loud rather than left
|
|
616
|
+
// for them to notice — by PATH only, because echoing the matched string would
|
|
617
|
+
// be the same leak one hop on.
|
|
618
|
+
if (signed.scrubbed.length) onPhase('rubric-scrubbed', { paths: signed.scrubbed.map((s) => s.path) });
|
|
619
|
+
// `cases` is an array on every ok path — `validateCalibrationSet` reds a
|
|
620
|
+
// non-array before anything can be `ok` — and the narrowing is stated rather
|
|
621
|
+
// than defaulted: `?? []` here would fold an EMPTY signed set into a close,
|
|
622
|
+
// which is the one direction that must never be reachable.
|
|
623
|
+
closeDecl = foldJudgedArtifacts(closeDecl, { card: signed.card, cases: /** @type {any[]} */ (signed.cases) });
|
|
624
|
+
return {
|
|
625
|
+
ok: true,
|
|
626
|
+
refusal: null,
|
|
627
|
+
verdictType: picked,
|
|
628
|
+
closeDecl,
|
|
629
|
+
seedRef: seed,
|
|
630
|
+
authoring: authored,
|
|
631
|
+
judged,
|
|
632
|
+
reds: authored.reds,
|
|
633
|
+
stop: authored.stop,
|
|
634
|
+
cost: book.report(),
|
|
635
|
+
interview,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
487
639
|
return {
|
|
488
640
|
ok: true,
|
|
489
641
|
refusal: null,
|
|
490
642
|
verdictType: picked,
|
|
491
|
-
closeDecl
|
|
492
|
-
genre: GENRE,
|
|
493
|
-
lang,
|
|
494
|
-
stages: authored.declaration.stages,
|
|
495
|
-
...(Object.keys(applied).length ? { genreEnv: { ...applied } } : {}),
|
|
496
|
-
// `notes` is the one FREE-TEXT field the model writes straight into the
|
|
497
|
-
// signed spec, and this is that field's persist boundary. Scrubbed here
|
|
498
|
-
// for the same reason `scrubRed` scrubs uniformly a few lines down: every
|
|
499
|
-
// other model-authored string that survives this call rides the ONE
|
|
500
|
-
// redactor, and a signed spec outlives the run that produced it.
|
|
501
|
-
// `redactSecrets` returns a non-matching string byte-identical, so a note
|
|
502
|
-
// carrying nothing hashes exactly as it did — the mask costs the honest
|
|
503
|
-
// case nothing, which is the fail-safe direction.
|
|
504
|
-
...(authored.declaration.notes?.length
|
|
505
|
-
? { notes: authored.declaration.notes.map((/** @type {any} */ n) => (typeof n === 'string' ? redactSecrets(n) : n)) }
|
|
506
|
-
: {}),
|
|
507
|
-
},
|
|
643
|
+
closeDecl,
|
|
508
644
|
seedRef: seed,
|
|
509
645
|
authoring: authored,
|
|
646
|
+
judged,
|
|
510
647
|
reds: authored.reds,
|
|
511
648
|
stop: authored.stop,
|
|
512
649
|
cost: authored.cost,
|
|
@@ -587,6 +724,166 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
587
724
|
Object.entries(r).map(([k, v]) => [k, typeof v === 'string' ? redactSecrets(v) : v]),
|
|
588
725
|
));
|
|
589
726
|
|
|
727
|
+
/**
|
|
728
|
+
* GATE 4 — the calibration gate, and the only gate here that spends money.
|
|
729
|
+
*
|
|
730
|
+
* It answers ONE question: has this close's own ruler been shown to grade the
|
|
731
|
+
* signer's own ten correctly, itemized, and to resist all five attack styles?
|
|
732
|
+
* Anything else it learns — a missing set, an unwired judge, a dead call — is
|
|
733
|
+
* reported under its own name and refuses, because every one of them leaves the
|
|
734
|
+
* question unanswered and an unanswered calibration is not a passed one.
|
|
735
|
+
*
|
|
736
|
+
* THE CASUALTY SPLIT is the same one the judged stage runs (F45, contract (a)): a
|
|
737
|
+
* provider failure, an unpriced call or an unparseable emission after the ladder
|
|
738
|
+
* says NOTHING about the set. It refuses the signature — an ungraded ruler is
|
|
739
|
+
* still ungraded — but it refuses under the TRANSPORT's name, so the operator is
|
|
740
|
+
* sent to the socket or the meter and never to their own rubric.
|
|
741
|
+
* AND IT MEETS THE ONE CEILING. This is the run's THIRD paid seam — the largest of
|
|
742
|
+
* them, at up to `CALIBRATION_SIZE` locate calls plus a five-artifact battery, each
|
|
743
|
+
* under its own retry ladder — and it used to run with no bound at all while the
|
|
744
|
+
* scout and the declaration loop were both stopped by the operator's own number.
|
|
745
|
+
* That is the advertised budget and the enforced budget being two different numbers.
|
|
746
|
+
* The book is built HERE and PRIOR SPEND IS ABSORBED into it, so the ceiling folds in
|
|
747
|
+
* what the earlier seams already spent and re-invoking a seam cannot silently widen
|
|
748
|
+
* it; the gate reads it between calls and stops as a refusal that names money.
|
|
749
|
+
* @param {{spec: any, judgedStages: any[], judgeLoop: Function|null,
|
|
750
|
+
* onJudgeCost: ((c: any) => void)|null, calibrateFn: Function,
|
|
751
|
+
* ceilingUsd: number|null, priorCalls: any[]}} o
|
|
752
|
+
* @returns {Promise<{ok: boolean, record: any, reds: Red[], refusal: Refusal|null}>}
|
|
753
|
+
*/
|
|
754
|
+
async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, calibrateFn, ceilingUsd, priorCalls }) {
|
|
755
|
+
const cases = spec.closeDecl?.calibration?.cases ?? null;
|
|
756
|
+
// ONE card, and it is the SIGNED one — the card the calibration is graded
|
|
757
|
+
// against must be the card the close will RUN, or the gate certifies a ruler
|
|
758
|
+
// nobody is going to use. `validateCloseDecl` has already refused a second
|
|
759
|
+
// judged stage (AT_MOST_ONCE_KINDS), so there is exactly one here.
|
|
760
|
+
const card = judgedStages[0]?.params?.card ?? null;
|
|
761
|
+
|
|
762
|
+
if (cases === null || cases === undefined) {
|
|
763
|
+
const detail = `This close judges with a rubric card, and no calibration set is stored with it. The judged floor `
|
|
764
|
+
+ `is only a floor once the whole pipe has graded ${CALIBRATION_SIZE} signed cases correctly — until then the `
|
|
765
|
+
+ 'ruler is unmeasured, and an unmeasured ruler cannot be signed.';
|
|
766
|
+
return {
|
|
767
|
+
ok: false,
|
|
768
|
+
record: { ok: false, stop: 'calibration-missing', judgeModel: JUDGE_MODEL, required: CALIBRATION_SIZE },
|
|
769
|
+
reds: [{ code: 'calibration-missing', path: 'closeDecl.calibration.cases', detail }],
|
|
770
|
+
refusal: refuse({
|
|
771
|
+
kind: 'decision-ready',
|
|
772
|
+
verb: null,
|
|
773
|
+
path: 'closeDecl.calibration',
|
|
774
|
+
detail,
|
|
775
|
+
options: [
|
|
776
|
+
`author the calibration set from the interview's own Q7 answer (${CALIBRATION_SIZE} cases, mixed polarity) and re-sign`,
|
|
777
|
+
'drop the judged stage and describe a job whose done is machine-checkable',
|
|
778
|
+
'abandon the job',
|
|
779
|
+
],
|
|
780
|
+
}),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ONE CEILING, ALL THE PAID SEAMS (`authorCloseForJob` says the same words over
|
|
785
|
+
// the compile). The book absorbs what the run has already spent, so what the gate
|
|
786
|
+
// reads is the REMAINING balance and never a fresh allowance; `absorb` deliberately
|
|
787
|
+
// does not re-fire a reporter, so nothing is counted twice downstream.
|
|
788
|
+
const book = makeCostBook({ ceilingUsd });
|
|
789
|
+
book.absorb(Array.isArray(priorCalls) ? priorCalls : []);
|
|
790
|
+
const cal = await calibrateFn({
|
|
791
|
+
cases,
|
|
792
|
+
card,
|
|
793
|
+
judgeLoop,
|
|
794
|
+
// the gate's own calls join the same tally the ceiling is read against — a
|
|
795
|
+
// ceiling that cannot see the spend it bounds is F45's blind detector in a
|
|
796
|
+
// money coat. The caller's reporter fires with the call's whole record, after.
|
|
797
|
+
onCost: (/** @type {any} */ c) => {
|
|
798
|
+
book.absorb([{ label: c?.label ?? 'calibrate', costUsd: c?.costUsd ?? null, unpricedRounds: c?.unpricedRounds ?? 0 }]);
|
|
799
|
+
if (typeof onJudgeCost === 'function') onJudgeCost(c);
|
|
800
|
+
},
|
|
801
|
+
capStop: () => book.capStop(),
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
/** what the signing evidence KEEPS about this gate. The per-case artifacts are
|
|
805
|
+
* deliberately not in it — they are already in the signed spec, and a second
|
|
806
|
+
* copy in the evidence file is a second thing to keep in step. What IS kept is
|
|
807
|
+
* WHICH BYTES were certified (`cardHash`/`casesHash`/`setHash`) and BY WHICH
|
|
808
|
+
* MODEL (`judgeModel`), which is what makes a judge-model bump detectable at
|
|
809
|
+
* all. */
|
|
810
|
+
const record = {
|
|
811
|
+
ok: cal.ok,
|
|
812
|
+
stop: cal.stop,
|
|
813
|
+
judgeModel: cal.judgeModel,
|
|
814
|
+
cardHash: cal.cardHash,
|
|
815
|
+
casesHash: cal.casesHash,
|
|
816
|
+
setHash: cal.setHash,
|
|
817
|
+
graded: cal.graded,
|
|
818
|
+
failures: cal.failures,
|
|
819
|
+
injection: cal.injection,
|
|
820
|
+
casualty: cal.casualty,
|
|
821
|
+
costUsd: cal.costUsd,
|
|
822
|
+
knownUsd: cal.knownUsd,
|
|
823
|
+
spendComplete: cal.spendComplete,
|
|
824
|
+
calls: cal.calls?.length ?? 0,
|
|
825
|
+
};
|
|
826
|
+
if (cal.ok) return { ok: true, record, reds: [], refusal: null };
|
|
827
|
+
|
|
828
|
+
const reds = cal.reds.map(scrubRed);
|
|
829
|
+
const casualty = cal.casualty !== null && cal.casualty !== undefined;
|
|
830
|
+
/** the OPERATOR's own governance stop, and it is neither a casualty nor a reading
|
|
831
|
+
* of the close: the ceiling ran out mid-question, and what was graded before it
|
|
832
|
+
* did says nothing either way about the rest of the set. */
|
|
833
|
+
const budget = cal.stop === 'cap-halt' || cal.stop === 'pricing-red';
|
|
834
|
+
const detail = budget
|
|
835
|
+
? (cal.stop === 'cap-halt'
|
|
836
|
+
? `The calibration gate ran out of CEILING before it could finish: ${cal.graded.length} of `
|
|
837
|
+
+ `${Array.isArray(cases) ? cases.length : 0} case(s) were graded and the next call was never made. A `
|
|
838
|
+
+ 'part-graded set is an ungraded ruler, so nothing is signable — and nothing here says the ruler is wrong.'
|
|
839
|
+
: 'The calibration gate stopped on an UNPRICED call: with the run\'s total unknown the ceiling cannot be read '
|
|
840
|
+
+ 'against it, and unpriced is never free (F6). Nothing here says anything about the set.')
|
|
841
|
+
: casualty
|
|
842
|
+
? `The calibration gate could not be RUN: the judge produced no usable facts for ${cal.casualty.kind} `
|
|
843
|
+
+ `"${cal.casualty.at}" [${cal.casualty.axis}]. A broken judge is a casualty and never a verdict, so this says `
|
|
844
|
+
+ 'nothing about the set — but an ungraded ruler is still an ungraded ruler, and it cannot be signed.'
|
|
845
|
+
: cal.stop === 'no-judge'
|
|
846
|
+
? `This close carries a judged stage and no judge seam was wired, so the ${CALIBRATION_SIZE}-case gate never ran. `
|
|
847
|
+
+ 'An absent seam is a wiring gap, never a fall-back: a gate that grades nothing would certify every close.'
|
|
848
|
+
: cal.stop === 'invalid-set'
|
|
849
|
+
? 'The stored rubric card and calibration set do not compose into something gradeable, so the gate refused '
|
|
850
|
+
+ 'before spending anything.'
|
|
851
|
+
: `The whole pipe did not grade this close's own calibration set correctly: `
|
|
852
|
+
+ `${cal.failures.length} of ${Array.isArray(cases) ? cases.length : 0} case(s) graded wrong`
|
|
853
|
+
+ `${cal.injection.leaks.length ? ` and ${cal.injection.leaks.length} injection style(s) leaked` : ''}. `
|
|
854
|
+
+ `The floor is ${CALIBRATION_SIZE}/${CALIBRATION_SIZE} with itemized reds and all styles resisting `
|
|
855
|
+
+ '(hamr, 2026-08-18) — the fix for a miss is a new card line or a corrected case, and a re-sign.';
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
record,
|
|
860
|
+
reds,
|
|
861
|
+
refusal: refuse({
|
|
862
|
+
kind: 'decision-ready',
|
|
863
|
+
verb: null,
|
|
864
|
+
path: 'closeDecl.calibration',
|
|
865
|
+
detail,
|
|
866
|
+
options: budget
|
|
867
|
+
? [
|
|
868
|
+
'raise the authoring ceiling and re-run the gate — the $0 gates above are already passed',
|
|
869
|
+
'run the gate unbounded (omit the ceiling) — that is a stated operator choice, never a default',
|
|
870
|
+
'abandon the job',
|
|
871
|
+
]
|
|
872
|
+
: casualty || cal.stop === 'no-judge'
|
|
873
|
+
? [
|
|
874
|
+
'wire a judge provider pinned to the judged tier and re-run the gate',
|
|
875
|
+
'wait out the provider and re-run — nothing about the close was judged here',
|
|
876
|
+
'abandon the job',
|
|
877
|
+
]
|
|
878
|
+
: [
|
|
879
|
+
'correct the card line or the case the pipe graded differently, and re-sign',
|
|
880
|
+
'read the itemized rows above: a case the pipe reds for the WRONG reason is the card talking, not the judge',
|
|
881
|
+
'abandon the job',
|
|
882
|
+
],
|
|
883
|
+
}),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
590
887
|
/**
|
|
591
888
|
* D9, in code: *nothing JUDGES the close; three mechanical gates plus a
|
|
592
889
|
* signature.* No LLM validates another LLM's close, here or anywhere.
|
|
@@ -600,6 +897,24 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
600
897
|
* A close with NO work stage red at seed has nothing to do and is refused
|
|
601
898
|
* decision-ready — gate-2 round 2's arm B died exactly there, on a mypy that
|
|
602
899
|
* scanned nothing and therefore read clean.
|
|
900
|
+
* 4. THE CALIBRATION GATE (softgreen module 5) — for a close carrying a
|
|
901
|
+
* `judged-floor` stage ONLY. The whole judged pipe runs over the signed ten
|
|
902
|
+
* and the five arbiter-owned injection artifacts, and anything short of
|
|
903
|
+
* 10/10 with itemized reds and 5/5 resisted is an unsignable close. It is
|
|
904
|
+
* the ONE gate here that spends money, so it runs LAST — every free refusal
|
|
905
|
+
* has already had its chance, and nobody pays to discover a broken
|
|
906
|
+
* declaration.
|
|
907
|
+
*
|
|
908
|
+
* D9.3 AND THE JUDGED-ONLY CLOSE (2026-08-18 second addendum, ruling 3; hamr:
|
|
909
|
+
* *"fix it now, we are delivering softgreen, isn't that the whole point?"*). Gate
|
|
910
|
+
* 3 protects against a close that cannot fail — one that scans nothing and reads
|
|
911
|
+
* green at seed against a true red. A judged stage SKIPS the seed read by ruling
|
|
912
|
+
* 8, so a close whose only work stage is judged has no seed red to show and used
|
|
913
|
+
* to be unsignable: the one job shape softgreen exists for. For that shape, and
|
|
914
|
+
* ONLY that shape, a PASSED calibration gate plays the seed-red role — it is that
|
|
915
|
+
* close's own proof of failability, held to the same all-or-nothing bar. A close
|
|
916
|
+
* with a mechanical work stage is UNCHANGED: it still needs a seed red, whatever
|
|
917
|
+
* its calibration says.
|
|
603
918
|
*
|
|
604
919
|
* Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
|
|
605
920
|
* job creation the tree IS the seed, so "does every stage run against the real
|
|
@@ -612,18 +927,32 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
612
927
|
* comes back is the resolved spec, its hash, and the evidence to sign against.
|
|
613
928
|
*
|
|
614
929
|
* @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
|
|
615
|
-
* timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function
|
|
930
|
+
* timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function,
|
|
931
|
+
* judgeLoop?: Function|null, onJudgeCost?: ((c: any) => void)|null, calibrateFn?: Function,
|
|
932
|
+
* ceilingUsd?: number|null, priorCalls?: any[]}} o
|
|
616
933
|
* @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
|
|
617
934
|
* gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
|
|
618
935
|
*/
|
|
619
936
|
export async function prepareSigning({
|
|
620
937
|
spec, workdir, seedRef = null, shellCapUsd, timeoutMs,
|
|
621
938
|
seedFn = seedAtHead, listingFn = seedListing, seedReadFn = runSeedRead,
|
|
939
|
+
// THE PAID SEAM, and it is the operator's to wire — this module owns no
|
|
940
|
+
// provider and picks none (the same contract `authorCloseForJob` keeps for the
|
|
941
|
+
// drafting model). Absent is never a fall-back: a judged close whose gate could
|
|
942
|
+
// not run is refused, not waved through.
|
|
943
|
+
judgeLoop = null, onJudgeCost = null, calibrateFn = runCalibration,
|
|
944
|
+
// THE OPERATOR'S CEILING, and the spend it has already met. `null`/absent is
|
|
945
|
+
// UNBOUNDED and is a stated choice, never a default invented here (the same
|
|
946
|
+
// contract `authorCloseForJob` keeps). `priorCalls` is what the earlier paid
|
|
947
|
+
// seams of this run already cost — a `makeCostBook().report().calls` list — and
|
|
948
|
+
// it is ABSORBED so the gate reads the remaining balance rather than opening a
|
|
949
|
+
// fresh allowance under the same number.
|
|
950
|
+
ceilingUsd = null, priorCalls = [],
|
|
622
951
|
}) {
|
|
623
952
|
/** @type {any} */
|
|
624
953
|
const base = {
|
|
625
954
|
ok: false, specHash: null, seedRef: null, work: [], guards: [], stops: [], reds: [], refusal: null,
|
|
626
|
-
gates: { declaration: null, precheck: null, seedVerdict: null },
|
|
955
|
+
gates: { declaration: null, precheck: null, seedVerdict: null, calibration: null },
|
|
627
956
|
};
|
|
628
957
|
|
|
629
958
|
// ── gate 1a: the spec itself. A declared close that is not on a plan-shape
|
|
@@ -759,12 +1088,41 @@ export async function prepareSigning({
|
|
|
759
1088
|
workRed: workRed.map((r) => r.stage),
|
|
760
1089
|
};
|
|
761
1090
|
|
|
1091
|
+
// ── gate 4: THE CALIBRATION GATE (softgreen module 5). The ONLY paid gate, and
|
|
1092
|
+
// it runs LAST for exactly that reason: every $0 refusal above has already had
|
|
1093
|
+
// its chance, so a broken declaration or a stage that cannot run is found for
|
|
1094
|
+
// free. MANDATORY for any close carrying a judged-floor stage — module 4's
|
|
1095
|
+
// validator checks a stored set for LEGALITY only and deliberately allows a
|
|
1096
|
+
// judged stage with no set at all, because making it mandatory is a signing
|
|
1097
|
+
// decision and this is the signing gate.
|
|
1098
|
+
const judgedStages = (spec.closeDecl.stages ?? []).filter((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND);
|
|
1099
|
+
if (judgedStages.length) {
|
|
1100
|
+
const cal = await calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, calibrateFn, ceilingUsd, priorCalls });
|
|
1101
|
+
base.gates.calibration = cal.record;
|
|
1102
|
+
if (!cal.ok) return { ...base, work, guards, reds: cal.reds, refusal: cal.refusal };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
762
1105
|
// D9.3 — nothing red at seed means nothing to do. Round 2's arm B read GREEN
|
|
763
1106
|
// with value 0 because mypy died on an unrelated broken fixture and scanned
|
|
764
1107
|
// nothing: an instrument scanning nothing reads clean, which is F6's shape at
|
|
765
1108
|
// the close-authoring layer, and this gate is what refuses it BEFORE a
|
|
766
1109
|
// signature rather than after a paid run.
|
|
767
|
-
|
|
1110
|
+
//
|
|
1111
|
+
// THE JUDGED-ONLY ROUTE (ruling 3). A close whose work stages are ALL
|
|
1112
|
+
// seed-exempt has no seed verdict to be red — not because it measures nothing,
|
|
1113
|
+
// but because ruling 8 exempts a judged stage from being measured at seed. Its
|
|
1114
|
+
// proof of failability is the calibration gate, which has just run and passed
|
|
1115
|
+
// (a failed one returned above), and which contains signed cases the pipe must
|
|
1116
|
+
// RED as well as ones it must pass — the polarity law makes "this close can
|
|
1117
|
+
// fail" a thing that was demonstrated rather than assumed. A close with any
|
|
1118
|
+
// mechanical work stage takes the original path unchanged.
|
|
1119
|
+
const mechanicalWork = work.filter((r) => !SEED_EXEMPT_KINDS.includes(r.kind));
|
|
1120
|
+
const judgedOnly = judgedStages.length > 0 && mechanicalWork.length === 0;
|
|
1121
|
+
if (judgedOnly && base.gates.calibration?.ok) {
|
|
1122
|
+
// said out loud in the record: this close passed gate 3 on a DIFFERENT proof,
|
|
1123
|
+
// and a reader must never have to infer which one from an empty workRed list
|
|
1124
|
+
base.gates.seedVerdict.satisfiedBy = 'calibration';
|
|
1125
|
+
} else if (!workRed.length) {
|
|
768
1126
|
return {
|
|
769
1127
|
...base,
|
|
770
1128
|
work,
|
package/src/authorscout.js
CHANGED
|
@@ -349,6 +349,7 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
349
349
|
* @param {{workdir: string, provider?: any,
|
|
350
350
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
351
351
|
* attempts?: number, ceilingUsd?: number|null,
|
|
352
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
352
353
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
353
354
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
354
355
|
* @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
|
|
@@ -364,6 +365,7 @@ export async function runAuthorScout({
|
|
|
364
365
|
ctx = true,
|
|
365
366
|
attempts = SCOUT_ATTEMPTS,
|
|
366
367
|
ceilingUsd = null,
|
|
368
|
+
onCall = () => {},
|
|
367
369
|
createLoop = defaultLoop,
|
|
368
370
|
createSurveyor = defaultSurveyor,
|
|
369
371
|
}) {
|
|
@@ -384,7 +386,17 @@ export async function runAuthorScout({
|
|
|
384
386
|
* @param {string} label @param {number} attempt @param {any} r @returns {number}
|
|
385
387
|
*/
|
|
386
388
|
const record = (label, attempt, r) => {
|
|
387
|
-
|
|
389
|
+
const call = { label, ...priceOf(r) };
|
|
390
|
+
calls.push(call);
|
|
391
|
+
// …and the SHELL is told, at the same instant and off the same entry. A run
|
|
392
|
+
// killed from outside used to lose 100% of its spend record (F6/F12: a
|
|
393
|
+
// halted attempt's spend was invisible by 300×) — the totals only ever
|
|
394
|
+
// existed in this array, which dies with the process. Fired from inside the
|
|
395
|
+
// one recorder rather than at the call sites, so a paid call cannot be
|
|
396
|
+
// metered without the report going out: exactly the pairing invariant the
|
|
397
|
+
// raw already rides. It REPORTS and decides nothing; the ceiling still binds
|
|
398
|
+
// through `capStop` between calls, untouched.
|
|
399
|
+
onCall({ ...call });
|
|
388
400
|
return raws.push(scrubRaw({ label, attempt, text: r?.text })) - 1;
|
|
389
401
|
};
|
|
390
402
|
// TIGHTEN-ONLY: an operator may lower the ceiling, never raise it. A garbage
|