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
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// THE TWO SIGNED ARTIFACTS — softgreen module 4 (design record
|
|
2
|
+
// docs/02-features/2026-08-17-softgreen-review-door-design.md §4.3 and §4.4,
|
|
3
|
+
// plus the 2026-08-18 threshold addendum).
|
|
4
|
+
//
|
|
5
|
+
// Module 1 built the judged floor's core, module 2 wired the stage into a close,
|
|
6
|
+
// module 3 opened the class and asked the two extra questions. Nothing compiled
|
|
7
|
+
// their answers. This module does exactly that and nothing more:
|
|
8
|
+
//
|
|
9
|
+
// Q6 → THE RUBRIC CARD (§4.3) — the person's own pass/fail lines, as
|
|
10
|
+
// ENUMERATED items over the arbiter's own rulebook.
|
|
11
|
+
// Q7 → THE CALIBRATION SET (§4.4) — TEN cases, frozen, that the whole pipe
|
|
12
|
+
// must grade before the close is signable.
|
|
13
|
+
//
|
|
14
|
+
// ── THE D5 PATH, AND WHY IT IS THIS ONE ─────────────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// Both artifacts are SHOWN AND FIXED: an LLM proposes the extractable rewrite of
|
|
17
|
+
// what the person said, the SIGNER fixes it, and THE FIXED VERSION IS WHAT IS
|
|
18
|
+
// STORED — enumerated inside the spec hash, so a card line or a case is a re-sign
|
|
19
|
+
// exactly like every other spec edit. Asking the person to write the rubric
|
|
20
|
+
// themselves is the SWE tax this product refuses; letting the model's draft ride
|
|
21
|
+
// unsigned is the arbiter authored by a model. The signer in the middle is the
|
|
22
|
+
// whole design.
|
|
23
|
+
//
|
|
24
|
+
// ── WHAT THIS MODULE DOES NOT DO ────────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// * IT DOES NOT GRADE. The calibration GATE — running LOCATE and `decide()`
|
|
27
|
+
// over the ten and refusing a signature on anything short of 10/10 with
|
|
28
|
+
// itemized reds, alongside the upstream INJECTION_BATTERY — is module 5. What
|
|
29
|
+
// is built here is what that gate reads.
|
|
30
|
+
// * IT STORES NO INJECTION CASES. The battery is upstream's and it is module
|
|
31
|
+
// 5's to run; a copy of it stored here would be a second inventory of
|
|
32
|
+
// somebody else's evidence.
|
|
33
|
+
// * IT PICKS NO NUMBER. `CALIBRATION_SIZE` is hamr's own ruling, verbatim
|
|
34
|
+
// (*"go build 10, we could double later"*). It is operator territory and a
|
|
35
|
+
// size change is a spec-level threshold change — the standing
|
|
36
|
+
// no-agent-threshold-picking rule binds here exactly as everywhere else.
|
|
37
|
+
// * IT ADMITS NO DOC-GENRE ARTIFACT. A case is code-genre text, because the
|
|
38
|
+
// catalogue is code-genre only.
|
|
39
|
+
//
|
|
40
|
+
// ── THE CASE SHAPE, AND WHY EACH FIELD EXISTS ───────────────────────────────
|
|
41
|
+
//
|
|
42
|
+
// { id, artifact, expect: { verdict: "pass"|"red", reds: [{ rule, fn }] } }
|
|
43
|
+
//
|
|
44
|
+
// `artifact` REAL TEXT — a file excerpt from the patient or a realistic
|
|
45
|
+
// snippet the signer edits. It is what LOCATE will be pointed at,
|
|
46
|
+
// so it is the artifact and never a description of one.
|
|
47
|
+
// `verdict` what `decide()` must render. Two values, both reachable.
|
|
48
|
+
// `reds` the ITEMIZED expectation, and the floor is itemized reds rather
|
|
49
|
+
// than verdict-match: a pipe that reds the right case for the wrong
|
|
50
|
+
// reason has not been calibrated, it has been lucky. `rule` selects
|
|
51
|
+
// from the CARD's own lines (a case cannot expect a red from a line
|
|
52
|
+
// the card never named — §4.3's ceiling, made mechanical), and `fn`
|
|
53
|
+
// names the function the red lands on, which is the address
|
|
54
|
+
// `decide()` itself reports.
|
|
55
|
+
//
|
|
56
|
+
// WHAT IS DELIBERATELY NOT STORED: the red's `why` and its `quote`. Both are the
|
|
57
|
+
// judge's own prose about a specific emission, and a signer cannot predict either
|
|
58
|
+
// — pinning them would fail an honest pipe on wording. `expectedOf` is the one
|
|
59
|
+
// spelling of that reduction, so the shape the signer stores and the shape the
|
|
60
|
+
// pipe produces are one shape read twice (module 5 compares them mechanically).
|
|
61
|
+
|
|
62
|
+
import {
|
|
63
|
+
JUDGE_RULE_IDS, JUDGE_RULES, CALIBRATION_SIZE, CASE_VERDICTS, validateJudgedArtifacts, JUDGE_MODEL,
|
|
64
|
+
} from './judged.js';
|
|
65
|
+
import { askStructured, makeCostBook } from './authorflow.js';
|
|
66
|
+
import { JUDGED_FLOOR_KIND } from './kinds.js';
|
|
67
|
+
import { isObj, redactSecrets } from './validate.js';
|
|
68
|
+
|
|
69
|
+
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
70
|
+
|
|
71
|
+
// ── THE NUMBERS, ALL OPERATOR-SET ───────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// `CALIBRATION_SIZE` and `CASE_VERDICTS` are imported from `src/judged.js`, never
|
|
74
|
+
// re-spelled: what a legal set IS belongs to the arbiter beside the rulebook, and
|
|
75
|
+
// a second copy of the size here is a second threshold to keep in step.
|
|
76
|
+
|
|
77
|
+
/** the output channel for the compile. It takes no action; it carries the answer. */
|
|
78
|
+
export const PROPOSAL_TOOL_NAME = 'propose_rubric';
|
|
79
|
+
/** what the tool returns. The loop stops on delivery, so the model never reads it. */
|
|
80
|
+
export const PROPOSAL_ACK = 'proposal received';
|
|
81
|
+
/** the label the compile call is metered and persisted under — one spelling */
|
|
82
|
+
export const PROPOSAL_LABEL = 'judged-compile';
|
|
83
|
+
/** a malformed EMISSION is a transport fault, bounded on its own axis — the same
|
|
84
|
+
* ceiling `authorClose` declares for the declaration channel */
|
|
85
|
+
export const MAX_PROPOSAL_RETRIES = 2;
|
|
86
|
+
|
|
87
|
+
// ── 1. THE SCHEMA — the closed sets, handed over ────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The proposal's shape, and therefore the whole shape the compiler can express.
|
|
91
|
+
*
|
|
92
|
+
* `rule` is an ENUM off the judge's own rulebook in BOTH places it appears — the
|
|
93
|
+
* card's lines and a case's expected reds — because wherever only a closed set
|
|
94
|
+
* can satisfy a field, the illegal value is made INEXPRESSIBLE rather than
|
|
95
|
+
* rejected after the fact (the `check-passes(name)` model). The SIZE is pinned by
|
|
96
|
+
* `minItems`/`maxItems` from the one constant, so nine and eleven are both
|
|
97
|
+
* unsayable rather than argued about afterwards.
|
|
98
|
+
*
|
|
99
|
+
* `artifact` and `text` are the only free strings, and neither decides anything:
|
|
100
|
+
* one is the code the judge will read, the other is the signer's own words.
|
|
101
|
+
*/
|
|
102
|
+
export function proposalSchema() {
|
|
103
|
+
const ruleEnum = { enum: [...JUDGE_RULE_IDS], description: 'which owned rule this line asserts' };
|
|
104
|
+
return {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
card: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
description: 'the rubric, as enumerated items: one line per thing the person actually looks for, in the '
|
|
110
|
+
+ 'order they look for them.',
|
|
111
|
+
properties: {
|
|
112
|
+
items: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
minItems: 1,
|
|
115
|
+
items: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
rule: ruleEnum,
|
|
119
|
+
text: { type: 'string', minLength: 1, description: 'what this line means, in the person\'s own words' },
|
|
120
|
+
},
|
|
121
|
+
required: ['rule', 'text'],
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ['items'],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
cases: {
|
|
130
|
+
type: 'array',
|
|
131
|
+
minItems: CALIBRATION_SIZE,
|
|
132
|
+
maxItems: CALIBRATION_SIZE,
|
|
133
|
+
description: `exactly ${CALIBRATION_SIZE} calibration cases, and at least one of each verdict — a set that `
|
|
134
|
+
+ 'cannot fail one way proves nothing',
|
|
135
|
+
items: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
id: { type: 'string', minLength: 1, description: 'a unique lowercase-hyphenated slug' },
|
|
139
|
+
artifact: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
minLength: 1,
|
|
142
|
+
description: 'the REAL source text this case is about — what the judge will actually read, never a '
|
|
143
|
+
+ 'description of it',
|
|
144
|
+
},
|
|
145
|
+
expect: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
verdict: { enum: [...CASE_VERDICTS] },
|
|
149
|
+
reds: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
description: 'the itemized expectation: empty for a pass, and one entry per card line the artifact '
|
|
152
|
+
+ 'violates for a red',
|
|
153
|
+
items: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
rule: ruleEnum,
|
|
157
|
+
fn: { type: 'string', minLength: 1, description: 'the function the red lands on' },
|
|
158
|
+
},
|
|
159
|
+
required: ['rule', 'fn'],
|
|
160
|
+
additionalProperties: false,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
required: ['verdict', 'reds'],
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
required: ['id', 'artifact', 'expect'],
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
required: ['card', 'cases'],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The output channel. It records and acknowledges; it takes no action, which is
|
|
180
|
+
* why handing it to a toolless compile call does not reopen D3.
|
|
181
|
+
* @param {{calls: any[]}} box
|
|
182
|
+
*/
|
|
183
|
+
export function proposalTool(box) {
|
|
184
|
+
return {
|
|
185
|
+
name: PROPOSAL_TOOL_NAME,
|
|
186
|
+
description: `Deliver the rubric card and the ${CALIBRATION_SIZE} calibration cases. Call this exactly once.`,
|
|
187
|
+
parameters: proposalSchema(),
|
|
188
|
+
execute: async (/** @type {any} */ args) => { box.calls.push(args); return PROPOSAL_ACK; },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── 2. THE PROMPT ───────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The system prompt a caller BINDS ITS SEAM WITH — this module owns no provider
|
|
196
|
+
* and picks none, exactly as `authorClose` does not: the compile call arrives as
|
|
197
|
+
* `generate`, and a caller wires it with `makeLoopGenerate(provider, {system:
|
|
198
|
+
* COMPILE_SYSTEM})`, whose own `maxTokens` default (F30) is what holds ten
|
|
199
|
+
* artifacts. Exported rather than inlined so the one register this call is made
|
|
200
|
+
* in is not re-spelled at every integration.
|
|
201
|
+
*/
|
|
202
|
+
export const COMPILE_SYSTEM = 'You compile a person\'s own words into a rubric a deterministic arbiter can run. You '
|
|
203
|
+
+ 'never decide whether anything passes, you never write code that runs, and you never invent a rule: the rulebook '
|
|
204
|
+
+ 'is fixed and you select from it.';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The compile prompt. Q6's and Q7's answers travel VERBATIM — they are the whole
|
|
208
|
+
* input, and paraphrasing a lay answer is how a rubric stops being the person's.
|
|
209
|
+
*
|
|
210
|
+
* The rulebook is rendered from `JUDGE_RULES` itself rather than restated, for
|
|
211
|
+
* the reason the catalogue block is rendered from the catalogue: the prompt and
|
|
212
|
+
* the gate must describe one set of facts.
|
|
213
|
+
* @param {{answers: Record<string|number, any>, questions?: Record<string|number, string>}} o
|
|
214
|
+
*/
|
|
215
|
+
export function cardCasesPrompt({ answers, questions = {} }) {
|
|
216
|
+
const q6 = questions[6] ?? 'When you judge the result yourself, what separates a pass from a fail?';
|
|
217
|
+
const q7 = questions[7] ?? 'Give one example you\'d pass and one you\'d fail, and say why.';
|
|
218
|
+
const rules = JUDGE_RULE_IDS.map((id) => `- ${id}\n the facts it reads: ${JUDGE_RULES[id].ask.trim().split('\n')[0].trim()}`);
|
|
219
|
+
|
|
220
|
+
return `You are compiling ONE person's answers into the two artifacts a judged close is signed with.
|
|
221
|
+
|
|
222
|
+
They are not a software engineer. They described what they look for in their own words, and your whole job is to
|
|
223
|
+
express that over the rulebook below — never to add a standard they did not state, and never to drop one they did.
|
|
224
|
+
|
|
225
|
+
THEIR ANSWERS — verbatim, and the only input you have
|
|
226
|
+
|
|
227
|
+
Q6. ${q6}
|
|
228
|
+
A6. ${answers?.[6] ?? '(no answer given)'}
|
|
229
|
+
|
|
230
|
+
Q7. ${q7}
|
|
231
|
+
A7. ${answers?.[7] ?? '(no answer given)'}
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
THE RULEBOOK — the only rules that exist. You SELECT from it; a rule that is not here cannot be asked for.
|
|
236
|
+
|
|
237
|
+
${rules.join('\n')}
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
THE CARD
|
|
242
|
+
|
|
243
|
+
One item per thing they actually look for, in the order they look for them. \`rule\` is selected from the rulebook;
|
|
244
|
+
\`text\` is what that line means IN THEIR WORDS. The judge only ever checks lines this card names, so a standard you
|
|
245
|
+
leave out is a standard nobody checks — and a standard they never stated is one you must not add.
|
|
246
|
+
|
|
247
|
+
THE CALIBRATION SET
|
|
248
|
+
|
|
249
|
+
Exactly ${CALIBRATION_SIZE} cases. Each one is REAL SOURCE TEXT — a small, realistic file the judge could be pointed
|
|
250
|
+
at — plus what the arbiter must decide about it.
|
|
251
|
+
|
|
252
|
+
- \`artifact\` is the code itself, never a description of it.
|
|
253
|
+
- \`expect.verdict\` is "pass" when the artifact satisfies every card line, "red" when it violates any.
|
|
254
|
+
- \`expect.reds\` is EMPTY for a pass, and for a red it names one entry per violation: the card \`rule\` it breaks
|
|
255
|
+
and the \`fn\` (function name) it breaks it on.
|
|
256
|
+
|
|
257
|
+
AT LEAST ONE PASS AND AT LEAST ONE RED. A set that can only fail one way measures nothing.
|
|
258
|
+
Every rule you name in a case must be a rule your card carries.
|
|
259
|
+
|
|
260
|
+
HOW TO ANSWER
|
|
261
|
+
|
|
262
|
+
Call the ${PROPOSAL_TOOL_NAME} tool exactly once, with the card and the ${CALIBRATION_SIZE} cases as its arguments.
|
|
263
|
+
Do not write them as text.`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── 3. THE COMPILE CHANNEL ──────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
/** the compile's own channel, over the SHARED bounded ladder (`askStructured`) */
|
|
269
|
+
const proposalChannel = () => ({
|
|
270
|
+
name: PROPOSAL_TOOL_NAME,
|
|
271
|
+
instruction: `Your reply did not deliver exactly one proposal through the ${PROPOSAL_TOOL_NAME} tool. `
|
|
272
|
+
+ `Call ${PROPOSAL_TOOL_NAME} exactly once, with the card and the ${CALIBRATION_SIZE} cases as its arguments. `
|
|
273
|
+
+ 'Do not write them as text.',
|
|
274
|
+
textPath: 'proposal',
|
|
275
|
+
tool: (/** @type {{calls: any[]}} */ box) => proposalTool(box),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── 4. SHOWN AND FIXED ──────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/** every free string in the two artifacts, scrubbed at INGEST — an answer, a
|
|
281
|
+
* proposed snippet and a signer's edit all become a SIGNED artefact that outlives
|
|
282
|
+
* the run, and a file that captures a key captures it forever. The ONE shared
|
|
283
|
+
* inventory, never a second one.
|
|
284
|
+
*
|
|
285
|
+
* AND IT ANNOUNCES. A scrub that silently rewrites bytes leaves a signer reading
|
|
286
|
+
* one artifact on screen and signing another on disk — the same failure
|
|
287
|
+
* `applyGenreEnv` refuses by returning its `dropped` list rather than quietly
|
|
288
|
+
* deleting a model-authored env var. Every altered value is reported by PATH (the
|
|
289
|
+
* path only: echoing the matched secret into the announcement would be the same
|
|
290
|
+
* leak, one hop on), so the caller can say *this was masked* out loud.
|
|
291
|
+
* @param {any} v @param {string} at @param {{path: string}[]} altered */
|
|
292
|
+
function scrubDeep(v, at, altered) {
|
|
293
|
+
if (typeof v === 'string') {
|
|
294
|
+
const masked = redactSecrets(v);
|
|
295
|
+
// `redactSecrets` returns a non-matching string byte-identical, so this
|
|
296
|
+
// compares equal on every honest value and costs the clean path nothing
|
|
297
|
+
if (masked !== v) altered.push({ path: at });
|
|
298
|
+
return masked;
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(v)) return v.map((x, i) => scrubDeep(x, `${at}[${i}]`, altered));
|
|
301
|
+
if (isObj(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, scrubDeep(x, at ? `${at}.${k}` : k, altered)]));
|
|
302
|
+
return v;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* THE D5 MOMENT. The proposal is SHOWN; the signer's `fix` is what is STORED.
|
|
307
|
+
*
|
|
308
|
+
* The fix replaces the proposal WHOLE rather than merging into it. A merge would
|
|
309
|
+
* mean a signer who deleted a card line got it back, and "the signer signs the
|
|
310
|
+
* whole hash" is not a rule that survives a field-wise reconciliation. Handing
|
|
311
|
+
* back no fix is a legal answer — signing the proposal as proposed — and it says
|
|
312
|
+
* so (`source`), because *a person read this and changed nothing* and *nobody has
|
|
313
|
+
* looked* are two different facts.
|
|
314
|
+
*
|
|
315
|
+
* THE GATE RUNS OVER WHAT WILL BE STORED, never over the proposal: a signer can
|
|
316
|
+
* fix a legal proposal into an illegal one, and the artifact that reaches the
|
|
317
|
+
* hash is the one that must be legal.
|
|
318
|
+
*
|
|
319
|
+
* `scrubbed` is the INGEST SCRUB'S OWN ANNOUNCEMENT — the paths whose bytes the
|
|
320
|
+
* secret inventory masked on the way in. Empty on every honest artifact, and
|
|
321
|
+
* never silent when it is not: a signer signs a hash over the STORED bytes, so
|
|
322
|
+
* "what you typed is not what was stored" is a fact they are owed rather than one
|
|
323
|
+
* the redactor keeps.
|
|
324
|
+
* @param {{proposal: any, fix?: any}} o
|
|
325
|
+
* @returns {{ok: boolean, reds: Red[], card: any, cases: any[]|null, source: 'signer'|'proposal',
|
|
326
|
+
* scrubbed: {path: string}[]}}
|
|
327
|
+
*/
|
|
328
|
+
export function signJudgedArtifacts({ proposal, fix = null }) {
|
|
329
|
+
const source = /** @type {'signer'|'proposal'} */ (fix === null || fix === undefined ? 'proposal' : 'signer');
|
|
330
|
+
const chosen = source === 'signer' ? fix : proposal;
|
|
331
|
+
if (!isObj(chosen)) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
reds: [{ code: 'invalid-value', path: 'proposal', detail: 'the signed artifacts are an object {card, cases}' }],
|
|
335
|
+
card: null,
|
|
336
|
+
cases: null,
|
|
337
|
+
source,
|
|
338
|
+
scrubbed: [],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// A COPY, scrubbed. The caller keeps its proposal (the run's own audit of what
|
|
342
|
+
// the model said); what is stored can never be reached back through it.
|
|
343
|
+
/** @type {{path: string}[]} */
|
|
344
|
+
const scrubbed = [];
|
|
345
|
+
const card = scrubDeep(structuredClone(/** @type {any} */ (chosen).card ?? null), 'card', scrubbed);
|
|
346
|
+
const cases = scrubDeep(structuredClone(/** @type {any} */ (chosen).cases ?? null), 'cases', scrubbed);
|
|
347
|
+
const v = validateJudgedArtifacts({ card, cases });
|
|
348
|
+
return { ok: v.ok, reds: v.reds, card, cases: Array.isArray(cases) ? cases : null, source, scrubbed };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── 5. THE FOLD — enumerated into the signed spec ───────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Fold the two signed artifacts into the close declaration, ENUMERATED.
|
|
355
|
+
*
|
|
356
|
+
* WHERE EACH LANDS, and both are inside the spec so `jobSpecHash` covers them by
|
|
357
|
+
* construction (the hash is over the whole resolved spec):
|
|
358
|
+
* - THE CARD replaces the judged stage's own `params.card`. ONE card, and it is
|
|
359
|
+
* the SIGNED one: the composer's draft is a draft, and two cards in one close
|
|
360
|
+
* is the drift a single storage site exists to prevent. The stage is where it
|
|
361
|
+
* already lives, which is what `validateCard`, the M2 runner and the locate
|
|
362
|
+
* prompt all read.
|
|
363
|
+
* - THE CASES land beside the stages as `calibration.cases` — a close-level
|
|
364
|
+
* artifact, because the set calibrates the RULER and not one stage's params,
|
|
365
|
+
* and because module 5's gate reads it before any stage runs.
|
|
366
|
+
* - THE JUDGE MODEL lands beside them as `calibration.judgeModel`, and it is the
|
|
367
|
+
* PIN (`JUDGE_MODEL`) rather than anything a caller may name. A calibration set
|
|
368
|
+
* is only worth the tier that graded it — §4.2's whole safety argument is worth
|
|
369
|
+
* exactly as much as BA-20's injection evidence, which is haiku-4.5's alone — so
|
|
370
|
+
* the set has to carry which tier certified it or nothing downstream can tell a
|
|
371
|
+
* signed floor from a bumped one. Storing it HERE, inside the spec, is what makes
|
|
372
|
+
* a bump flip `jobSpecHash`: the signature dies, the signer re-signs, and a
|
|
373
|
+
* re-sign is a re-run of the calibration gate. That is the docstring on
|
|
374
|
+
* `JUDGE_MODEL` finally given a detector instead of a promise (F45).
|
|
375
|
+
*
|
|
376
|
+
* A card with NOWHERE TO LAND THROWS. It is a caller's own broken input — a
|
|
377
|
+
* signed rubric folded into a close with no judged ruler — and there is no safe
|
|
378
|
+
* direction to degrade toward: storing it would sign a rubric nothing reads.
|
|
379
|
+
*
|
|
380
|
+
* The input is never mutated: a closeDecl is a signed artefact and rewriting a
|
|
381
|
+
* caller's copy in place is how two readers come to disagree about what was
|
|
382
|
+
* signed.
|
|
383
|
+
* @param {any} closeDecl @param {{card: any, cases: any[]}} o
|
|
384
|
+
* @returns {any} a new closeDecl
|
|
385
|
+
*/
|
|
386
|
+
export function foldJudgedArtifacts(closeDecl, { card, cases }) {
|
|
387
|
+
const out = structuredClone(closeDecl);
|
|
388
|
+
const stages = Array.isArray(out?.stages) ? out.stages : [];
|
|
389
|
+
const judged = stages.filter((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND);
|
|
390
|
+
if (judged.length !== 1) {
|
|
391
|
+
throw new Error(`[cardauthor] the signed card folds into the close's ONE judged-floor stage, and this close has `
|
|
392
|
+
+ `${judged.length} — a rubric with nowhere to land is signed evidence nothing reads, and a second judged stage `
|
|
393
|
+
+ 'is a second bar for one verdict');
|
|
394
|
+
}
|
|
395
|
+
judged[0].params = { ...(isObj(judged[0].params) ? judged[0].params : {}), card: structuredClone(card) };
|
|
396
|
+
out.calibration = { cases: structuredClone(cases), judgeModel: JUDGE_MODEL };
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── 6. THE PAID SEAM ────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* ONE compile call: the person's Q6 and Q7 in, a proposal out.
|
|
404
|
+
*
|
|
405
|
+
* It buys ONE artifact through ONE channel, and the malformed-emission ladder is
|
|
406
|
+
* the SHARED one (`askStructured`) rather than a second copy — a second ladder is
|
|
407
|
+
* a second place for the cap, the reject block and the re-ask to drift.
|
|
408
|
+
*
|
|
409
|
+
* WHAT IS AND IS NOT RETRIED, and the split is the standing one: a reply that
|
|
410
|
+
* carries no proposal is a TRANSPORT fault and is retried under the bound; a
|
|
411
|
+
* proposal that PARSED and then broke the ruling is not — the model answered
|
|
412
|
+
* perfectly clearly and re-asking buys another call to be told the same thing.
|
|
413
|
+
* The proposal is returned as it was said, never repaired: no JSON repair, ever.
|
|
414
|
+
*
|
|
415
|
+
* The book is the CALLER's, so one ceiling governs the survey, the declaration
|
|
416
|
+
* ladder and this compile — the advertised ceiling and the enforced ceiling are
|
|
417
|
+
* the same ceiling.
|
|
418
|
+
* @param {{answers: Record<string|number, any>, questions?: Record<string|number, string>,
|
|
419
|
+
* generate: Function, book?: ReturnType<typeof makeCostBook>,
|
|
420
|
+
* structuredMode?: 'tool'|'text', structureRetries?: number}} o
|
|
421
|
+
* @returns {Promise<{ok: boolean, proposal: any, reds: Red[], stop: string|null,
|
|
422
|
+
* attempts: number, raw: string, cost: any, raws: any[]}>}
|
|
423
|
+
*/
|
|
424
|
+
export async function proposeJudgedArtifacts({
|
|
425
|
+
answers, questions = {}, generate,
|
|
426
|
+
book = makeCostBook({}),
|
|
427
|
+
structuredMode = 'tool',
|
|
428
|
+
structureRetries = MAX_PROPOSAL_RETRIES,
|
|
429
|
+
}) {
|
|
430
|
+
// TIGHTEN-ONLY, floor 0, exactly as `authorClose` clamps its own two axes: a
|
|
431
|
+
// caller may buy fewer malformed-emission retries, never more than the ceiling
|
|
432
|
+
// this module declares and the suite pins.
|
|
433
|
+
const retries = Math.max(0, Math.min(Math.trunc(Number(structureRetries)) || 0, MAX_PROPOSAL_RETRIES));
|
|
434
|
+
const prompt = cardCasesPrompt({ answers, questions });
|
|
435
|
+
const ask = await askStructured({
|
|
436
|
+
messages: [{ role: 'user', content: prompt }],
|
|
437
|
+
generate,
|
|
438
|
+
mode: structuredMode,
|
|
439
|
+
retries,
|
|
440
|
+
label: PROPOSAL_LABEL,
|
|
441
|
+
book,
|
|
442
|
+
channel: proposalChannel(),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const base = {
|
|
446
|
+
proposal: ask.artifact ?? null, attempts: ask.attempts, raw: ask.raw,
|
|
447
|
+
cost: book.report(), raws: book.raws(),
|
|
448
|
+
};
|
|
449
|
+
if (ask.budget) {
|
|
450
|
+
return {
|
|
451
|
+
...base,
|
|
452
|
+
ok: false,
|
|
453
|
+
stop: ask.budget,
|
|
454
|
+
reds: [{
|
|
455
|
+
code: ask.budget,
|
|
456
|
+
path: 'budgetUsd',
|
|
457
|
+
detail: `the authoring ceiling bound before the rubric compile: $${book.ceilingUsd} is spent, and the call was `
|
|
458
|
+
+ 'never made. The stop IS the checkpoint — raise the ceiling and re-run, or read what is here and stop',
|
|
459
|
+
}],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (ask.providerError) {
|
|
463
|
+
// a casualty, never evidence about the model — and the transport's own prose
|
|
464
|
+
// is scrubbed on the way into a red the caller keeps
|
|
465
|
+
return {
|
|
466
|
+
...base,
|
|
467
|
+
ok: false,
|
|
468
|
+
stop: 'provider-red',
|
|
469
|
+
reds: [{ code: 'provider-red', path: PROPOSAL_LABEL, detail: redactSecrets(String(ask.providerError)) }],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (!ask.artifact) {
|
|
473
|
+
return { ...base, ok: false, stop: 'artifact-red', reds: [/** @type {Red} */ (ask.red)] };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const v = validateJudgedArtifacts({ card: ask.artifact.card, cases: ask.artifact.cases });
|
|
477
|
+
if (!v.ok) return { ...base, ok: false, stop: 'proposal-invalid', reds: v.reds };
|
|
478
|
+
return { ...base, ok: true, stop: null, reds: [] };
|
|
479
|
+
}
|
package/src/declaredclose.js
CHANGED
|
@@ -68,11 +68,14 @@
|
|
|
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 } from './kinds.js';
|
|
71
|
+
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED, JUDGED_FLOOR_KIND } 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,
|
|
75
75
|
} from './authoring.js';
|
|
76
|
+
// the calibration set's own gate lives beside the rulebook it is graded against
|
|
77
|
+
// (src/judged.js) — one opinion about what a judged floor's signed artifacts are
|
|
78
|
+
import { validateCalibrationSet } from './judged.js';
|
|
76
79
|
import { stageGap } from './ralph.js';
|
|
77
80
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
78
81
|
|
|
@@ -99,7 +102,14 @@ export const DECLARED_GENRES = Object.freeze([TYPES_GENRE.name]);
|
|
|
99
102
|
* constrained to the names the genre OWNS, so it can never become an environment
|
|
100
103
|
* channel of its own.
|
|
101
104
|
*/
|
|
102
|
-
export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv']);
|
|
105
|
+
export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv', 'calibration']);
|
|
106
|
+
|
|
107
|
+
/** what a stored calibration set carries, ENUMERATED: the frozen cases and the judge
|
|
108
|
+
* tier that graded them. Both are inside the spec, so `jobSpecHash` covers both — which
|
|
109
|
+
* is the whole mechanism behind "a judge-model bump forces a full recalibration"
|
|
110
|
+
* (src/judged.js, `JUDGE_MODEL`): the field moves, the hash moves, the signature dies,
|
|
111
|
+
* and the re-sign re-runs the gate. Anything else in there is a field nobody reads. */
|
|
112
|
+
export const CALIBRATION_FIELDS = Object.freeze(['cases', 'judgeModel']);
|
|
103
113
|
|
|
104
114
|
/**
|
|
105
115
|
* The prefix M1 puts on EVERY gap line of a declared stage (its `ctx.gapKeep`).
|
|
@@ -165,15 +175,22 @@ export const HUMAN_CHECKPOINTS = Object.freeze([HITL_PAUSE]);
|
|
|
165
175
|
* validator reads one hierarchy table rather than hard-coding a class.
|
|
166
176
|
*
|
|
167
177
|
* `hard` is by CONSTRUCTION: every mechanical kind is a command whose measurement
|
|
168
|
-
* is the truth. `hitl` joined it at N4 slice 1, and
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
178
|
+
* is the truth. `hitl` joined it at N4 slice 1, and `soft` at softgreen module 3
|
|
179
|
+
* — each only because its kind is genuinely LIVE (`human-confirms`, then
|
|
180
|
+
* `judged-floor`): the widening and the kind go in one commit, or this table
|
|
181
|
+
* admits a class the executor cannot render.
|
|
182
|
+
*
|
|
183
|
+
* `hitl` STAYS despite the class's retirement (PRD v1.71: the human stage moved
|
|
184
|
+
* out of the close and became the review door at the end of every run). It is
|
|
185
|
+
* kept because `jobs/litectx-maintainer.json` is a SIGNED spec carrying a hitl
|
|
186
|
+
* declaration, and a signed spec is untouchable — dropping the spelling here
|
|
187
|
+
* would invalidate a signature the operator already gave, which is a re-sign
|
|
188
|
+
* nobody asked for. Retiring it is a separate, deliberate deletion.
|
|
172
189
|
*
|
|
173
190
|
* The direction that matters is unchanged. This says which class a declaration
|
|
174
191
|
* MAY claim; `closeCeiling` says which class its stages actually NEED. A pick
|
|
175
192
|
* below the ceiling is still the silent upgrade `class-ceiling` refuses. */
|
|
176
|
-
export const DECLARED_CLOSE_CLASSES = Object.freeze(['hard', 'hitl']);
|
|
193
|
+
export const DECLARED_CLOSE_CLASSES = Object.freeze(['hard', 'soft', 'hitl']);
|
|
177
194
|
|
|
178
195
|
/** @param {any} job @returns {boolean} */
|
|
179
196
|
export function isDeclaredClose(job) {
|
|
@@ -186,11 +203,24 @@ export function isDeclaredClose(job) {
|
|
|
186
203
|
*
|
|
187
204
|
* `offer` and `needs` pass through untouched when a declaration carries them
|
|
188
205
|
* (v1's schema does not let one), so `checkMenu`'s derivation is unchanged.
|
|
206
|
+
*
|
|
207
|
+
* THE JUDGE TIER RIDES ALONG, and it rides as a STAMP for the same reason `offer`
|
|
208
|
+
* does. The tier that graded this close's calibration set is a CLOSE-level fact
|
|
209
|
+
* (`calibration.judgeModel`), and the stage that would buy a judge call is a STAGE;
|
|
210
|
+
* `runStage` is handed stages and never the declaration they came out of. Rather than
|
|
211
|
+
* plumb a second close-level channel down through every executor seam, the one place
|
|
212
|
+
* that already turns a declaration into stages carries it across — and it carries it
|
|
213
|
+
* from the SIGNED bytes, so a stage can never name a tier the signature does not.
|
|
214
|
+
* `runJudgedFloor` refuses a mismatch; the absence of a stamp is the SPEC gate's
|
|
215
|
+
* business (`validateCloseDecl` reds a set with no model), never a runtime fall-back.
|
|
189
216
|
* @param {any} closeDecl
|
|
190
217
|
* @returns {any[]|null} null when this is not a declaration
|
|
191
218
|
*/
|
|
192
219
|
export function declaredStages(closeDecl) {
|
|
193
220
|
if (!isObj(closeDecl) || !Array.isArray(closeDecl.stages)) return null;
|
|
221
|
+
const calibrationJudgeModel = isObj(closeDecl.calibration) && isNonEmptyString(closeDecl.calibration.judgeModel)
|
|
222
|
+
? closeDecl.calibration.judgeModel
|
|
223
|
+
: null;
|
|
194
224
|
return closeDecl.stages.map((s) => (isObj(s)
|
|
195
225
|
// RULING 5, stamped by the ARBITER rather than trusted from the artefact:
|
|
196
226
|
// a judged or human stage is `offer: false` BY LAW, so `checkMenu` can never
|
|
@@ -198,7 +228,12 @@ export function declaredStages(closeDecl) {
|
|
|
198
228
|
// declaration that says anything else (`human-stage-offered`), so this is
|
|
199
229
|
// never overwriting a signer-visible value — it is filling in a law the
|
|
200
230
|
// declaration schema deliberately gives the composer no way to state.
|
|
201
|
-
? {
|
|
231
|
+
? {
|
|
232
|
+
...s,
|
|
233
|
+
gapKeep: DECLARED_GAP_KEEP,
|
|
234
|
+
...(NEVER_OFFERED_KINDS.includes(s.kind) ? { offer: false } : {}),
|
|
235
|
+
...(s.kind === JUDGED_FLOOR_KIND && calibrationJudgeModel !== null ? { calibrationJudgeModel } : {}),
|
|
236
|
+
}
|
|
202
237
|
: s));
|
|
203
238
|
}
|
|
204
239
|
|
|
@@ -374,6 +409,56 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
374
409
|
});
|
|
375
410
|
for (const r of v.reds) reds.push({ ...r, path: r.path ? `${at}.${r.path}` : at });
|
|
376
411
|
|
|
412
|
+
// ── THE FROZEN CALIBRATION SET (softgreen module 4, design §4.4). It rides
|
|
413
|
+
// the closeDecl rather than a stage's params because it calibrates the RULER,
|
|
414
|
+
// not one stage's parameters — and because it is read BEFORE any stage runs.
|
|
415
|
+
//
|
|
416
|
+
// WHAT IS CHECKED HERE is that a stored set is LEGAL: exactly the signed size,
|
|
417
|
+
// both polarities present, itemized reds naming lines THIS close's card
|
|
418
|
+
// actually carries. WHAT IS NOT is whether the pipe grades it correctly — that
|
|
419
|
+
// is module 5's gate, it costs paid calls, and a validator cannot buy them.
|
|
420
|
+
//
|
|
421
|
+
// A set with NO judged stage to calibrate is its own red: the card lives on
|
|
422
|
+
// the stage, so an orphan set is graded against nothing and would validate
|
|
423
|
+
// clean forever. The reverse (a judged stage with no set) is deliberately NOT
|
|
424
|
+
// redded here — making the set MANDATORY is the signing gate's call, and
|
|
425
|
+
// module 5 owns it.
|
|
426
|
+
if (closeDecl.calibration !== undefined) {
|
|
427
|
+
const judged = (Array.isArray(closeDecl.stages) ? closeDecl.stages : [])
|
|
428
|
+
.filter((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND);
|
|
429
|
+
if (!isObj(closeDecl.calibration)) {
|
|
430
|
+
red('invalid-value', `${at}.calibration`, 'the frozen calibration set, as an object carrying `cases`');
|
|
431
|
+
} else if (judged.length !== 1) {
|
|
432
|
+
red('calibration-orphan', `${at}.calibration`, `this close carries a calibration set and ${judged.length} `
|
|
433
|
+
+ 'judged-floor stage(s). The set calibrates ONE judged ruler against ONE card — with no judged stage it is '
|
|
434
|
+
+ 'graded against nothing and would read clean forever, and with two there is no one card it belongs to',
|
|
435
|
+
{ judgedStages: judged.length });
|
|
436
|
+
} else {
|
|
437
|
+
for (const key of Object.keys(closeDecl.calibration)) {
|
|
438
|
+
if (!CALIBRATION_FIELDS.includes(key)) red('invalid-value', `${at}.calibration.${key}`, `a calibration set carries ${CALIBRATION_FIELDS.map((f) => `\`${f}\``).join(' and ')} and nothing else`);
|
|
439
|
+
}
|
|
440
|
+
// WHICH TIER GRADED THIS SET, and it is REQUIRED rather than optional. A
|
|
441
|
+
// stored set is only worth the judge that graded it (§4.2's safety argument
|
|
442
|
+
// is BA-20's haiku-4.5 injection evidence and nothing else), so a set with
|
|
443
|
+
// no model beside it is a set nobody can attribute — and the `JUDGE_MODEL`
|
|
444
|
+
// docstring's promise that a bump forces recalibration has nothing to fire
|
|
445
|
+
// on. Required HERE and not at the stage, for the same reason the SET's own
|
|
446
|
+
// presence is the signing gate's call and not this validator's: this gate
|
|
447
|
+
// reads a stored artifact and says whether it is legal. The RUNTIME half —
|
|
448
|
+
// refusing to grade when the stored tier is not the tier this run would buy
|
|
449
|
+
// — is the judged stage's (`runJudgedFloor`), reached through the stamp
|
|
450
|
+
// `declaredStages` puts on the stage.
|
|
451
|
+
if (closeDecl.calibration.judgeModel === undefined) {
|
|
452
|
+
red('missing-required', `${at}.calibration.judgeModel`, 'the judge tier this set was GRADED by — a calibration '
|
|
453
|
+
+ 'nobody can attribute to a model is a floor nobody can tell apart from a bumped one');
|
|
454
|
+
} else if (!isNonEmptyString(closeDecl.calibration.judgeModel)) {
|
|
455
|
+
red('invalid-value', `${at}.calibration.judgeModel`, 'the judge tier this set was graded by, as a non-empty string');
|
|
456
|
+
}
|
|
457
|
+
const cv = validateCalibrationSet(closeDecl.calibration.cases, { card: judged[0]?.params?.card ?? null });
|
|
458
|
+
for (const r of cv.reds) reds.push({ ...r, path: r.path ? `${at}.${r.path}` : `${at}.calibration` });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
377
462
|
const ok = reds.length === 0;
|
|
378
463
|
return {
|
|
379
464
|
ok,
|
|
@@ -480,11 +565,16 @@ function translate(r, redact) {
|
|
|
480
565
|
* @param {(s: string) => string} [redact] the scrub, applied at THIS boundary
|
|
481
566
|
* @param {{timeoutMs?: number, cwd?: string, seedRef?: string,
|
|
482
567
|
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
|
|
483
|
-
* humanRuling?: {decision: string, text?: string|null}|null
|
|
568
|
+
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
569
|
+
* judgeLoop?: ((o: {system: string}) => any)|null,
|
|
570
|
+
* onJudgeCost?: ((c: any) => void)|null}} [opts]
|
|
484
571
|
* @returns {Promise<any>}
|
|
485
572
|
*/
|
|
486
573
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
487
|
-
const {
|
|
574
|
+
const {
|
|
575
|
+
timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
|
|
576
|
+
judgeLoop = null, onJudgeCost = null,
|
|
577
|
+
} = opts;
|
|
488
578
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
489
579
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
490
580
|
// missing one is the close unable to run, not a red about the worker.
|
|
@@ -513,6 +603,14 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
513
603
|
// human stage reads the absence as "nobody has answered yet", which is the
|
|
514
604
|
// whole of what a pause means.
|
|
515
605
|
humanRuling,
|
|
606
|
+
// THE PAID SEAM and THE METER, both operator territory and both absent on
|
|
607
|
+
// every mechanical close. A judged stage with no seam STOPS as a wiring gap
|
|
608
|
+
// rather than falling back to some other model (M1 owns no provider and the
|
|
609
|
+
// judge tier is pinned), and every locate call it does buy is reported OUT
|
|
610
|
+
// through the meter so the run's ONE ledger can see it — a close has cost $0
|
|
611
|
+
// until this kind, and a budget must fund the attempt PLUS its close.
|
|
612
|
+
judgeLoop,
|
|
613
|
+
onJudgeCost,
|
|
516
614
|
seedTrees,
|
|
517
615
|
};
|
|
518
616
|
try {
|