bareloop 0.11.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 +159 -0
- package/README.md +3 -2
- package/bareloop.context.md +282 -8
- package/package.json +1 -1
- package/src/authorflow.js +176 -33
- package/src/authoring.js +170 -25
- package/src/authorjob.js +340 -26
- package/src/bridges.js +237 -10
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/declaredclose.js +108 -10
- package/src/index.js +84 -8
- package/src/job.js +27 -13
- package/src/judged.js +765 -0
- package/src/kinds.js +390 -6
- package/src/planrun.js +328 -15
- package/src/reuse.js +319 -89
- package/src/reviewdoor.js +246 -0
- package/src/run.js +73 -6
- package/src/selection.js +22 -6
- package/types/authorflow.d.ts +147 -9
- package/types/authoring.d.ts +54 -8
- package/types/authorjob.d.ts +36 -6
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/declaredclose.d.ts +34 -5
- package/types/index.d.ts +10 -6
- package/types/job.d.ts +22 -9
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +151 -4
- package/types/planrun.d.ts +56 -2
- package/types/reuse.d.ts +144 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +49 -1
package/src/authorjob.js
CHANGED
|
@@ -91,11 +91,16 @@
|
|
|
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';
|
|
103
|
+
import { QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook } from './authorflow.js';
|
|
99
104
|
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
100
105
|
import {
|
|
101
106
|
DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
|
|
@@ -369,10 +374,11 @@ function composerRefusal(reds) {
|
|
|
369
374
|
* onPhase?: (phase: string, data?: any) => void,
|
|
370
375
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
371
376
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
372
|
-
* authorFn?: Function, authorOpts?: object
|
|
377
|
+
* authorFn?: Function, authorOpts?: object,
|
|
378
|
+
* signerFix?: Function|any, proposeFn?: Function, compileOpts?: object}} o
|
|
373
379
|
* @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
|
|
374
|
-
* closeDecl: any, seedRef: string|null, authoring: any, reds: Red[],
|
|
375
|
-
* cost: any, interview: any}>}
|
|
380
|
+
* closeDecl: any, seedRef: string|null, authoring: any, judged: any, reds: Red[],
|
|
381
|
+
* stop: string|null, cost: any, interview: any}>}
|
|
376
382
|
*/
|
|
377
383
|
export async function authorCloseForJob({
|
|
378
384
|
answers, repoPath = null, lang, verdictType = null, questions = null,
|
|
@@ -387,11 +393,16 @@ export async function authorCloseForJob({
|
|
|
387
393
|
onPhase = () => {}, onCall = () => {},
|
|
388
394
|
seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
|
|
389
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 = {},
|
|
390
401
|
}) {
|
|
391
402
|
/** @type {any} */
|
|
392
403
|
const base = {
|
|
393
404
|
ok: false, refusal: null, verdictType: null, closeDecl: null, seedRef: null,
|
|
394
|
-
authoring: null, reds: [], stop: null, cost: null, interview: null,
|
|
405
|
+
authoring: null, judged: null, reds: [], stop: null, cost: null, interview: null,
|
|
395
406
|
};
|
|
396
407
|
|
|
397
408
|
const interview = runInterview({ answers, verdictType, repoPath, questions });
|
|
@@ -528,29 +539,111 @@ export async function authorCloseForJob({
|
|
|
528
539
|
// declaration above it), and stated here rather than re-derived. The remaining
|
|
529
540
|
// precondition (D9.3 — the close has work to do) is measured by
|
|
530
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
|
+
|
|
531
639
|
return {
|
|
532
640
|
ok: true,
|
|
533
641
|
refusal: null,
|
|
534
642
|
verdictType: picked,
|
|
535
|
-
closeDecl
|
|
536
|
-
genre: GENRE,
|
|
537
|
-
lang,
|
|
538
|
-
stages: authored.declaration.stages,
|
|
539
|
-
...(Object.keys(applied).length ? { genreEnv: { ...applied } } : {}),
|
|
540
|
-
// `notes` is the one FREE-TEXT field the model writes straight into the
|
|
541
|
-
// signed spec, and this is that field's persist boundary. Scrubbed here
|
|
542
|
-
// for the same reason `scrubRed` scrubs uniformly a few lines down: every
|
|
543
|
-
// other model-authored string that survives this call rides the ONE
|
|
544
|
-
// redactor, and a signed spec outlives the run that produced it.
|
|
545
|
-
// `redactSecrets` returns a non-matching string byte-identical, so a note
|
|
546
|
-
// carrying nothing hashes exactly as it did — the mask costs the honest
|
|
547
|
-
// case nothing, which is the fail-safe direction.
|
|
548
|
-
...(authored.declaration.notes?.length
|
|
549
|
-
? { notes: authored.declaration.notes.map((/** @type {any} */ n) => (typeof n === 'string' ? redactSecrets(n) : n)) }
|
|
550
|
-
: {}),
|
|
551
|
-
},
|
|
643
|
+
closeDecl,
|
|
552
644
|
seedRef: seed,
|
|
553
645
|
authoring: authored,
|
|
646
|
+
judged,
|
|
554
647
|
reds: authored.reds,
|
|
555
648
|
stop: authored.stop,
|
|
556
649
|
cost: authored.cost,
|
|
@@ -631,6 +724,166 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
631
724
|
Object.entries(r).map(([k, v]) => [k, typeof v === 'string' ? redactSecrets(v) : v]),
|
|
632
725
|
));
|
|
633
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
|
+
|
|
634
887
|
/**
|
|
635
888
|
* D9, in code: *nothing JUDGES the close; three mechanical gates plus a
|
|
636
889
|
* signature.* No LLM validates another LLM's close, here or anywhere.
|
|
@@ -644,6 +897,24 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
644
897
|
* A close with NO work stage red at seed has nothing to do and is refused
|
|
645
898
|
* decision-ready — gate-2 round 2's arm B died exactly there, on a mypy that
|
|
646
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.
|
|
647
918
|
*
|
|
648
919
|
* Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
|
|
649
920
|
* job creation the tree IS the seed, so "does every stage run against the real
|
|
@@ -656,18 +927,32 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
656
927
|
* comes back is the resolved spec, its hash, and the evidence to sign against.
|
|
657
928
|
*
|
|
658
929
|
* @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
|
|
659
|
-
* 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
|
|
660
933
|
* @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
|
|
661
934
|
* gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
|
|
662
935
|
*/
|
|
663
936
|
export async function prepareSigning({
|
|
664
937
|
spec, workdir, seedRef = null, shellCapUsd, timeoutMs,
|
|
665
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 = [],
|
|
666
951
|
}) {
|
|
667
952
|
/** @type {any} */
|
|
668
953
|
const base = {
|
|
669
954
|
ok: false, specHash: null, seedRef: null, work: [], guards: [], stops: [], reds: [], refusal: null,
|
|
670
|
-
gates: { declaration: null, precheck: null, seedVerdict: null },
|
|
955
|
+
gates: { declaration: null, precheck: null, seedVerdict: null, calibration: null },
|
|
671
956
|
};
|
|
672
957
|
|
|
673
958
|
// ── gate 1a: the spec itself. A declared close that is not on a plan-shape
|
|
@@ -803,12 +1088,41 @@ export async function prepareSigning({
|
|
|
803
1088
|
workRed: workRed.map((r) => r.stage),
|
|
804
1089
|
};
|
|
805
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
|
+
|
|
806
1105
|
// D9.3 — nothing red at seed means nothing to do. Round 2's arm B read GREEN
|
|
807
1106
|
// with value 0 because mypy died on an unrelated broken fixture and scanned
|
|
808
1107
|
// nothing: an instrument scanning nothing reads clean, which is F6's shape at
|
|
809
1108
|
// the close-authoring layer, and this gate is what refuses it BEFORE a
|
|
810
1109
|
// signature rather than after a paid run.
|
|
811
|
-
|
|
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) {
|
|
812
1126
|
return {
|
|
813
1127
|
...base,
|
|
814
1128
|
work,
|