bareloop 0.22.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 +247 -0
- package/README.md +1 -1
- package/bareloop.context.md +113 -29
- package/package.json +2 -2
- package/src/authorflow.js +24 -9
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/authorscout.js +83 -9
- package/src/calibrate.js +43 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +73 -21
- package/src/declaredclose.js +25 -11
- package/src/index.js +8 -5
- package/src/job.js +92 -2
- package/src/judged.js +240 -19
- package/src/kinds.js +89 -12
- package/src/planrun.js +194 -41
- package/src/providers.js +301 -0
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +9 -1
- package/src/run.js +68 -9
- package/src/transport.js +29 -12
- package/src/validate.js +4 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/authorscout.d.ts +27 -2
- 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 +8 -1
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +130 -16
- package/types/kinds.d.ts +51 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +166 -0
- package/types/ratelimit.d.ts +59 -0
- package/types/run.d.ts +17 -3
- package/types/transport.d.ts +10 -4
package/src/judged.js
CHANGED
|
@@ -65,32 +65,150 @@
|
|
|
65
65
|
|
|
66
66
|
import { createRequire } from 'node:module';
|
|
67
67
|
import { extractArtifact, priceOf, scrubRaw } from './text.js';
|
|
68
|
+
import { resolveProvider } from './providers.js';
|
|
68
69
|
|
|
69
70
|
const require = createRequire(import.meta.url);
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
* THE JUDGE
|
|
73
|
-
* the
|
|
74
|
-
*
|
|
75
|
-
* worth exactly as much as that evidence. A judge-model bump forces a full
|
|
76
|
-
* recalibration — mandatory, per the model-bump dead-weight replay rule.
|
|
73
|
+
* THE JUDGE'S HISTORICAL PIN. Until PRD item 32 (2026-09-10) this constant WAS
|
|
74
|
+
* the judge: every grading path imported it, so bareloop could run a worker on
|
|
75
|
+
* any provider and still judge only on Claude.
|
|
77
76
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* It is no longer read by any grading path. The judge identity is now RESOLVED
|
|
78
|
+
* per job (`resolveJudge` below) and travels as data — `ctx.judgeModel` into
|
|
79
|
+
* `runJudgedFloor`, an explicit argument into the calibration gate, and
|
|
80
|
+
* `closeDecl.calibration.judgeModel` into the signed spec, exactly as before.
|
|
81
|
+
* Keeping the export is deliberate: it is part of the public API
|
|
82
|
+
* (`src/index.js`) and an adopter importing it should not break on a minor.
|
|
83
|
+
* What it means now is "the judge bareloop pinned before item 32", which is
|
|
84
|
+
* still the right DEFAULT for an anthropic-api job and still the only tier with
|
|
85
|
+
* established injection resistance upstream (BA-20's `INJECTION_BATTERY` is
|
|
86
|
+
* haiku-4.5 only).
|
|
87
|
+
*
|
|
88
|
+
* THE ENFORCEMENT IS UNCHANGED, and still lives in three wired places because a
|
|
89
|
+
* rule with no detector is prose (F45) — only the value being compared moved
|
|
90
|
+
* from this constant to the resolved identity:
|
|
91
|
+
* 1. the identity is STORED with the set it graded — `foldJudgedArtifacts`
|
|
81
92
|
* writes it as `closeDecl.calibration.judgeModel`, inside the spec, so
|
|
82
|
-
* `jobSpecHash` covers it by construction.
|
|
93
|
+
* `jobSpecHash` covers it by construction. Change the judge and every
|
|
83
94
|
* signed judged spec hashes differently: the signature dies, and re-signing
|
|
84
95
|
* is what re-runs the calibration gate.
|
|
85
96
|
* 2. `validateCloseDecl` REQUIRES it on any stored set — a set nobody can
|
|
86
|
-
* attribute to a
|
|
87
|
-
* 3. `runJudgedFloor` refuses to grade when the stored
|
|
88
|
-
* naming both
|
|
89
|
-
* the new
|
|
90
|
-
* not on offer.
|
|
97
|
+
* attribute to a judge is a floor nobody can tell apart from a moved one.
|
|
98
|
+
* 3. `runJudgedFloor` refuses to grade when the stored identity is not the one
|
|
99
|
+
* about to grade, naming both. It does not degrade in either direction:
|
|
100
|
+
* grading on the new judge judges against a floor nobody calibrated, and
|
|
101
|
+
* the old judge is not on offer.
|
|
91
102
|
*/
|
|
92
103
|
export const JUDGE_MODEL = 'claude-haiku-4-5';
|
|
93
104
|
|
|
105
|
+
/**
|
|
106
|
+
* RESOLVE WHICH MODEL GRADES THIS JOB (PRD item 32.1).
|
|
107
|
+
*
|
|
108
|
+
* hamr, 2026-09-10: *"what i care about is that judge becomes llm agnostic and
|
|
109
|
+
* not set to one model or provider"* — and the shape he named: *"defaults to
|
|
110
|
+
* whatever llm is used and can be overriden"*.
|
|
111
|
+
*
|
|
112
|
+
* So, in precedence order:
|
|
113
|
+
* 1. `spec.judge` — an explicit, SIGNED `{provider, model}`. This is how a run
|
|
114
|
+
* pins a judge deliberately different from its worker (an independent
|
|
115
|
+
* reader, or a judge whose calibration a job wants to keep while the worker
|
|
116
|
+
* moves). It is a spec field the HUMAN signs, part of `jobSpecHash` like
|
|
117
|
+
* every other; the drafting agent cannot express it, because naming your
|
|
118
|
+
* own examiner is arbiter territory.
|
|
119
|
+
* 2. otherwise the job's OWN worker provider and model. A DeepSeek job judges
|
|
120
|
+
* on DeepSeek and needs no second account — which is the whole point: after
|
|
121
|
+
* item 31.4 a green job runs on its own key alone, and this is what lets a
|
|
122
|
+
* JUDGED one do the same.
|
|
123
|
+
*
|
|
124
|
+
* There is deliberately NO third fallback to `JUDGE_MODEL`. A caller that cannot
|
|
125
|
+
* say which model will grade has not wired a judge, and the grading path stops
|
|
126
|
+
* as a wiring gap rather than grading under an identity nobody chose — the same
|
|
127
|
+
* answer an absent `judgeLoop` already gets. A silent default here would be the
|
|
128
|
+
* exact silent-ignore class that `endpointKey` and F149 exist to prevent, except
|
|
129
|
+
* pointed at the arbiter.
|
|
130
|
+
*
|
|
131
|
+
* SELF-GRADING is licensed by measurement, not by this function: the judge
|
|
132
|
+
* renders no verdict (it locates facts; `decide()` is deterministic), and the
|
|
133
|
+
* calibration gate — 10 cases, a 10-of-10 floor, no partial credit, cases that
|
|
134
|
+
* MUST fail — is exactly the instrument that catches a judge which cannot see
|
|
135
|
+
* failures. If a worker-family judge cannot pass its own calibration, that is
|
|
136
|
+
* the finding, and the gate refuses to sign.
|
|
137
|
+
*
|
|
138
|
+
* @param {{specJudge?: any, workerProvider?: string, workerModel?: string}} o
|
|
139
|
+
* @returns {{provider: string, model: string}}
|
|
140
|
+
*/
|
|
141
|
+
export function resolveJudge({ specJudge, workerProvider, workerModel } = {}) {
|
|
142
|
+
const nonEmpty = (/** @type {any} */ v) => typeof v === 'string' && v.trim() !== '';
|
|
143
|
+
if (specJudge !== null && typeof specJudge === 'object'
|
|
144
|
+
&& nonEmpty(specJudge.provider) && nonEmpty(specJudge.model)) {
|
|
145
|
+
return { provider: specJudge.provider, model: specJudge.model };
|
|
146
|
+
}
|
|
147
|
+
if (specJudge !== undefined && specJudge !== null) {
|
|
148
|
+
// A malformed `judge` is never quietly ignored into the worker default: the
|
|
149
|
+
// spec VALIDATOR reds it (src/job.js), and reaching here with one means a
|
|
150
|
+
// caller skipped validation. Throwing beats grading under an identity the
|
|
151
|
+
// signer did not write.
|
|
152
|
+
throw new Error('[judged] spec.judge, when present, is {provider, model} with both non-empty — '
|
|
153
|
+
+ `got ${JSON.stringify(specJudge)}. A malformed judge is not a request to use the worker's.`);
|
|
154
|
+
}
|
|
155
|
+
if (!nonEmpty(workerProvider) || !nonEmpty(workerModel)) {
|
|
156
|
+
throw new Error('[judged] resolveJudge needs the job\'s worker provider and model to default the judge to them '
|
|
157
|
+
+ '(PRD item 32.1) — a caller that can name neither a judge nor a worker has not wired a judge, and there is '
|
|
158
|
+
+ 'no library-pinned fall-back any more.');
|
|
159
|
+
}
|
|
160
|
+
return { provider: /** @type {string} */ (workerProvider), model: /** @type {string} */ (workerModel) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `resolveJudge` needs a worker MODEL already resolved, and an authoring
|
|
165
|
+
* pipeline has to compute one identity at TWO different moments — once at
|
|
166
|
+
* compose time, to STAMP `closeDecl.calibration.judgeModel` into the spec it
|
|
167
|
+
* is about to sign, and again at the calibration gate that grades against
|
|
168
|
+
* that stamp — from a value that is not yet a validated spec (the operator's
|
|
169
|
+
* draft) the first time and is one (the assembled spec) the second. Two
|
|
170
|
+
* hand-spelled compositions of `resolveWorkerModel` + `resolveJudge` is
|
|
171
|
+
* exactly the kind of pair that drifts (PRD F45's "a rule with no detector is
|
|
172
|
+
* prose" applies to a rule spelled TWICE just as much as to one spelled
|
|
173
|
+
* nowhere) — this is the one spelling both calls make, and it mirrors the
|
|
174
|
+
* SAME composition `scripts/run-u.mjs` applies at launch time: the named
|
|
175
|
+
* provider's own `tiers.sonnet` defaults an absent model, and a signed
|
|
176
|
+
* `judge` override wins over the worker default.
|
|
177
|
+
*
|
|
178
|
+
* `source` is read for `provider`/`model`/`judge` only, so a draft and the
|
|
179
|
+
* spec `assembleSpec` folds it into resolve identically — `assembleSpec`
|
|
180
|
+
* spreads the draft into the spec untouched on those three fields — and any
|
|
181
|
+
* draft that goes on to validate and sign therefore stamps and grades under
|
|
182
|
+
* the exact same identity `scripts/run-u.mjs` will resolve at run time.
|
|
183
|
+
*
|
|
184
|
+
* `source.provider` absent, or unresolvable, or `source.judge` malformed all
|
|
185
|
+
* THROW here exactly as `resolveProvider`/`resolveJudge` already do on their
|
|
186
|
+
* own — a caller composing this from a pre-validation draft is expected to
|
|
187
|
+
* catch that and use a placeholder, because a draft that cannot resolve here
|
|
188
|
+
* is a draft `validateJob` reds a few lines later regardless, before anything
|
|
189
|
+
* gets signed under the placeholder.
|
|
190
|
+
*
|
|
191
|
+
* `resolveWorkerModelFn` is taken as a PARAMETER rather than imported — this
|
|
192
|
+
* module cannot import `./job.js` directly: `job.js` imports
|
|
193
|
+
* `./declaredclose.js`, which imports `resolveJudge` from THIS module, and a
|
|
194
|
+
* `judged.js` → `job.js` edge would close that into a cycle. `resolveProvider`
|
|
195
|
+
* has no such conflict (`./providers.js` imports nothing local) and is
|
|
196
|
+
* imported directly.
|
|
197
|
+
*
|
|
198
|
+
* @param {{provider?: string, model?: string, judge?: any}} source draft or spec
|
|
199
|
+
* @param {string} fallbackProviderName used only when `source.provider` is absent
|
|
200
|
+
* @param {(o: {specModel?: string, flagModel?: string, defaultModel: string}) => {model: string, source: string}} resolveWorkerModelFn `resolveWorkerModel` from `./job.js`
|
|
201
|
+
* @returns {{provider: string, model: string}}
|
|
202
|
+
*/
|
|
203
|
+
export function resolveJobJudge(source, fallbackProviderName, resolveWorkerModelFn) {
|
|
204
|
+
const providerName = source?.provider ?? fallbackProviderName;
|
|
205
|
+
const providerEntry = resolveProvider(providerName);
|
|
206
|
+
const { model } = resolveWorkerModelFn({
|
|
207
|
+
specModel: source?.model, flagModel: undefined, defaultModel: providerEntry.tiers.sonnet,
|
|
208
|
+
});
|
|
209
|
+
return resolveJudge({ specJudge: source?.judge, workerProvider: providerName, workerModel: model });
|
|
210
|
+
}
|
|
211
|
+
|
|
94
212
|
/**
|
|
95
213
|
* The locate call's token ceiling. The POC's two real artifacts came back well
|
|
96
214
|
* inside 2000; this doubles that for headroom on a longer artifact, and the
|
|
@@ -565,6 +683,43 @@ export function validateFacts(facts) {
|
|
|
565
683
|
|
|
566
684
|
// ── THE LOCATE PROMPT ───────────────────────────────────────────────────────
|
|
567
685
|
|
|
686
|
+
/**
|
|
687
|
+
* THE ONE SHAPE INVENTORY (PRD 30.8), stated ONCE and never twice. It drives
|
|
688
|
+
* BOTH the LOCATE prompt's prose ("declared with ...") AND the completeness
|
|
689
|
+
* detector's regex — the same standing rule the secret-shape scrubber
|
|
690
|
+
* follows (one inventory drives detection, never two spellings that can
|
|
691
|
+
* drift). A shape added here rides both sides in the same commit; a shape
|
|
692
|
+
* added to only a hand-typed prompt string or only a hand-typed regex is
|
|
693
|
+
* exactly the blind-instrument class this repo keeps paying for.
|
|
694
|
+
*
|
|
695
|
+
* Two BASE shapes, each carrying its own independent `export`/`async`
|
|
696
|
+
* modifiers (the modifiers sit in different POSITIONS in the two shapes —
|
|
697
|
+
* `async function foo(` vs `const foo = async () =>` — so they cannot be
|
|
698
|
+
* factored into one shared prefix):
|
|
699
|
+
* - `function name(...)` — optionally `export`, optionally `async`,
|
|
700
|
+
* in either order before `function`.
|
|
701
|
+
* - `const name = (...) =>` — optionally `export` before `const`,
|
|
702
|
+
* optionally `async` after `=`.
|
|
703
|
+
*
|
|
704
|
+
* Restricted to column-0 lines only, in both the prompt's prose and the
|
|
705
|
+
* detector: a nested function or a function expression returned from one is
|
|
706
|
+
* indented, and excluding it is the SAME cure PROMPT_HEAD already states
|
|
707
|
+
* (the POC's nested-function cure).
|
|
708
|
+
* @type {ReadonlyArray<{prose: string, match: (line: string) => string|null}>}
|
|
709
|
+
*/
|
|
710
|
+
const FN_SHAPES = Object.freeze([
|
|
711
|
+
{
|
|
712
|
+
prose: '`function name(...)` (optionally `async`, optionally `export`, in either order)',
|
|
713
|
+
match: (/** @type {string} */ line) =>
|
|
714
|
+
line.match(/^(?:(?:export|async)\s+){0,2}function\s+([A-Za-z_$][\w$]*)\s*\(/)?.[1] ?? null,
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
prose: '`const name = (...) =>` (optionally `export` before `const`, optionally `async` after `=`)',
|
|
718
|
+
match: (/** @type {string} */ line) =>
|
|
719
|
+
line.match(/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/)?.[1] ?? null,
|
|
720
|
+
},
|
|
721
|
+
]);
|
|
722
|
+
|
|
568
723
|
/** The spine every card shares. Both POC cures live here rather than on a rule,
|
|
569
724
|
* because both are properties of ENUMERATING functions and not of any one
|
|
570
725
|
* question about them: the nested-function clause (the POC counted a returned
|
|
@@ -573,8 +728,7 @@ export function validateFacts(facts) {
|
|
|
573
728
|
const PROMPT_HEAD =
|
|
574
729
|
'You LOCATE FACTS in a source file. You never decide whether anything passes, and you never judge — '
|
|
575
730
|
+ 'another system does that.\n'
|
|
576
|
-
+
|
|
577
|
-
+ 'or `export function name(...)`), report:\n'
|
|
731
|
+
+ `For EVERY top-level function in the FILE (declared with ${FN_SHAPES.map((s) => s.prose).join(', or ')}), report:\n`
|
|
578
732
|
+ 'Count ONLY functions declared at the top level of the module — never a function nested inside '
|
|
579
733
|
+ 'another function, and never a function expression returned from one.\n'
|
|
580
734
|
+ ' "name": the function name\n'
|
|
@@ -596,6 +750,43 @@ export function locatePrompt(card) {
|
|
|
596
750
|
return `${PROMPT_HEAD}${asks}\n${PROMPT_TAIL}`;
|
|
597
751
|
}
|
|
598
752
|
|
|
753
|
+
/**
|
|
754
|
+
* THE COMPLETENESS MIRROR of `PROMPT_HEAD`'s own contract (PRD 30.8): which
|
|
755
|
+
* top-level function declarations the artifact ACTUALLY holds, read off the
|
|
756
|
+
* SAME `FN_SHAPES` inventory the prompt's prose is built from, restricted to
|
|
757
|
+
* column-0 lines.
|
|
758
|
+
*
|
|
759
|
+
* This is a HEURISTIC, never a parser, and it is used for exactly one thing:
|
|
760
|
+
* telling `decide()` when LOCATE's `functions` list is short of what the file
|
|
761
|
+
* holds. It never grades a rule and it never adds a fact `decide()` acts on
|
|
762
|
+
* beyond "this name exists at the top level" — the missing function's rule
|
|
763
|
+
* violations, if any, stay unknown, which is exactly why the miss is UNSURE
|
|
764
|
+
* and not a manufactured red on a specific rule.
|
|
765
|
+
*
|
|
766
|
+
* TWO DIRECTIONS, both handled but not symmetrically by design:
|
|
767
|
+
* - a shape THIS inventory does not enumerate is a FALSE NEGATIVE — the
|
|
768
|
+
* check stays silent about that function, same as it stays silent with
|
|
769
|
+
* no `artifactText` at all. Widening `FN_SHAPES` closes the hole; nothing
|
|
770
|
+
* here manufactures a claim about a shape it was never told to look for.
|
|
771
|
+
* - a column-0 line that only LOOKS like a declaration (e.g. inside a
|
|
772
|
+
* template literal) is a FALSE POSITIVE and reds a real judged close —
|
|
773
|
+
* that is the SAFE side of the fail-safe tiebreak (unsure is red), and it
|
|
774
|
+
* is never silently swallowed the way a false negative would be.
|
|
775
|
+
* @param {string} artifactText @returns {string[]}
|
|
776
|
+
*/
|
|
777
|
+
function topLevelFunctionNames(artifactText) {
|
|
778
|
+
/** @type {string[]} */
|
|
779
|
+
const names = [];
|
|
780
|
+
for (const line of artifactText.split('\n')) {
|
|
781
|
+
if (/^\s/.test(line)) continue; // indented: nested, or a returned function expression — never top level
|
|
782
|
+
for (const shape of FN_SHAPES) {
|
|
783
|
+
const name = shape.match(line);
|
|
784
|
+
if (name) { names.push(name); break; }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return names;
|
|
788
|
+
}
|
|
789
|
+
|
|
599
790
|
/** the default toolless seam — one spelling of how this repo drives a judge.
|
|
600
791
|
* A `Loop`, NOT `provider.generate()`: the POC measured that a bare provider
|
|
601
792
|
* result carries no `costUsd` at all, and under F6 an unpriced call is a red,
|
|
@@ -628,12 +819,23 @@ export const defaultJudgeLoop = ({ provider, system }) => {
|
|
|
628
819
|
* On every route out, red or clean, `onCost` fires exactly once with the honest
|
|
629
820
|
* read: a paid call that leaves no meter record is F12 wearing a judge's coat.
|
|
630
821
|
* @param {{artifactText: string, card: any, loopFactory: (o: {provider?: any, system: string}) => any,
|
|
631
|
-
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void
|
|
822
|
+
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void,
|
|
823
|
+
* callBounds?: {timeoutMs?: number, deadlineMs?: number}}} o
|
|
632
824
|
* @returns {Promise<{ok: boolean, facts: any|null, red: {axis: string, detail: string}|null,
|
|
633
825
|
* costUsd: number|null, unpricedRounds: number, truncated: boolean, parseError: boolean,
|
|
634
826
|
* raw: ReturnType<typeof scrubRaw>}>}
|
|
635
827
|
*/
|
|
636
|
-
export async function runLocate({
|
|
828
|
+
export async function runLocate({
|
|
829
|
+
artifactText, card, loopFactory, maxTokens = JUDGE_MAX_TOKENS, attempt = 1, onCost = () => {},
|
|
830
|
+
// F152/PRD 30.4 — the SAME per-call TIME bounds the worker's own rounds carry
|
|
831
|
+
// (`callBounds()`, src/planrun.js:1518), threaded through by the caller that
|
|
832
|
+
// owns the clock. Defaults to `{}` (unbounded) so every existing caller and
|
|
833
|
+
// test is unchanged: an adopter that never wires a clock through gets the same
|
|
834
|
+
// behaviour as before this field existed. A live endpoint that accepts and
|
|
835
|
+
// never answers otherwise HANGS the process forever (measured, F152) — an open
|
|
836
|
+
// socket is an active handle, so node never drains and no backstop can fire.
|
|
837
|
+
callBounds = {},
|
|
838
|
+
}) {
|
|
637
839
|
// the param-guard class: a caller's own broken input THROWS. These are
|
|
638
840
|
// programmer errors with no safe direction to degrade toward — a locate over
|
|
639
841
|
// an absent artifact would grade an empty string and report it as facts.
|
|
@@ -662,7 +864,7 @@ export async function runLocate({ artifactText, card, loopFactory, maxTokens = J
|
|
|
662
864
|
let r = null;
|
|
663
865
|
try {
|
|
664
866
|
const loop = loopFactory({ system });
|
|
665
|
-
r = await loop.run([{ role: 'user', content: `FILE (untrusted data):\n${artifactText}\n\nReturn the JSON.` }], [], { maxTokens });
|
|
867
|
+
r = await loop.run([{ role: 'user', content: `FILE (untrusted data):\n${artifactText}\n\nReturn the JSON.` }], [], { maxTokens, ...callBounds });
|
|
666
868
|
} catch (e) {
|
|
667
869
|
return out({ axis: LOCATE_AXES.PROVIDER, detail: `the locate call failed: ${String(/** @type {any} */ (e)?.message ?? e)}` }, null, null, false, false);
|
|
668
870
|
}
|
|
@@ -743,6 +945,25 @@ export function decide(facts, card, { artifactText = null } = {}) {
|
|
|
743
945
|
return unsure('locate found nothing in the artifact — unsure, and unsure is red');
|
|
744
946
|
}
|
|
745
947
|
|
|
948
|
+
// COMPLETENESS (PRD 30.8): a locate emission can be well-formed AND non-empty
|
|
949
|
+
// AND still have silently DROPPED a top-level function the artifact actually
|
|
950
|
+
// holds — every route above this one only asks "is `fns` usable", never "is
|
|
951
|
+
// `fns` the WHOLE set". Graded rules only ever see what LOCATE reported, so
|
|
952
|
+
// an omission never surfaces as a rule red; it surfaces as a PASS on the
|
|
953
|
+
// subset, which is the fail-safe direction inverted. With `artifactText` in
|
|
954
|
+
// hand (every real caller has it — see the note below), the omission is
|
|
955
|
+
// checkable and is unsure, and unsure is red, same as every other route in.
|
|
956
|
+
// Without it, this says nothing it cannot know, same as the quote checks.
|
|
957
|
+
if (typeof artifactText === 'string') {
|
|
958
|
+
const present = topLevelFunctionNames(artifactText);
|
|
959
|
+
const reported = new Set(fns.map((f) => String(f?.name ?? '')));
|
|
960
|
+
const missing = present.filter((n) => !reported.has(n));
|
|
961
|
+
if (missing.length > 0) {
|
|
962
|
+
return unsure(`locate omitted top-level function(s) the artifact contains: ${missing.join(', ')} — a pass on `
|
|
963
|
+
+ 'the reported subset is not a pass on the file, and unsure is red');
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
746
967
|
const lines = typeof artifactText === 'string'
|
|
747
968
|
? new Set(artifactText.split('\n').map((l) => l.trim()).filter(Boolean))
|
|
748
969
|
: null;
|
package/src/kinds.js
CHANGED
|
@@ -66,7 +66,7 @@ import { tmpdir } from 'node:os';
|
|
|
66
66
|
import { join, sep } from 'node:path';
|
|
67
67
|
import { globToPrefix, isNonEmptyString } from './validate.js';
|
|
68
68
|
import { CLOSE_ENV_DENY } from './ralph.js';
|
|
69
|
-
import { runLocate, decide, validateCard, LOCATE_AXES, LOCATE_LABEL
|
|
69
|
+
import { runLocate, decide, validateCard, LOCATE_AXES, LOCATE_LABEL } from './judged.js';
|
|
70
70
|
|
|
71
71
|
/** the close's two judgment exits — a stage's verdict, in the shape the
|
|
72
72
|
* hand-written closes already spoke (exit code is truth) */
|
|
@@ -173,6 +173,46 @@ export const MAX_JUDGED_PATHS = 8;
|
|
|
173
173
|
* decision on it, and five literals is how they come to disagree. */
|
|
174
174
|
export const JUDGED_FLOOR_KIND = 'judged-floor';
|
|
175
175
|
export const SEED_EXEMPT_KINDS = Object.freeze([JUDGED_FLOOR_KIND, 'human-confirms']);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The judged stages of a close declaration, and whether it has any — the ONE
|
|
179
|
+
* spelling of "does this close ask a model to render anything?".
|
|
180
|
+
*
|
|
181
|
+
* The comment above `JUDGED_FLOOR_KIND` says five literals is how the readers
|
|
182
|
+
* come to disagree, and by PRD item 31.4 there were SIX open-coded
|
|
183
|
+
* `stages.filter(s => s.kind === JUDGED_FLOOR_KIND)` across the tree
|
|
184
|
+
* (`src/authorjob.js` twice, `src/cardauthor.js`, `src/declaredclose.js`,
|
|
185
|
+
* `scripts/run-author.mjs`, `scripts/author-readout.mjs`). 31.4 needed a
|
|
186
|
+
* seventh, in the runner, to decide whether a judge KEY is demanded — and a
|
|
187
|
+
* seventh spelling deciding a REFUSAL is the drift `scripts/run-u.mjs`'s own
|
|
188
|
+
* judge-provider comment warns about ("a second reading of the declaration
|
|
189
|
+
* that can drift from the one the runner actually executes"). So there is one
|
|
190
|
+
* reading now, and every caller shares it.
|
|
191
|
+
*
|
|
192
|
+
* Defensive on shape ON PURPOSE: callers pass raw specs read off disk and
|
|
193
|
+
* half-built drafts, not validated declarations. An absent/!object `closeDecl`
|
|
194
|
+
* or `stages` is "no judged stages", never a throw — the close VALIDATOR is
|
|
195
|
+
* what rejects a malformed declaration, and this predicate answering a
|
|
196
|
+
* question about one is not the place to re-litigate it.
|
|
197
|
+
* @param {any} closeDecl a close declaration (or anything, including undefined)
|
|
198
|
+
* @returns {any[]} the judged-floor stages, in declaration order
|
|
199
|
+
*/
|
|
200
|
+
export function judgedStages(closeDecl) {
|
|
201
|
+
const stages = closeDecl && typeof closeDecl === 'object' ? closeDecl.stages : undefined;
|
|
202
|
+
if (!Array.isArray(stages)) return [];
|
|
203
|
+
return stages.filter((s) => s !== null && typeof s === 'object' && s.kind === JUDGED_FLOOR_KIND);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Does this close declaration judge — i.e. will a judge model be CALLED?
|
|
208
|
+
* The question `scripts/run-u.mjs` asks before demanding a judge key (PRD item
|
|
209
|
+
* 31.4): a job that never judges must run on its own provider's key alone.
|
|
210
|
+
* @param {any} closeDecl
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
export function closeJudges(closeDecl) {
|
|
214
|
+
return judgedStages(closeDecl).length > 0;
|
|
215
|
+
}
|
|
176
216
|
/** the one kind a PERSON renders — named once, because the runner keys three
|
|
177
217
|
* decisions on it (is there a door to answer, which stage paused, whose ruling
|
|
178
218
|
* this is) and three literals is how they come to disagree */
|
|
@@ -258,8 +298,10 @@ export function mechanicalStages(stages) {
|
|
|
258
298
|
* gapCap?: number, maxBuffer?: number, seedTrees?: SeedTrees,
|
|
259
299
|
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
260
300
|
* judgeLoop?: (o: {system: string}) => any,
|
|
301
|
+
* judgeModel?: string,
|
|
261
302
|
* onJudgeCost?: (c: {stage: string|null, kind: string|null, path: string, attempt: number,
|
|
262
303
|
* label: string, model: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
304
|
+
* callBounds?: () => {timeoutMs?: number, deadlineMs?: number},
|
|
263
305
|
* onStage?: (row: {stage: string|null, kind: string|null, verdict: string, durationMs: number}) => void}} Ctx
|
|
264
306
|
* The arbiter's half of a stage run. Every field here is operator/runner
|
|
265
307
|
* territory: a declaration parameterises kinds, never the contracts.
|
|
@@ -267,14 +309,25 @@ export function mechanicalStages(stages) {
|
|
|
267
309
|
* runner — never authored, never defaulted, and absent on every ordinary run.
|
|
268
310
|
* `judgeLoop` is the PAID seam the judged stage runs its locate call through, and
|
|
269
311
|
* it is operator territory for the same reason the worker's provider is: this
|
|
270
|
-
* module owns no provider and never picks one
|
|
271
|
-
*
|
|
272
|
-
* model — the stage stops as a wiring gap, the same answer `runPlan` gives a
|
|
312
|
+
* module owns no provider and never picks one. Absent is not a fall-back to some
|
|
313
|
+
* other model — the stage stops as a wiring gap, the same answer `runPlan` gives a
|
|
273
314
|
* native job with no native factory.
|
|
315
|
+
* `judgeModel` is the seam's other half (PRD item 32.1): WHICH model that loop
|
|
316
|
+
* drives, resolved by the runner (`resolveJudge`, src/judged.js — the spec's signed
|
|
317
|
+
* `judge` override, else the job's own worker model) rather than pinned in this
|
|
318
|
+
* library. It is required whenever `judgeLoop` is, and for the same reason: a
|
|
319
|
+
* verdict whose grader cannot be named is a floor nobody can attribute, and the
|
|
320
|
+
* signed calibration record has nothing to compare against. The two travel
|
|
321
|
+
* together or the stage stops.
|
|
274
322
|
* `onJudgeCost` is the METER, and it is the only reason a judged stage can be
|
|
275
323
|
* funded honestly: this executor cannot reach the run's ledger, so every locate
|
|
276
324
|
* call's cost is reported OUT through it, once per call, on every route including
|
|
277
325
|
* the red ones — a paid call that leaves no meter record is F12 in a judge's coat.
|
|
326
|
+
* `callBounds` is F152/PRD 30.4's per-call TIME bound, read fresh before every
|
|
327
|
+
* locate attempt — the same derivation the run's own worker rounds carry
|
|
328
|
+
* (`callBounds()`, src/planrun.js). Absent is the honest unbounded default:
|
|
329
|
+
* an adopter or test that wires no clock through behaves exactly as before
|
|
330
|
+
* this field existed.
|
|
278
331
|
* `onStage` is a REPORTING seam and nothing else: the seed read spawns a real
|
|
279
332
|
* toolchain per stage (a suite is the slow one) and a whole read is one opaque
|
|
280
333
|
* await, so the shell gets told as each stage lands. It decides nothing, it is
|
|
@@ -1730,16 +1783,34 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1730
1783
|
// model on it (`validateCloseDecl`), so the runtime question is only ever WHICH
|
|
1731
1784
|
// model — and a stage descriptor handed straight to `runStage` by an adopter or a
|
|
1732
1785
|
// test is not a signed spec making a claim about a tier.
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1786
|
+
//
|
|
1787
|
+
// PRD item 32.1: WHICH model grades is no longer this module's constant — it is
|
|
1788
|
+
// `ctx.judgeModel`, the identity the run resolved and wired (`resolveJudge`,
|
|
1789
|
+
// src/judged.js: the spec's signed `judge` override, else the job's own worker
|
|
1790
|
+
// model). The IDENTITY is checked before the seam and before the stamp, because
|
|
1791
|
+
// an unwired identity makes both of the messages below unwritable: neither
|
|
1792
|
+
// "would grade with X" nor "the judge is X" can be said by a stage that cannot
|
|
1793
|
+
// name X. There is deliberately NO fall-back to `JUDGE_MODEL` here — a run that
|
|
1794
|
+
// cannot say which model grades has not wired a judge, and grading under an
|
|
1795
|
+
// identity nobody chose is exactly the silent-ignore class this repo refuses
|
|
1796
|
+
// everywhere else.
|
|
1797
|
+
if (!isNonEmptyString(ctx.judgeModel)) {
|
|
1798
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" is a judged stage and this run wired no judge IDENTITY `
|
|
1799
|
+
+ '(ctx.judgeModel) — an adopter wiring gap, never a silent fall-back to a library pin. A verdict whose grader '
|
|
1800
|
+
+ 'cannot be named is a floor nobody can attribute, and the calibration record has nothing to compare against',
|
|
1801
|
+
{}, STOP_FAULTS.FAILED);
|
|
1802
|
+
}
|
|
1803
|
+
const judgeModel = /** @type {string} */ (ctx.judgeModel);
|
|
1804
|
+
if (isNonEmptyString(stage.calibrationJudgeModel) && stage.calibrationJudgeModel !== judgeModel) {
|
|
1805
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" would grade with ${judgeModel}, and the calibration `
|
|
1806
|
+
+ `set signed with this close was graded by ${stage.calibrationJudgeModel} — the judge moved under the `
|
|
1736
1807
|
+ 'signature. A floor is worth exactly the judge that certified it, so this stops rather than grading against a '
|
|
1737
|
-
+ 'floor nobody calibrated: re-sign the spec with the new
|
|
1738
|
-
{ signedJudgeModel: stage.calibrationJudgeModel, judgeModel
|
|
1808
|
+
+ 'floor nobody calibrated: re-sign the spec with the new judge and re-run the calibration gate (recalibrate)',
|
|
1809
|
+
{ signedJudgeModel: stage.calibrationJudgeModel, judgeModel }, STOP_FAULTS.FAILED);
|
|
1739
1810
|
}
|
|
1740
1811
|
if (typeof ctx.judgeLoop !== 'function') {
|
|
1741
1812
|
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" is a judged stage and this run wired no judge seam `
|
|
1742
|
-
+ `(ctx.judgeLoop) — an adopter wiring gap, never a silent fall-back: the judge is ${
|
|
1813
|
+
+ `(ctx.judgeLoop) — an adopter wiring gap, never a silent fall-back: the judge is ${judgeModel} and a `
|
|
1743
1814
|
+ 'stage that quietly graded on whatever model was lying around would be a floor nobody calibrated',
|
|
1744
1815
|
{}, STOP_FAULTS.FAILED);
|
|
1745
1816
|
}
|
|
@@ -1781,11 +1852,17 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1781
1852
|
card: p.card,
|
|
1782
1853
|
loopFactory,
|
|
1783
1854
|
attempt,
|
|
1855
|
+
// F152/PRD 30.4 — read fresh per attempt, the same way `planrun.js`'s own
|
|
1856
|
+
// worker rounds re-read `callBounds()` at every call site rather than
|
|
1857
|
+
// once: the remaining time shrinks between attempts. Absent (no clock
|
|
1858
|
+
// wired through) is `{}`, the honest unbounded default `runLocate` itself
|
|
1859
|
+
// already carries.
|
|
1860
|
+
callBounds: typeof ctx.callBounds === 'function' ? ctx.callBounds() : {},
|
|
1784
1861
|
onCost: (c) => {
|
|
1785
1862
|
if (typeof ctx.onJudgeCost !== 'function') return;
|
|
1786
1863
|
ctx.onJudgeCost({
|
|
1787
1864
|
stage: stage?.name ?? null, kind: stage?.kind ?? null, path: rel, attempt,
|
|
1788
|
-
label: LOCATE_LABEL, model:
|
|
1865
|
+
label: LOCATE_LABEL, model: judgeModel, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
1789
1866
|
});
|
|
1790
1867
|
},
|
|
1791
1868
|
});
|
|
@@ -1836,7 +1913,7 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1836
1913
|
exitCode: red ? EXIT_RED : EXIT_GREEN,
|
|
1837
1914
|
gapLines: gap.render(),
|
|
1838
1915
|
detail: {
|
|
1839
|
-
model:
|
|
1916
|
+
model: judgeModel,
|
|
1840
1917
|
card: p.card.items.map((/** @type {any} */ it) => it.rule),
|
|
1841
1918
|
paths,
|
|
1842
1919
|
perPath,
|