bareloop 0.23.0 → 0.24.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 +122 -0
- package/bareloop.context.md +56 -19
- package/package.json +1 -1
- package/src/authorflow.js +17 -7
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/calibrate.js +33 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +36 -5
- package/src/declaredclose.js +17 -10
- package/src/index.js +8 -5
- package/src/job.js +58 -3
- package/src/judged.js +132 -14
- package/src/kinds.js +77 -12
- package/src/planrun.js +14 -2
- package/src/providers.js +143 -18
- package/src/run.js +9 -4
- package/src/validate.js +4 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/calibrate.d.ts +9 -2
- package/types/cardauthor.d.ts +13 -11
- package/types/cli.d.ts +2 -1
- package/types/declaredclose.d.ts +2 -0
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +22 -1
- package/types/judged.d.ts +123 -14
- package/types/kinds.d.ts +42 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +73 -11
- package/types/run.d.ts +9 -3
package/src/authorjob.js
CHANGED
|
@@ -35,11 +35,19 @@
|
|
|
35
35
|
// same way `repoPath` does — a closed set handed over enumerated, never read out
|
|
36
36
|
// of prose.
|
|
37
37
|
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
38
|
+
// THE MENU IS TWO SHAPES: `green` (a command decides done) and `soft-green` (a
|
|
39
|
+
// rubric decides it, through the judged floor). Both are LIVE and both author
|
|
40
|
+
// here. `hitl` is named by the radio, still validates and still runs, and is NOT
|
|
41
|
+
// OFFERED — the class moved to fwdloop (PRD items 29 and 31.1); picking it
|
|
42
|
+
// returns the honest counted refusal on the `request-red` admission path (D13's
|
|
43
|
+
// mechanism), carrying demand for a VERDICT CLASS rather than only for a genre.
|
|
44
|
+
// It refuses at ADMISSION — before that class's questions are ever asked.
|
|
45
|
+
//
|
|
46
|
+
// This comment said the opposite until 2026-09-09 (*"v1 STILL ADMITS ONLY
|
|
47
|
+
// `green`"*), which was false for a long time and sent one build in the wrong
|
|
48
|
+
// direction: `LOCKED_CLASSES` was empty, so `runInterview` returned `ok:true` for
|
|
49
|
+
// every class including the retired one. Check `MENU_CLASSES` at runtime rather
|
|
50
|
+
// than trusting any sentence here, this one included.
|
|
43
51
|
//
|
|
44
52
|
// AND THE INTERVIEW KEYS ON THE CLASS. Three frozen question sets replace one
|
|
45
53
|
// set per genre: genres are a fat tail that cannot be enumerated, classes are
|
|
@@ -91,14 +99,15 @@
|
|
|
91
99
|
// in the stored declaration (forward-compat point 3), and the suite pins it.
|
|
92
100
|
|
|
93
101
|
import { jobSpecHash, validateJob } from './job.js';
|
|
94
|
-
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, SEED_EXEMPT_KINDS,
|
|
102
|
+
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, SEED_EXEMPT_KINDS, judgedStages, closeJudges } from './kinds.js';
|
|
95
103
|
// SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
|
|
96
104
|
// and the gate that decides whether the ruler they describe is a ruler.
|
|
97
105
|
import { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts } from './cardauthor.js';
|
|
98
106
|
import { runCalibration } from './calibrate.js';
|
|
99
|
-
import {
|
|
107
|
+
import { CALIBRATION_SIZE } from './judged.js';
|
|
100
108
|
import {
|
|
101
109
|
GENRE_LANGUAGES, LOCKED_KINDS, TYPES_GENRE, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES,
|
|
110
|
+
UNLISTED_CLASSES, MENU_CLASSES,
|
|
102
111
|
} from './authoring.js';
|
|
103
112
|
import { QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook } from './authorflow.js';
|
|
104
113
|
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
@@ -123,7 +132,7 @@ export { QUESTION_SETS, questionsFor, requiredAnswersFor };
|
|
|
123
132
|
* rule about it: illegal becomes inexpressible instead of rejected after the
|
|
124
133
|
* fact (the `check-passes(name)` model). A UI renders it as three buttons, and
|
|
125
134
|
* two of them are marked locked. */
|
|
126
|
-
export { VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES };
|
|
135
|
+
export { VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, UNLISTED_CLASSES, MENU_CLASSES };
|
|
127
136
|
|
|
128
137
|
/** The territory every refusal here lands against. bareloop's OWN catalogue —
|
|
129
138
|
* the genre menu, the verdict menu, the kind menu — never a bare-suite package.
|
|
@@ -141,6 +150,15 @@ const LEVERS = ([
|
|
|
141
150
|
'wait for the verdict-classes rung — this refusal is the evidence it waits on',
|
|
142
151
|
]);
|
|
143
152
|
|
|
153
|
+
/** the levers for an UNLISTED class (PRD item 31.1): the class is BUILT, so
|
|
154
|
+
* "wait for the rung" is the wrong sentence — there is no rung left to wait on.
|
|
155
|
+
* The honest next step is the other product, or a class this one still offers. */
|
|
156
|
+
/** @type {string[]} */
|
|
157
|
+
const UNLISTED_LEVERS = ([
|
|
158
|
+
'restate the job for a class bareloop still offers: green (a command decides done) or soft-green (a rubric does)',
|
|
159
|
+
'take a job that needs a person\'s sign-off to fwdloop — that product is built for the human-in-the-loop shape',
|
|
160
|
+
]);
|
|
161
|
+
|
|
144
162
|
/**
|
|
145
163
|
* One refusal, in the shape both the caller and the ledger need.
|
|
146
164
|
* @param {{verb: string|null, path: string, detail: string, kind?: 'request-red'|'decision-ready', options?: string[]}} o
|
|
@@ -236,17 +254,33 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
|
|
|
236
254
|
return out({});
|
|
237
255
|
}
|
|
238
256
|
|
|
239
|
-
// A
|
|
240
|
-
// for the class itself — the verdict-classes rung is what this
|
|
241
|
-
|
|
257
|
+
// A class that is not ON THE MENU refuses HERE, before its questions are asked.
|
|
258
|
+
// Counted demand for the class itself — the verdict-classes rung is what this
|
|
259
|
+
// evidence is for. TWO reasons a pick lands here, and they are different facts
|
|
260
|
+
// (PRD item 31.1):
|
|
261
|
+
// * LOCKED — no guard battery exists, so nothing could validate the close.
|
|
262
|
+
// * UNLISTED — the class is built and still RUNS; this product just no longer
|
|
263
|
+
// offers it. `hitl` moved to fwdloop (item 29); its code, tests
|
|
264
|
+
// and runtime stay, and an existing hitl spec still validates.
|
|
265
|
+
// Either way the person cannot author one, and the refusal says which it is
|
|
266
|
+
// rather than pooling two different situations into one sentence.
|
|
267
|
+
if (LOCKED_CLASSES.includes(picked) || UNLISTED_CLASSES.includes(picked)) {
|
|
268
|
+
const unlisted = UNLISTED_CLASSES.includes(picked);
|
|
242
269
|
return out({
|
|
243
270
|
refusal: refuse({
|
|
244
271
|
verb: picked,
|
|
245
272
|
path: 'verdictType',
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
273
|
+
options: unlisted ? UNLISTED_LEVERS : LEVERS,
|
|
274
|
+
detail: unlisted
|
|
275
|
+
? `You picked "${picked}", and this product no longer offers that class: the menu is `
|
|
276
|
+
+ `${MENU_CLASSES.join(', ')} — a job whose done a command can decide, or one whose done needs `
|
|
277
|
+
+ 'judgement against a rubric. A person\'s sign-off moved to fwdloop, which is built for it. The '
|
|
278
|
+
+ 'class itself is not deleted — an existing spec still validates and still runs — so picking one is '
|
|
279
|
+
+ 'recorded as demand rather than dropped.'
|
|
280
|
+
: `You picked "${picked}", and v1 cannot close that yet: it admits ${MENU_CLASSES.join(', ')} only — a `
|
|
281
|
+
+ 'job whose done a command can decide. A class with no guard battery is named in the menu and locked, '
|
|
282
|
+
+ 'so picking one is recorded as demand rather than dropped, and this refusal is the evidence the '
|
|
283
|
+
+ 'verdict-classes rung waits on.',
|
|
250
284
|
}),
|
|
251
285
|
});
|
|
252
286
|
}
|
|
@@ -277,21 +311,32 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
|
|
|
277
311
|
}
|
|
278
312
|
if (reds.length) return out({ answers: given });
|
|
279
313
|
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
314
|
+
// A GENRE precondition, not a class one — and the difference is why this text
|
|
315
|
+
// was wrong until 2026-09-09 (PRD item 31.2). It used to answer a missing repo
|
|
316
|
+
// with "that needs a judged (soft-green) or a human close", which told a person
|
|
317
|
+
// who had just PICKED soft-green that they needed soft-green. Both live classes
|
|
318
|
+
// land here identically: D1's "a repo is never a precondition" holds for the
|
|
319
|
+
// INTERVIEW and not for D9's validity gates, all three of which rest on a
|
|
320
|
+
// runnable patient with a git seed, and the close CATALOGUE is code-genre only
|
|
321
|
+
// (doc-genre kinds are not admitted — PRD item 26). So a doc or a website job
|
|
322
|
+
// refuses for want of a SEED, whichever class it picked, on the same counted
|
|
285
323
|
// path. The class the user picked cannot be honoured, so it is not returned.
|
|
324
|
+
//
|
|
325
|
+
// The `verb` stays `non-green-verdict` deliberately. It is the ledger's key for
|
|
326
|
+
// this demand and renaming it would split one running count into two that look
|
|
327
|
+
// like different demands; the name is a poor description of a genre refusal,
|
|
328
|
+
// and that is a smaller cost than a discontinuous ledger.
|
|
286
329
|
if (!isNonEmptyString(repoPath)) {
|
|
287
330
|
return out({
|
|
288
331
|
answers: given,
|
|
289
332
|
refusal: refuse({
|
|
290
333
|
verb: 'non-green-verdict',
|
|
291
334
|
path: 'repoPath',
|
|
292
|
-
detail: 'This job has no code repository, so there is no seed to measure against and no changed set to
|
|
293
|
-
+ '
|
|
294
|
-
+ '
|
|
335
|
+
detail: 'This job has no code repository, so there is no seed to measure against and no changed set to '
|
|
336
|
+
+ 'read. This authoring flow drafts CODE-GENRE closes against a git seed only, and that is true of both '
|
|
337
|
+
+ 'classes it offers: a green close runs commands over the tree, and a soft-green close judges the '
|
|
338
|
+
+ 'artifacts the run changed in it. A job with no repository needs a doc-genre close, which is not built '
|
|
339
|
+
+ 'here yet.',
|
|
295
340
|
}),
|
|
296
341
|
});
|
|
297
342
|
}
|
|
@@ -370,7 +415,7 @@ function composerRefusal(reds) {
|
|
|
370
415
|
* @param {{answers: Record<string|number, any>, repoPath?: string|null, lang: string,
|
|
371
416
|
* verdictType?: string|null,
|
|
372
417
|
* questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
|
|
373
|
-
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
|
|
418
|
+
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
|
|
374
419
|
* onPhase?: (phase: string, data?: any) => void,
|
|
375
420
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
376
421
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
@@ -384,6 +429,11 @@ export async function authorCloseForJob({
|
|
|
384
429
|
answers, repoPath = null, lang, verdictType = null, questions = null,
|
|
385
430
|
generate, provider = null, seedRef = null, scout = null, listing = null,
|
|
386
431
|
ceilingUsd = null,
|
|
432
|
+
// THE JUDGE IDENTITY (PRD item 32.1), required only on the path that actually
|
|
433
|
+
// composes a judged stage — the stored calibration set carries the judge that
|
|
434
|
+
// certified it, and there is no library pin to fall back to. Absent on a purely
|
|
435
|
+
// mechanical close is correct and costs nothing: that close never judges.
|
|
436
|
+
judgeModel = null,
|
|
387
437
|
// THE PROGRESS SEAMS. This composition is the pipeline's long silence — a real
|
|
388
438
|
// survey, a real declaration ladder, and a real toolchain per close stage — and
|
|
389
439
|
// it emitted nothing until it returned, so an operator watching a live run
|
|
@@ -573,7 +623,7 @@ export async function authorCloseForJob({
|
|
|
573
623
|
// as the caller-error guard it is.
|
|
574
624
|
/** @type {any} */
|
|
575
625
|
let judged = null;
|
|
576
|
-
if ((closeDecl
|
|
626
|
+
if (closeJudges(closeDecl)) {
|
|
577
627
|
// ONE CEILING, ALL THREE PAID SEAMS. The declaration loop's own calls are
|
|
578
628
|
// ABSORBED before the compile is asked for, so the ceiling folds in prior
|
|
579
629
|
// spend and re-invoking a seam cannot silently widen it. `absorb` deliberately
|
|
@@ -620,7 +670,14 @@ export async function authorCloseForJob({
|
|
|
620
670
|
// non-array before anything can be `ok` — and the narrowing is stated rather
|
|
621
671
|
// than defaulted: `?? []` here would fold an EMPTY signed set into a close,
|
|
622
672
|
// which is the one direction that must never be reachable.
|
|
623
|
-
|
|
673
|
+
if (typeof judgeModel !== 'string' || judgeModel.trim() === '') {
|
|
674
|
+
throw new Error('[authorjob] authorCloseForJob composed a JUDGED close and was given no judgeModel (PRD item '
|
|
675
|
+
+ '32.1). The stored calibration set carries the judge that certified it, and a set stamped with a model '
|
|
676
|
+
+ 'nobody named is exactly the unattributable floor that field exists to prevent.');
|
|
677
|
+
}
|
|
678
|
+
closeDecl = foldJudgedArtifacts(closeDecl, {
|
|
679
|
+
card: signed.card, cases: /** @type {any[]} */ (signed.cases), judgeModel,
|
|
680
|
+
});
|
|
624
681
|
return {
|
|
625
682
|
ok: true,
|
|
626
683
|
refusal: null,
|
|
@@ -760,12 +817,15 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
|
|
|
760
817
|
* The book is built HERE and PRIOR SPEND IS ABSORBED into it, so the ceiling folds in
|
|
761
818
|
* what the earlier seams already spent and re-invoking a seam cannot silently widen
|
|
762
819
|
* it; the gate reads it between calls and stops as a refusal that names money.
|
|
763
|
-
*
|
|
820
|
+
* `judgeModel` is the RESOLVED judge identity (PRD item 32.1), required and with no
|
|
821
|
+
* default: it is stamped onto the graded set and onto the calibration-missing record
|
|
822
|
+
* alike, so both say which judge the close would have been certified by.
|
|
823
|
+
* @param {{spec: any, judgedStages: any[], judgeLoop: Function|null, judgeModel: string,
|
|
764
824
|
* onJudgeCost: ((c: any) => void)|null, calibrateFn: Function,
|
|
765
825
|
* ceilingUsd: number|null, priorCalls: any[]}} o
|
|
766
826
|
* @returns {Promise<{ok: boolean, record: any, reds: Red[], refusal: Refusal|null}>}
|
|
767
827
|
*/
|
|
768
|
-
async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, calibrateFn, ceilingUsd, priorCalls }) {
|
|
828
|
+
async function calibrationGate({ spec, judgedStages, judgeLoop, judgeModel, onJudgeCost, calibrateFn, ceilingUsd, priorCalls }) {
|
|
769
829
|
const cases = spec.closeDecl?.calibration?.cases ?? null;
|
|
770
830
|
// ONE card, and it is the SIGNED one — the card the calibration is graded
|
|
771
831
|
// against must be the card the close will RUN, or the gate certifies a ruler
|
|
@@ -779,7 +839,7 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, cal
|
|
|
779
839
|
+ 'ruler is unmeasured, and an unmeasured ruler cannot be signed.';
|
|
780
840
|
return {
|
|
781
841
|
ok: false,
|
|
782
|
-
record: { ok: false, stop: 'calibration-missing', judgeModel
|
|
842
|
+
record: { ok: false, stop: 'calibration-missing', judgeModel, required: CALIBRATION_SIZE },
|
|
783
843
|
reds: [{ code: 'calibration-missing', path: 'closeDecl.calibration.cases', detail }],
|
|
784
844
|
refusal: refuse({
|
|
785
845
|
kind: 'decision-ready',
|
|
@@ -805,6 +865,7 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, cal
|
|
|
805
865
|
cases,
|
|
806
866
|
card,
|
|
807
867
|
judgeLoop,
|
|
868
|
+
judgeModel,
|
|
808
869
|
// the gate's own calls join the same tally the ceiling is read against — a
|
|
809
870
|
// ceiling that cannot see the spend it bounds is F45's blind detector in a
|
|
810
871
|
// money coat. The caller's reporter fires with the call's whole record, after.
|
|
@@ -942,8 +1003,8 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, onJudgeCost, cal
|
|
|
942
1003
|
*
|
|
943
1004
|
* @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
|
|
944
1005
|
* timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function,
|
|
945
|
-
* judgeLoop?: Function|null, onJudgeCost?: ((c: any) => void)|null,
|
|
946
|
-
* ceilingUsd?: number|null, priorCalls?: any[]}} o
|
|
1006
|
+
* judgeLoop?: Function|null, judgeModel?: string|null, onJudgeCost?: ((c: any) => void)|null,
|
|
1007
|
+
* calibrateFn?: Function, ceilingUsd?: number|null, priorCalls?: any[]}} o
|
|
947
1008
|
* @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
|
|
948
1009
|
* gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
|
|
949
1010
|
*/
|
|
@@ -953,8 +1014,11 @@ export async function prepareSigning({
|
|
|
953
1014
|
// THE PAID SEAM, and it is the operator's to wire — this module owns no
|
|
954
1015
|
// provider and picks none (the same contract `authorCloseForJob` keeps for the
|
|
955
1016
|
// drafting model). Absent is never a fall-back: a judged close whose gate could
|
|
956
|
-
// not run is refused, not waved through.
|
|
957
|
-
|
|
1017
|
+
// not run is refused, not waved through. `judgeModel` is the seam's other half
|
|
1018
|
+
// (PRD item 32.1) — WHICH model that loop drives, resolved by the caller
|
|
1019
|
+
// (`resolveJudge`, src/judged.js). The two travel together: a gate that grades
|
|
1020
|
+
// without recording its judge certifies a floor nobody can attribute.
|
|
1021
|
+
judgeLoop = null, judgeModel = null, onJudgeCost = null, calibrateFn = runCalibration,
|
|
958
1022
|
// THE OPERATOR'S CEILING, and the spend it has already met. `null`/absent is
|
|
959
1023
|
// UNBOUNDED and is a stated choice, never a default invented here (the same
|
|
960
1024
|
// contract `authorCloseForJob` keeps). `priorCalls` is what the earlier paid
|
|
@@ -1109,9 +1173,17 @@ export async function prepareSigning({
|
|
|
1109
1173
|
// validator checks a stored set for LEGALITY only and deliberately allows a
|
|
1110
1174
|
// judged stage with no set at all, because making it mandatory is a signing
|
|
1111
1175
|
// decision and this is the signing gate.
|
|
1112
|
-
const
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1176
|
+
const judged = judgedStages(spec.closeDecl);
|
|
1177
|
+
if (judged.length) {
|
|
1178
|
+
if (judgeLoop !== null && (typeof judgeModel !== 'string' || judgeModel.trim() === '')) {
|
|
1179
|
+
throw new Error('[authorjob] prepareSigning was wired a judgeLoop with no judgeModel (PRD item 32.1). The two '
|
|
1180
|
+
+ 'halves of the judge seam travel together: the graded set is stamped with the judge that certified it, and '
|
|
1181
|
+
+ 'there is no library pin to fall back to.');
|
|
1182
|
+
}
|
|
1183
|
+
const cal = await calibrationGate({
|
|
1184
|
+
spec, judgedStages: judged, judgeLoop, judgeModel: /** @type {string} */ (judgeModel),
|
|
1185
|
+
onJudgeCost, calibrateFn, ceilingUsd, priorCalls,
|
|
1186
|
+
});
|
|
1115
1187
|
base.gates.calibration = cal.record;
|
|
1116
1188
|
if (!cal.ok) return { ...base, work, guards, reds: cal.reds, refusal: cal.refusal };
|
|
1117
1189
|
}
|
package/src/calibrate.js
CHANGED
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
// calibration becomes a refusal, exactly as D9's other three gates work.
|
|
47
47
|
// * IT PICKS NO NUMBER. The size is `CALIBRATION_SIZE` (hamr's), the floor is
|
|
48
48
|
// all-of-them (hamr's), the ladder is `JUDGE_ATTEMPTS` (operator-confirmed),
|
|
49
|
-
// and the model is
|
|
49
|
+
// and the model is the run's RESOLVED judge identity, handed in (PRD item
|
|
50
|
+
// 32.1) rather than pinned in this library. Nothing here invents a threshold
|
|
51
|
+
// and nothing here picks a judge.
|
|
50
52
|
// * IT OWNS NO PROVIDER. The judge seam arrives as `judgeLoop`; an absent one
|
|
51
53
|
// is a WIRING GAP that stops the gate, never a silent skip — a judged close
|
|
52
54
|
// signed off a calibration nobody ran is the exact state this gate exists to
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
|
|
59
61
|
import { createHash } from 'node:crypto';
|
|
60
62
|
import {
|
|
61
|
-
|
|
63
|
+
LOCATE_LABEL, LOCATE_AXES, CALIBRATION_SIZE,
|
|
62
64
|
runLocate, decide, expectedOf, validateCard, validateCalibrationSet,
|
|
63
65
|
} from './judged.js';
|
|
64
66
|
import { JUDGE_ATTEMPTS } from './kinds.js';
|
|
@@ -309,11 +311,11 @@ export function factsResist(facts, gold, artifactText) {
|
|
|
309
311
|
* when the model is malforming, which is the worst moment for a paid ladder to be
|
|
310
312
|
* unbounded. A stop comes back as its own field rather than as a `red`: a ceiling
|
|
311
313
|
* is the OPERATOR's governance, never the transport's casualty.
|
|
312
|
-
* @param {{artifactText: string, card: any, judgeLoop: Function, attempts: number,
|
|
314
|
+
* @param {{artifactText: string, card: any, judgeLoop: Function, judgeModel: string, attempts: number,
|
|
313
315
|
* meter: (c: any) => void, id: string, kind: string,
|
|
314
316
|
* capStop: () => 'cap-halt'|'pricing-red'|null}} o
|
|
315
317
|
*/
|
|
316
|
-
async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, kind, capStop }) {
|
|
318
|
+
async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, meter, id, kind, capStop }) {
|
|
317
319
|
/** @type {any} */
|
|
318
320
|
let last = null;
|
|
319
321
|
/** @type {any[]} */
|
|
@@ -341,7 +343,7 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
341
343
|
// when ITS OWN wall is unbounded (`PROVIDER_TIMEOUT_MS`, src/clock.js).
|
|
342
344
|
callBounds: { timeoutMs: PROVIDER_TIMEOUT_MS },
|
|
343
345
|
onCost: (c) => meter({
|
|
344
|
-
id, kind, attempt, label: CALIBRATION_LABEL, model:
|
|
346
|
+
id, kind, attempt, label: CALIBRATION_LABEL, model: judgeModel,
|
|
345
347
|
costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
346
348
|
}),
|
|
347
349
|
});
|
|
@@ -381,7 +383,13 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
381
383
|
* graded one: `ok` is false, the cases nobody bought are absent rather than recorded
|
|
382
384
|
* as failures, and `casualty` stays null — the transport was fine, the operator's own
|
|
383
385
|
* governance stopped the gate.
|
|
384
|
-
*
|
|
386
|
+
* `judgeModel` is REQUIRED and has no default (PRD item 32.1). It is the identity
|
|
387
|
+
* the caller resolved (`resolveJudge`, src/judged.js) and it is what the graded set
|
|
388
|
+
* is STAMPED with — `setHash` folds it in, so a set certified by one judge cannot be
|
|
389
|
+
* mistaken for the same set certified by another. A default here would be the silent
|
|
390
|
+
* fall-back the whole judge-identity change exists to remove: a gate that grades
|
|
391
|
+
* under a model nobody named certifies a floor nobody can attribute.
|
|
392
|
+
* @param {{cases: any, card: any, judgeLoop: Function|null, judgeModel: string,
|
|
385
393
|
* onCost?: (c: any) => void, attempts?: number, battery?: any[], batteryCard?: any,
|
|
386
394
|
* capStop?: () => 'cap-halt'|'pricing-red'|null}} o
|
|
387
395
|
* @returns {Promise<{ok: boolean, stop: string|null, casualty: any,
|
|
@@ -392,12 +400,25 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
392
400
|
* costUsd: number|null, knownUsd: number, spendComplete: boolean, calls: any[]}>}
|
|
393
401
|
*/
|
|
394
402
|
export async function runCalibration({
|
|
395
|
-
cases, card, judgeLoop, onCost = () => {},
|
|
403
|
+
cases, card, judgeLoop, judgeModel, onCost = () => {},
|
|
396
404
|
attempts = JUDGE_ATTEMPTS,
|
|
397
405
|
battery = /** @type {any[]} */ (/** @type {unknown} */ (INJECTION_LOCATE_BATTERY)),
|
|
398
406
|
batteryCard = INJECTION_CARD,
|
|
399
407
|
capStop = () => null,
|
|
400
408
|
}) {
|
|
409
|
+
// The mandatory-identity throw guards a REAL judge seam graded under nobody's
|
|
410
|
+
// name — it does not apply when there is no seam at all. A caller wiring no
|
|
411
|
+
// `judgeLoop` (an adopter who has not connected a judge yet) legitimately hands
|
|
412
|
+
// in no `judgeModel` either, and that case is the WIRING-GAP `no-judge` stop
|
|
413
|
+
// below, returned as a red rather than thrown. Throwing here ahead of that
|
|
414
|
+
// check would make the wiring-gap stop unreachable through its own documented
|
|
415
|
+
// path — `prepareSigning` (src/authorjob.js) draws exactly this same line with
|
|
416
|
+
// its own `judgeLoop !== null` guard, and this function has to agree with it.
|
|
417
|
+
if (typeof judgeLoop === 'function' && (typeof judgeModel !== 'string' || judgeModel.trim() === '')) {
|
|
418
|
+
throw new Error('[calibrate] runCalibration needs the resolved judgeModel (PRD item 32.1): the graded set is '
|
|
419
|
+
+ 'STAMPED with the judge that certified it, and a set nobody can attribute is a floor nobody can tell apart '
|
|
420
|
+
+ 'from one graded by a different model. There is no library pin to fall back to.');
|
|
421
|
+
}
|
|
401
422
|
/** @type {any[]} */
|
|
402
423
|
const calls = [];
|
|
403
424
|
/** @param {any} c */
|
|
@@ -413,7 +434,7 @@ export async function runCalibration({
|
|
|
413
434
|
return /** @type {any} */ ({
|
|
414
435
|
ok: false, stop: null, casualty: null, graded: [], failures: [],
|
|
415
436
|
injection: { styles: [], allResisted: false, leaks: [] },
|
|
416
|
-
reds: [], judgeModel
|
|
437
|
+
reds: [], judgeModel,
|
|
417
438
|
cardHash: null, casesHash: null, setHash: null,
|
|
418
439
|
costUsd: t.costUsd, knownUsd: t.knownUsd, spendComplete: t.spendComplete, calls: calls.map((c) => ({ ...c })),
|
|
419
440
|
...extra,
|
|
@@ -464,7 +485,7 @@ export async function runCalibration({
|
|
|
464
485
|
code: 'no-judge-seam',
|
|
465
486
|
path: 'judgeLoop',
|
|
466
487
|
detail: `the calibration gate runs ${CALIBRATION_SIZE} real locate calls plus ${battery.length} injection `
|
|
467
|
-
+ `artifacts through ${
|
|
488
|
+
+ `artifacts through ${judgeModel}, and no judge seam was wired. An absent seam is an adopter wiring gap and `
|
|
468
489
|
+ 'never a fall-back: a gate that grades nothing would certify every close it was asked about',
|
|
469
490
|
}],
|
|
470
491
|
});
|
|
@@ -472,7 +493,7 @@ export async function runCalibration({
|
|
|
472
493
|
|
|
473
494
|
const cardHash = artifactHash(card);
|
|
474
495
|
const casesHash = artifactHash(cases);
|
|
475
|
-
const setHash = artifactHash({ card, cases, judgeModel
|
|
496
|
+
const setHash = artifactHash({ card, cases, judgeModel });
|
|
476
497
|
const stamped = { cardHash, casesHash, setHash };
|
|
477
498
|
|
|
478
499
|
// ── the signed ten ────────────────────────────────────────────────────────
|
|
@@ -480,7 +501,7 @@ export async function runCalibration({
|
|
|
480
501
|
const graded = [];
|
|
481
502
|
for (const c of cases) {
|
|
482
503
|
const r = await pipeOnce({
|
|
483
|
-
artifactText: String(c.artifact), card, judgeLoop: /** @type {Function} */ (judgeLoop),
|
|
504
|
+
artifactText: String(c.artifact), card, judgeLoop: /** @type {Function} */ (judgeLoop), judgeModel,
|
|
484
505
|
attempts, meter, id: String(c.id), kind: 'case', capStop,
|
|
485
506
|
});
|
|
486
507
|
// the ceiling, ahead of the casualty branch: a call nobody funded is not a call
|
|
@@ -527,7 +548,7 @@ export async function runCalibration({
|
|
|
527
548
|
const styles = [];
|
|
528
549
|
for (const b of battery) {
|
|
529
550
|
const r = await pipeOnce({
|
|
530
|
-
artifactText: b.artifact, card: batteryCard, judgeLoop: /** @type {Function} */ (judgeLoop),
|
|
551
|
+
artifactText: b.artifact, card: batteryCard, judgeLoop: /** @type {Function} */ (judgeLoop), judgeModel,
|
|
531
552
|
attempts, meter, id: b.id, kind: 'injection', capStop,
|
|
532
553
|
});
|
|
533
554
|
if (r.budget) {
|
package/src/cardauthor.js
CHANGED
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
// pipe produces are one shape read twice (module 5 compares them mechanically).
|
|
61
61
|
|
|
62
62
|
import {
|
|
63
|
-
JUDGE_RULE_IDS, JUDGE_RULES, CALIBRATION_SIZE, CASE_VERDICTS, validateJudgedArtifacts,
|
|
63
|
+
JUDGE_RULE_IDS, JUDGE_RULES, CALIBRATION_SIZE, CASE_VERDICTS, validateJudgedArtifacts,
|
|
64
64
|
} from './judged.js';
|
|
65
65
|
import { askStructured, makeCostBook } from './authorflow.js';
|
|
66
|
-
import {
|
|
66
|
+
import { judgedStages } from './kinds.js';
|
|
67
67
|
import { isObj, redactSecrets } from './validate.js';
|
|
68
68
|
|
|
69
69
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -363,15 +363,16 @@ export function signJudgedArtifacts({ proposal, fix = null }) {
|
|
|
363
363
|
* - THE CASES land beside the stages as `calibration.cases` — a close-level
|
|
364
364
|
* artifact, because the set calibrates the RULER and not one stage's params,
|
|
365
365
|
* and because module 5's gate reads it before any stage runs.
|
|
366
|
-
* - THE JUDGE MODEL lands beside them as `calibration.judgeModel
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* the set has to carry which
|
|
371
|
-
* signed floor from a
|
|
372
|
-
* a
|
|
373
|
-
* re-sign is a re-run of the calibration gate.
|
|
374
|
-
*
|
|
366
|
+
* - THE JUDGE MODEL lands beside them as `calibration.judgeModel`. Since PRD item
|
|
367
|
+
* 32.1 that is the RESOLVED judge identity the caller hands in (`resolveJudge`,
|
|
368
|
+
* src/judged.js — the spec's signed `judge` override, else the job's own worker
|
|
369
|
+
* model), not a library pin. A calibration set is only worth the judge that
|
|
370
|
+
* graded it, so the set has to carry which judge certified it or nothing
|
|
371
|
+
* downstream can tell a signed floor from a moved one. Storing it HERE, inside
|
|
372
|
+
* the spec, is what makes a judge change flip `jobSpecHash`: the signature dies,
|
|
373
|
+
* the signer re-signs, and a re-sign is a re-run of the calibration gate. It is
|
|
374
|
+
* REQUIRED, with no default — a set stamped with a model nobody named is exactly
|
|
375
|
+
* the unattributable floor this field exists to prevent (F45).
|
|
375
376
|
*
|
|
376
377
|
* A card with NOWHERE TO LAND THROWS. It is a caller's own broken input — a
|
|
377
378
|
* signed rubric folded into a close with no judged ruler — and there is no safe
|
|
@@ -380,20 +381,26 @@ export function signJudgedArtifacts({ proposal, fix = null }) {
|
|
|
380
381
|
* The input is never mutated: a closeDecl is a signed artefact and rewriting a
|
|
381
382
|
* caller's copy in place is how two readers come to disagree about what was
|
|
382
383
|
* signed.
|
|
383
|
-
* @param {any} closeDecl @param {{card: any, cases: any[]}} o
|
|
384
|
+
* @param {any} closeDecl @param {{card: any, cases: any[], judgeModel: string}} o
|
|
384
385
|
* @returns {any} a new closeDecl
|
|
385
386
|
*/
|
|
386
|
-
export function foldJudgedArtifacts(closeDecl, { card, cases }) {
|
|
387
|
+
export function foldJudgedArtifacts(closeDecl, { card, cases, judgeModel }) {
|
|
388
|
+
if (typeof judgeModel !== 'string' || judgeModel.trim() === '') {
|
|
389
|
+
throw new Error('[cardauthor] foldJudgedArtifacts needs the resolved judgeModel (PRD item 32.1): the stored set '
|
|
390
|
+
+ 'carries the judge that certified it, and there is no library pin to fall back to.');
|
|
391
|
+
}
|
|
387
392
|
const out = structuredClone(closeDecl);
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
// `judgedStages` filters `out`'s OWN stage objects, so the card assignment
|
|
394
|
+
// below mutates the clone in place — the reason this reads `out`, not the
|
|
395
|
+
// caller's `closeDecl`.
|
|
396
|
+
const judged = judgedStages(out);
|
|
390
397
|
if (judged.length !== 1) {
|
|
391
398
|
throw new Error(`[cardauthor] the signed card folds into the close's ONE judged-floor stage, and this close has `
|
|
392
399
|
+ `${judged.length} — a rubric with nowhere to land is signed evidence nothing reads, and a second judged stage `
|
|
393
400
|
+ 'is a second bar for one verdict');
|
|
394
401
|
}
|
|
395
402
|
judged[0].params = { ...(isObj(judged[0].params) ? judged[0].params : {}), card: structuredClone(card) };
|
|
396
|
-
out.calibration = { cases: structuredClone(cases), judgeModel
|
|
403
|
+
out.calibration = { cases: structuredClone(cases), judgeModel };
|
|
397
404
|
return out;
|
|
398
405
|
}
|
|
399
406
|
|
package/src/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
33
|
exportBundle, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps,
|
|
34
|
-
runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel,
|
|
34
|
+
runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel, resolveJudge,
|
|
35
35
|
} from './index.js';
|
|
36
36
|
import { resolveProvider, buildRunnerProviders } from './providers.js';
|
|
37
37
|
|
|
@@ -84,9 +84,34 @@ function buildProviders(apiKey, spec) {
|
|
|
84
84
|
const MODEL = modelResolution.model;
|
|
85
85
|
const tierModels = modelResolution.source === 'spec' ? { ...entry.tiers, sonnet: MODEL } : entry.tiers;
|
|
86
86
|
const baseUrl = isObj(spec) && typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// THE JUDGE, RESOLVED RATHER THAN PINNED (PRD item 32.1/32.2). The bundle
|
|
88
|
+
// runner's key contract is `ANTHROPIC_API_KEY`-only and the refusal above has
|
|
89
|
+
// already established that the WORKER provider reads that key — so a judge
|
|
90
|
+
// defaulting to the worker's own identity reads the same key by construction,
|
|
91
|
+
// and a signed `judge` naming a different provider would need a key this
|
|
92
|
+
// runner does not have. That case refuses here, at $0, by name — the same
|
|
93
|
+
// shape and the same reason as the worker refusal above, never a call made
|
|
94
|
+
// with the wrong vendor's key.
|
|
95
|
+
const judge = resolveJudge({
|
|
96
|
+
specJudge: isObj(spec) ? spec.judge : undefined,
|
|
97
|
+
workerProvider: providerName,
|
|
98
|
+
workerModel: MODEL,
|
|
89
99
|
});
|
|
100
|
+
const judgeEntry = resolveProvider(judge.provider);
|
|
101
|
+
if (judgeEntry.envKey !== 'ANTHROPIC_API_KEY') {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`bareloop run: this bundle's spec names judge provider "${judge.provider}", which reads `
|
|
104
|
+
+ `${judgeEntry.envKey}. The bundle runner's key contract is ANTHROPIC_API_KEY only, so it `
|
|
105
|
+
+ 'refuses rather than calling that provider with the wrong key. Run this job through scripts/run-u.mjs.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...buildRunnerProviders({
|
|
110
|
+
providerName, apiKey, model: MODEL, tierModels, baseUrl,
|
|
111
|
+
judgeApiKey: apiKey, judgeModel: judge.model, judgeProviderName: judge.provider, judgeBaseUrl: baseUrl,
|
|
112
|
+
}),
|
|
113
|
+
judgeModel: judge.model,
|
|
114
|
+
};
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
/** @param {string[]} args @returns {{ positional: string[], flags: Record<string, string|true> }} */
|
|
@@ -236,6 +261,11 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
236
261
|
// provider (the test seam). Absent and no injected provider: print the
|
|
237
262
|
// operator questions (the bundle's own README) and the hash, spend nothing.
|
|
238
263
|
let { provider, providerFor, judgeProvider } = deps;
|
|
264
|
+
// the judge IDENTITY travels with the judge PROVIDER (PRD item 32.1). A test
|
|
265
|
+
// injecting `deps.judgeProvider` may name it too; absent, it is resolved
|
|
266
|
+
// alongside the real providers below, and a judged stage with neither stops as
|
|
267
|
+
// a wiring gap rather than grading under a name nobody wrote.
|
|
268
|
+
let judgeModel = /** @type {string|null} */ (deps.judgeModel ?? null);
|
|
239
269
|
if (!provider) {
|
|
240
270
|
const apiKey = env.ANTHROPIC_API_KEY;
|
|
241
271
|
if (!apiKey) {
|
|
@@ -246,7 +276,7 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
246
276
|
return 0;
|
|
247
277
|
}
|
|
248
278
|
try {
|
|
249
|
-
({ provider, providerFor, judgeProvider } = buildProviders(apiKey, bundle.spec));
|
|
279
|
+
({ provider, providerFor, judgeProvider, judgeModel } = buildProviders(apiKey, bundle.spec));
|
|
250
280
|
} catch (e) {
|
|
251
281
|
// a refusal from the key contract above: name it and spend nothing
|
|
252
282
|
err(String(/** @type {any} */ (e)?.message ?? e));
|
|
@@ -338,6 +368,7 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
338
368
|
provider,
|
|
339
369
|
providerFor,
|
|
340
370
|
judgeProvider,
|
|
371
|
+
judgeModel,
|
|
341
372
|
emit,
|
|
342
373
|
closeDir,
|
|
343
374
|
shellCapUsd: runSpec.budgetUsd,
|
|
@@ -470,7 +501,7 @@ async function runMenu(deps, ctx) {
|
|
|
470
501
|
* code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
|
|
471
502
|
* queued stdout on its own (F-something: `process.exit()` can discard it).
|
|
472
503
|
* @param {string[]} argv
|
|
473
|
-
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, now?: () => number, stdin?: any }} deps
|
|
504
|
+
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any }} deps
|
|
474
505
|
* @returns {Promise<number>}
|
|
475
506
|
*/
|
|
476
507
|
export async function main(argv, deps = {}) {
|
package/src/declaredclose.js
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
// once instead of once-per-invocation, but `runPlan` has a dozen return paths and
|
|
69
69
|
// a leaked worktree outlives the process that made it.
|
|
70
70
|
|
|
71
|
-
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED, JUDGED_FLOOR_KIND } from './kinds.js';
|
|
71
|
+
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED, JUDGED_FLOOR_KIND, judgedStages } from './kinds.js';
|
|
72
72
|
import {
|
|
73
73
|
validateDeclaration, classGuards, genreOwnedEnvNames, ungroundedGenreEnv, GENRE_LANGUAGES, TYPES_GENRE,
|
|
74
74
|
VERDICT_CLASSES, LIVE_CLASSES, NEVER_OFFERED_KINDS,
|
|
@@ -424,8 +424,7 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
424
424
|
// redded here — making the set MANDATORY is the signing gate's call, and
|
|
425
425
|
// module 5 owns it.
|
|
426
426
|
if (closeDecl.calibration !== undefined) {
|
|
427
|
-
const judged = (
|
|
428
|
-
.filter((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND);
|
|
427
|
+
const judged = judgedStages(closeDecl);
|
|
429
428
|
if (!isObj(closeDecl.calibration)) {
|
|
430
429
|
red('invalid-value', `${at}.calibration`, 'the frozen calibration set, as an object carrying `cases`');
|
|
431
430
|
} else if (judged.length !== 1) {
|
|
@@ -567,6 +566,7 @@ function translate(r, redact) {
|
|
|
567
566
|
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
|
|
568
567
|
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
569
568
|
* judgeLoop?: ((o: {system: string}) => any)|null,
|
|
569
|
+
* judgeModel?: string|null,
|
|
570
570
|
* onJudgeCost?: ((c: any) => void)|null,
|
|
571
571
|
* callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
|
|
572
572
|
* @returns {Promise<any>}
|
|
@@ -574,7 +574,7 @@ function translate(r, redact) {
|
|
|
574
574
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
575
575
|
const {
|
|
576
576
|
timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
|
|
577
|
-
judgeLoop = null, onJudgeCost = null, callBounds = null,
|
|
577
|
+
judgeLoop = null, judgeModel = null, onJudgeCost = null, callBounds = null,
|
|
578
578
|
} = opts;
|
|
579
579
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
580
580
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
@@ -604,13 +604,20 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
604
604
|
// human stage reads the absence as "nobody has answered yet", which is the
|
|
605
605
|
// whole of what a pause means.
|
|
606
606
|
humanRuling,
|
|
607
|
-
// THE PAID SEAM and THE METER,
|
|
608
|
-
// every mechanical close. A judged stage with no seam STOPS as a
|
|
609
|
-
// rather than falling back to some other model (M1 owns no
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
607
|
+
// THE PAID SEAM, THE IDENTITY and THE METER, all operator territory and all
|
|
608
|
+
// absent on every mechanical close. A judged stage with no seam STOPS as a
|
|
609
|
+
// wiring gap rather than falling back to some other model (M1 owns no
|
|
610
|
+
// provider), and `judgeModel` is the seam's OTHER half (PRD item 32.1) —
|
|
611
|
+
// WHICH model the seam drives, resolved by the caller (`resolveJudge`,
|
|
612
|
+
// src/judged.js) and passed through untouched here, exactly as `judgeLoop`
|
|
613
|
+
// is. `runStage` checks the identity before the seam, so a judged stage with
|
|
614
|
+
// neither wired reports the identity gap, and one with only the seam missing
|
|
615
|
+
// still names the judge it would have graded with. Every locate call the
|
|
616
|
+
// seam buys is reported OUT through the meter so the run's ONE ledger can
|
|
617
|
+
// see it — a close has cost $0 until this kind, and a budget must fund the
|
|
618
|
+
// attempt PLUS its close.
|
|
613
619
|
judgeLoop,
|
|
620
|
+
judgeModel,
|
|
614
621
|
onJudgeCost,
|
|
615
622
|
// F152/PRD 30.4 — the SAME per-call time bounds every worker round already
|
|
616
623
|
// carries (`callBounds()`, src/planrun.js:1518), so the judged stage's own
|