bareloop 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +510 -0
- package/README.md +18 -7
- package/bareloop.context.md +456 -19
- package/package.json +3 -3
- package/src/authorflow.js +290 -49
- package/src/authoring.js +258 -46
- package/src/authorjob.js +390 -32
- package/src/authorscout.js +13 -1
- package/src/bridges.js +238 -11
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/clock.js +21 -3
- package/src/declaredclose.js +188 -13
- package/src/index.js +90 -7
- package/src/job.js +28 -9
- package/src/judged.js +765 -0
- package/src/kinds.js +578 -13
- package/src/ledger.js +24 -1
- package/src/planrun.js +517 -14
- package/src/ralph.js +9 -0
- package/src/reuse.js +514 -96
- package/src/reviewdoor.js +246 -0
- package/src/root.js +1 -1
- package/src/run.js +81 -8
- package/src/selection.js +23 -7
- package/types/authorflow.d.ts +233 -21
- package/types/authoring.d.ts +99 -39
- package/types/authorjob.d.ts +44 -6
- package/types/authorscout.d.ts +7 -1
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +93 -6
- package/types/index.d.ts +11 -7
- package/types/job.d.ts +22 -4
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +227 -8
- package/types/planrun.d.ts +73 -3
- package/types/reuse.d.ts +218 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +60 -2
package/src/kinds.js
CHANGED
|
@@ -64,8 +64,9 @@ import { realpathSync } from 'node:fs';
|
|
|
64
64
|
import { readFile, stat, mkdtemp, rm, symlink } from 'node:fs/promises';
|
|
65
65
|
import { tmpdir } from 'node:os';
|
|
66
66
|
import { join, sep } from 'node:path';
|
|
67
|
-
import { globToPrefix } from './validate.js';
|
|
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, JUDGE_MODEL } from './judged.js';
|
|
69
70
|
|
|
70
71
|
/** the close's two judgment exits — a stage's verdict, in the shape the
|
|
71
72
|
* hand-written closes already spoke (exit code is truth) */
|
|
@@ -132,23 +133,163 @@ export const STOP_FAULTS = Object.freeze({
|
|
|
132
133
|
|
|
133
134
|
export const AGGREGATES = Object.freeze(['first', 'sum']);
|
|
134
135
|
export const SIGNS = Object.freeze([1, -1]);
|
|
135
|
-
/** every kind this executor implements.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
136
|
+
/** every kind this executor implements. It is asserted EQUAL to the catalogue's
|
|
137
|
+
* own live half by the suite, both directions: a kind the catalogue offers and
|
|
138
|
+
* this executor cannot run is a close that stops at runtime, and a kind this
|
|
139
|
+
* executor runs that the catalogue never names is a capability nothing can
|
|
140
|
+
* reach. `judged-floor` joined at softgreen module 2 — the runner and the
|
|
141
|
+
* catalogue unlock in ONE commit for exactly that reason. */
|
|
142
|
+
export const LIVE_KINDS = Object.freeze(['command-exit', 'count-not-worse', 'pattern-absent-in-diff', 'files-changed', 'human-confirms', 'judged-floor']);
|
|
143
|
+
/**
|
|
144
|
+
* The judged stage's ladder: ONE attempt plus ONE retry, and no more.
|
|
145
|
+
*
|
|
146
|
+
* The POC measured a malformed emission roughly 1 in 6, which is exactly the
|
|
147
|
+
* shape a single retry buys down and a longer ladder buys nothing more of — each
|
|
148
|
+
* further attempt is another paid call against the same unchanged artifact and
|
|
149
|
+
* the same unchanged prompt. It is a CEILING the arbiter owns, never a knob a
|
|
150
|
+
* declaration can widen: a stage that could re-ask a model forever is a stage
|
|
151
|
+
* that can spend a budget without ever rendering a verdict.
|
|
152
|
+
*
|
|
153
|
+
* A `pricing-red` is never retried at all (see `runJudgedFloor`): a retry of an
|
|
154
|
+
* unpriced call only buys more spend nobody can see (F6).
|
|
155
|
+
*/
|
|
156
|
+
export const JUDGE_ATTEMPTS = 2;
|
|
157
|
+
/**
|
|
158
|
+
* The ceiling on how many artifacts ONE judged stage may buy a judgement of —
|
|
159
|
+
* the sibling of `MAX_TERMS` and `MAX_STAGES`, and here for the same reason: a
|
|
160
|
+
* runaway list is a bill, not a red. Enforced by the catalogue's validator
|
|
161
|
+
* before any token, so a declaration that names more is refused at the gate
|
|
162
|
+
* rather than discovered at the invoice.
|
|
163
|
+
*/
|
|
164
|
+
export const MAX_JUDGED_PATHS = 8;
|
|
165
|
+
/** RULING 8 — the stages the seed-verdict read (D12) must NOT run. A judged
|
|
166
|
+
* number at seed is unstable and a person at seed is an interview, not a
|
|
167
|
+
* measurement; their bar comes from calibration (soft-green) or from the person
|
|
168
|
+
* themselves (hitl), never from the seed. The skip is RECORDED by name rather
|
|
169
|
+
* than taken in silence (F59: absent is not empty), so `seedRead` still returns
|
|
170
|
+
* one row per declared stage. */
|
|
171
|
+
/** the one kind a MODEL renders. Named once for the same reason `HUMAN_KIND` is:
|
|
172
|
+
* the authoring pipeline, the card, the catalogue and the close each key a
|
|
173
|
+
* decision on it, and five literals is how they come to disagree. */
|
|
174
|
+
export const JUDGED_FLOOR_KIND = 'judged-floor';
|
|
175
|
+
export const SEED_EXEMPT_KINDS = Object.freeze([JUDGED_FLOOR_KIND, 'human-confirms']);
|
|
176
|
+
/** the one kind a PERSON renders — named once, because the runner keys three
|
|
177
|
+
* decisions on it (is there a door to answer, which stage paused, whose ruling
|
|
178
|
+
* this is) and three literals is how they come to disagree */
|
|
179
|
+
export const HUMAN_KIND = 'human-confirms';
|
|
180
|
+
/** the three doors the signer is offered at a hitl pause (2026-08-12 §1, doors
|
|
181
|
+
* re-cut 2026-08-17). No fourth door, and no free-text-only variant: a red that
|
|
182
|
+
* is not one of these two is not a ruling the run can act on.
|
|
183
|
+
*
|
|
184
|
+
* `cancel` was the third door and is DELETED as a concept (hamr: *"what's the
|
|
185
|
+
* point of cancel anyways? pause can resume — that would be more honest"*). The
|
|
186
|
+
* case it served — a person who looks and does not want to carry on — is a pause
|
|
187
|
+
* nobody returns to, which the 60-day checkpoint TTL retires on its own. Nothing
|
|
188
|
+
* here forces a forever-decision at the one moment the person has least reason
|
|
189
|
+
* to make one. */
|
|
190
|
+
export const HUMAN_DECISIONS = Object.freeze(['accept', 'rerun', 'pause']);
|
|
191
|
+
|
|
192
|
+
// ── THE REVIEW DOOR's vocabulary (softgreen module 8, PRD v1.71 §3) ──────────
|
|
193
|
+
//
|
|
194
|
+
// The pause machinery re-homed: the same three doors, one level out — from a
|
|
195
|
+
// stage INSIDE the close to the door at the END of a run. The vocabulary lives
|
|
196
|
+
// here, beside the doors themselves, because `src/planrun.js` needs it to OPEN a
|
|
197
|
+
// door and `src/reviewdoor.js` needs it to ANSWER one, and a constant imported
|
|
198
|
+
// through the answering module would drag the registry (and `runJob` behind it)
|
|
199
|
+
// into the run's own import graph.
|
|
200
|
+
|
|
201
|
+
/** the door's record type on the spine. It is a DISPOSITION, never a verdict:
|
|
202
|
+
* hamr's law, verbatim — *"it's important not to change the loop self verdict"*.
|
|
203
|
+
* The close mints the verdict before this is written, and nothing downstream of
|
|
204
|
+
* this record may change what the ledger recorded. */
|
|
205
|
+
export const REVIEW_DOOR = 'review-door';
|
|
206
|
+
|
|
207
|
+
/** the terminals a door opens on: the verdict-bearing ones, and only those. A red,
|
|
208
|
+
* a casualty and a governance halt are not offered to a person as merchandise —
|
|
209
|
+
* they are already decision-ready in their own right, with their own levers. */
|
|
210
|
+
export const DOOR_OPEN_OUTCOMES = Object.freeze(['green', 'already-green']);
|
|
211
|
+
|
|
212
|
+
/** the classes whose door opens WITHOUT being asked for. Soft-green is here
|
|
213
|
+
* because the judge is young: its green is quarantined at mint (module 6) and a
|
|
214
|
+
* person's `accept` is the only thing that releases the credit, so a run that
|
|
215
|
+
* never offered the door would hold its own credit forever.
|
|
216
|
+
*
|
|
217
|
+
* `green` is deliberately ABSENT, and that is the one operator-facing default in
|
|
218
|
+
* this module: a green-class run behaves exactly as it always has unless the
|
|
219
|
+
* runner asks for the door (`reviewDoor: true`, `--review-door`). PRD v1.71 §3
|
|
220
|
+
* says *"the door at the end of EVERY run"* and also that a green door is
|
|
221
|
+
* NON-BLOCKING, and the two readings differ only in whether an unasked-for door
|
|
222
|
+
* appears on an existing job's spine. Flipping it is one line — adding `'green'`
|
|
223
|
+
* here — and it is hamr's to flip, not this module's to assume. A constant, never
|
|
224
|
+
* a threshold; widening it is arbiter territory. */
|
|
225
|
+
export const REVIEW_DOOR_CLASSES = Object.freeze(['soft-green']);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Does this run end at a review door? The operator's flag WINS in both
|
|
229
|
+
* directions and is never inferred: `true` opens it for any class, `false` shuts
|
|
230
|
+
* it for any class, and `null`/absent is the class default above.
|
|
231
|
+
* @param {any} job the signed spec (its `verdictType` is the class)
|
|
232
|
+
* @param {boolean|null} [reviewDoor] the operator's explicit choice, or null
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
export function doorOpens(job, reviewDoor = null) {
|
|
236
|
+
if (reviewDoor === true) return true;
|
|
237
|
+
if (reviewDoor === false) return false;
|
|
238
|
+
return REVIEW_DOOR_CLASSES.includes(job?.verdictType ?? 'green');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The stages an `accept` re-runs: the MECHANICAL ones, and never a judged floor
|
|
243
|
+
* or a person (`SEED_EXEMPT_KINDS`, ruling 8's set, for the same reason it was
|
|
244
|
+
* drawn — a machine may re-prove a machine's reading, and may not re-prove a
|
|
245
|
+
* judgment). hamr's N4 ruling: an accept re-runs the mechanical stages, because a
|
|
246
|
+
* door lives 60 days and nothing freezes a tree.
|
|
247
|
+
* @param {any[]|null|undefined} stages
|
|
248
|
+
* @returns {any[]}
|
|
249
|
+
*/
|
|
250
|
+
export function mechanicalStages(stages) {
|
|
251
|
+
return (Array.isArray(stages) ? stages : []).filter((s) => !SEED_EXEMPT_KINDS.includes(s?.kind));
|
|
252
|
+
}
|
|
140
253
|
|
|
141
254
|
/**
|
|
142
255
|
* @typedef {{code: string, path: string, detail: string}} Red
|
|
143
256
|
* @typedef {{workdir: string, seedRef: string, gapKeep: string,
|
|
144
257
|
* timeoutMsDefault?: number, baselineMode?: 'auto'|'worktree',
|
|
145
|
-
* gapCap?: number, maxBuffer?: number, seedTrees?: SeedTrees
|
|
258
|
+
* gapCap?: number, maxBuffer?: number, seedTrees?: SeedTrees,
|
|
259
|
+
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
260
|
+
* judgeLoop?: (o: {system: string}) => any,
|
|
261
|
+
* onJudgeCost?: (c: {stage: string|null, kind: string|null, path: string, attempt: number,
|
|
262
|
+
* label: string, model: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
263
|
+
* onStage?: (row: {stage: string|null, kind: string|null, verdict: string, durationMs: number}) => void}} Ctx
|
|
146
264
|
* The arbiter's half of a stage run. Every field here is operator/runner
|
|
147
265
|
* territory: a declaration parameterises kinds, never the contracts.
|
|
148
|
-
*
|
|
266
|
+
* `humanRuling` is the SIGNER's answer at a hitl pause, carried in from the
|
|
267
|
+
* runner — never authored, never defaulted, and absent on every ordinary run.
|
|
268
|
+
* `judgeLoop` is the PAID seam the judged stage runs its locate call through, and
|
|
269
|
+
* it is operator territory for the same reason the worker's provider is: this
|
|
270
|
+
* module owns no provider and never picks one, and the judge tier is PINNED
|
|
271
|
+
* (`JUDGE_MODEL`) rather than chosen. Absent is not a fall-back to some other
|
|
272
|
+
* model — the stage stops as a wiring gap, the same answer `runPlan` gives a
|
|
273
|
+
* native job with no native factory.
|
|
274
|
+
* `onJudgeCost` is the METER, and it is the only reason a judged stage can be
|
|
275
|
+
* funded honestly: this executor cannot reach the run's ledger, so every locate
|
|
276
|
+
* call's cost is reported OUT through it, once per call, on every route including
|
|
277
|
+
* the red ones — a paid call that leaves no meter record is F12 in a judge's coat.
|
|
278
|
+
* `onStage` is a REPORTING seam and nothing else: the seed read spawns a real
|
|
279
|
+
* toolchain per stage (a suite is the slow one) and a whole read is one opaque
|
|
280
|
+
* await, so the shell gets told as each stage lands. It decides nothing, it is
|
|
281
|
+
* never consulted, and no verdict depends on it — the house pattern is
|
|
282
|
+
* `runJob`'s `emit` (the library reports, the shell prints).
|
|
283
|
+
* @typedef {{verdict: 'green'|'red'|'instrument-stop'|'not-reached'|'pause'|'skipped',
|
|
149
284
|
* exitCode: number|null, value: number|null, baseline: number|null,
|
|
150
285
|
* baselineSource: string|null, gapLines: string[], judged: boolean,
|
|
151
286
|
* stage: string|null, kind: string|null, detail: Record<string, any>}} StageResult
|
|
287
|
+
* Six outcomes, and only two of them are verdicts. `pause` is N4's fifth: the
|
|
288
|
+
* close reached a stage a machine cannot render and is WAITING on a person —
|
|
289
|
+
* neither green nor red, and never graded (the F17 forbidden-zone rule applied
|
|
290
|
+
* to a non-verdict). `skipped` is the sixth and says a stage was deliberately
|
|
291
|
+
* not run at THIS reading (ruling 8's seed exemption), which is a different
|
|
292
|
+
* fact from `not-reached` (an earlier stage decided first).
|
|
152
293
|
* @typedef {{ensure: (workdir: string, seedRef: string) => Promise<{stop: string, fault: string}|{stop: null, dir: string}>,
|
|
153
294
|
* cleanup: () => Promise<{ok: boolean, leaked: string[], detail: string|null}>}} SeedTrees
|
|
154
295
|
*/
|
|
@@ -1323,12 +1464,397 @@ async function runFilesChanged(stage, ctx) {
|
|
|
1323
1464
|
});
|
|
1324
1465
|
}
|
|
1325
1466
|
|
|
1467
|
+
/**
|
|
1468
|
+
* THE SIGNER'S ANSWER, normalised at the seam that accepts it — the library half
|
|
1469
|
+
* of the three-button gate (2026-08-12 §1).
|
|
1470
|
+
*
|
|
1471
|
+
* `null` is not a refusal: no ruling at all IS the pause path, which is how every
|
|
1472
|
+
* ordinary run and every first leg of a hitl run arrives here. What is refused is
|
|
1473
|
+
* an answer that is not one of the three doors, and — the case the POC's negative
|
|
1474
|
+
* control measured — a `rerun` with no text. Ruling 3 makes the human's words the
|
|
1475
|
+
* gap the worker converts from; an empty one launches a fix worker that re-runs
|
|
1476
|
+
* as though nothing had been said, which is the failure this refusal exists for.
|
|
1477
|
+
*
|
|
1478
|
+
* It never throws and never repairs: a decision that cannot be read is handed
|
|
1479
|
+
* back as a refusal with its reason, and the caller decides what to do about it.
|
|
1480
|
+
* @param {any} ruling
|
|
1481
|
+
* @returns {{ok: boolean, ruling: {decision: string, text: string|null}|null, why: string|null}}
|
|
1482
|
+
*/
|
|
1483
|
+
export function normalizeHumanRuling(ruling) {
|
|
1484
|
+
if (ruling === null || ruling === undefined) return { ok: true, ruling: null, why: null };
|
|
1485
|
+
if (typeof ruling !== 'object' || Array.isArray(ruling)) {
|
|
1486
|
+
return { ok: false, ruling: null, why: `a decision is an object {decision, text?} — one of ${HUMAN_DECISIONS.join(' | ')}` };
|
|
1487
|
+
}
|
|
1488
|
+
const decision = String(/** @type {any} */ (ruling).decision);
|
|
1489
|
+
if (!HUMAN_DECISIONS.includes(decision)) {
|
|
1490
|
+
return {
|
|
1491
|
+
ok: false,
|
|
1492
|
+
ruling: null,
|
|
1493
|
+
why: `"${decision}" is not one of the three doors (${HUMAN_DECISIONS.join(' | ')}). There is no fourth, and no `
|
|
1494
|
+
+ 'free-text-only variant: a red that is not one of these two is not a ruling the run can act on',
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
const text = /** @type {any} */ (ruling).text;
|
|
1498
|
+
if (decision === 'rerun') {
|
|
1499
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
1500
|
+
return {
|
|
1501
|
+
ok: false,
|
|
1502
|
+
ruling: null,
|
|
1503
|
+
why: 'a "rerun" decision needs TEXT: the human is the gap author (ruling 3), and their words are the whole of '
|
|
1504
|
+
+ 'what the fix worker is given. An empty gap re-runs the worker as though nothing had been said',
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
return { ok: true, ruling: { decision, text }, why: null };
|
|
1508
|
+
}
|
|
1509
|
+
return { ok: true, ruling: { decision, text: null }, why: null };
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* WHICH ANSWER IS THIS LEG ACTING ON — the fresh one, or the one the checkpoint
|
|
1514
|
+
* held? F102's cure needs one seam that can say, because two callers ask the same
|
|
1515
|
+
* question (`runJob` declares the fold on `job-start` before `runPlan` reads the
|
|
1516
|
+
* ruling at all), and two spellings of "is this a fresh engagement" is how the
|
|
1517
|
+
* record and the clock come to disagree about the same leg.
|
|
1518
|
+
*
|
|
1519
|
+
* THE AMBIGUITY IS REFUSED, NEVER MERGED. A checkpoint that holds a decision and a
|
|
1520
|
+
* command line that carries another are two answers to one question. Merging them
|
|
1521
|
+
* picks a winner nobody chose; overriding silently discards words a person typed
|
|
1522
|
+
* (the F98/F28 class at a process boundary). So it comes back as a refusal that
|
|
1523
|
+
* NAMES the held decision, and the operator resolves it — by answering the held
|
|
1524
|
+
* one, or by starting a fresh run against the tree as it stands.
|
|
1525
|
+
*
|
|
1526
|
+
* `fresh` is F103's half: a `rerun` is a FRESH ENGAGEMENT (design record §3.4 —
|
|
1527
|
+
* *"redo/rerun comes with new authoring for money+time and keeps accounting of
|
|
1528
|
+
* this far and this session separate counters"*), and nothing else is. An `accept`
|
|
1529
|
+
* commissions no work and a `pause` runs nothing, so neither opens one.
|
|
1530
|
+
*
|
|
1531
|
+
* A REVIEW DOOR's rerun (module 8) is the SAME fact arriving one level out, and it
|
|
1532
|
+
* is folded in here rather than beside the clock, because "is this a fresh
|
|
1533
|
+
* engagement" gets ONE spelling — a second one is how a record and a clock come to
|
|
1534
|
+
* disagree about the same leg, which is the very thing this resolver exists to
|
|
1535
|
+
* prevent. It is deliberately NOT read as a ruling: a `humanRuling` answers a
|
|
1536
|
+
* close's human STAGE and is refused for a close that has none, so the door's words
|
|
1537
|
+
* stay in their own parameter and only this reading is shared.
|
|
1538
|
+
*
|
|
1539
|
+
* PRECEDENCE, stated because both CAN be handed to one leg: a door rerun makes the
|
|
1540
|
+
* engagement fresh whatever the ruling says, and the two are OR'd, never merged.
|
|
1541
|
+
* Freshness is the FAIL-SAFE direction — a fresh clock costs a leg nothing, while
|
|
1542
|
+
* inheriting a dead leg's wall is F103's measured, structurally-impossible
|
|
1543
|
+
* engagement. The door's own validation (empty words are refused) stays where it
|
|
1544
|
+
* is, at the seam that accepts it.
|
|
1545
|
+
* @param {any} fresh the answer arriving on this invocation (`--decide`), or null
|
|
1546
|
+
* @param {any} held the answer the CHECKPOINT carries (`readResume`'s
|
|
1547
|
+
* `restart.pendingDecision`), or null. `receivedAt` is when the PERSON said it —
|
|
1548
|
+
* carried through so the resumed leg records the receipt, not its own re-reading.
|
|
1549
|
+
* @param {any} [doorRerun] MODULE 8 — the review door's rerun, when this leg IS the
|
|
1550
|
+
* fresh engagement a signer commissioned at a previous run's door. Presence is the
|
|
1551
|
+
* whole of what is read here; the words are the accepting seam's business.
|
|
1552
|
+
* @returns {{ok: boolean, why: string|null, ruling: {decision: string, text: string|null}|null,
|
|
1553
|
+
* source: 'operator'|'checkpoint'|null, receivedAt: string|null, fresh: boolean}}
|
|
1554
|
+
*/
|
|
1555
|
+
export function resolveHumanRuling(fresh, held, doorRerun = null) {
|
|
1556
|
+
const has = (/** @type {any} */ v) => v !== null && v !== undefined;
|
|
1557
|
+
const atDoor = has(doorRerun);
|
|
1558
|
+
const none = { ruling: null, source: /** @type {any} */ (null), receivedAt: null, fresh: atDoor };
|
|
1559
|
+
if (has(fresh) && has(held)) {
|
|
1560
|
+
const d = typeof held === 'object' && !Array.isArray(held) ? String(/** @type {any} */ (held).decision) : String(held);
|
|
1561
|
+
return {
|
|
1562
|
+
ok: false,
|
|
1563
|
+
why: `this checkpoint already holds a "${d}" decision that no round was ever bought for, and a second answer `
|
|
1564
|
+
+ 'arrived with this invocation. Two answers to one question is ambiguity, not a merge: resume with no '
|
|
1565
|
+
+ 'decision to act on the held one, or start a fresh run against the tree as it stands.',
|
|
1566
|
+
...none,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const raw = has(fresh) ? fresh : held;
|
|
1570
|
+
const norm = normalizeHumanRuling(raw ?? null);
|
|
1571
|
+
if (!norm.ok) return { ok: false, why: norm.why, ...none };
|
|
1572
|
+
if (norm.ruling === null) return { ok: true, why: null, ...none };
|
|
1573
|
+
const at = has(held) ? /** @type {any} */ (held).receivedAt : null;
|
|
1574
|
+
return {
|
|
1575
|
+
ok: true,
|
|
1576
|
+
why: null,
|
|
1577
|
+
ruling: norm.ruling,
|
|
1578
|
+
source: has(fresh) ? 'operator' : 'checkpoint',
|
|
1579
|
+
receivedAt: typeof at === 'string' && at.length > 0 ? at : null,
|
|
1580
|
+
// OR'd, never merged: either door the person came through commissions work
|
|
1581
|
+
fresh: atDoor || norm.ruling.decision === 'rerun',
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* THE HUMAN STAGE (N4 slice 1) — the one kind that measures nothing.
|
|
1587
|
+
*
|
|
1588
|
+
* With no ruling in hand it does not RUN, it PAUSES: the fifth outcome, neither
|
|
1589
|
+
* green nor red, so nothing downstream grades it. With the signer's ruling in
|
|
1590
|
+
* hand it renders exactly what the person said — `accept` is a green a PERSON
|
|
1591
|
+
* judged, and `rerun` is a red whose gap is the human's own words, verbatim,
|
|
1592
|
+
* through the same bounded/announced channel every other stage's gap uses.
|
|
1593
|
+
*
|
|
1594
|
+
* It spawns nothing, reads nothing and touches no tree, which is why it can
|
|
1595
|
+
* render both without a workdir that exists.
|
|
1596
|
+
*
|
|
1597
|
+
* `pause` is deliberately NOT a case here. It is not a judgment about the tree
|
|
1598
|
+
* — it is the signer saying "not now" — and it is consumed above this stage, by
|
|
1599
|
+
* the runner and by `runPlan` before the close is ever asked (a `pause` that
|
|
1600
|
+
* reached this far has bypassed that seam, so it renders NO verdict rather than
|
|
1601
|
+
* being guessed into one). With no ruling in hand this kind pauses anyway, which
|
|
1602
|
+
* is the same place the person is left; what the door adds is the RECORD that
|
|
1603
|
+
* they looked and chose to keep the checkpoint.
|
|
1604
|
+
* @param {any} stage @param {Ctx} ctx @returns {Promise<StageResult>}
|
|
1605
|
+
*/
|
|
1606
|
+
async function runHumanConfirms(stage, ctx) {
|
|
1607
|
+
const ask = stage?.params?.ask;
|
|
1608
|
+
const norm = normalizeHumanRuling(ctx.humanRuling ?? null);
|
|
1609
|
+
if (!norm.ok) {
|
|
1610
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" was handed a decision it cannot read — ${norm.why}`,
|
|
1611
|
+
{}, STOP_FAULTS.CRASHED);
|
|
1612
|
+
}
|
|
1613
|
+
if (norm.ruling === null) {
|
|
1614
|
+
// THE PAUSE. `judged: false` and `exitCode: null` together are the whole
|
|
1615
|
+
// contract: nothing was measured and no process ran, so a zero here would be
|
|
1616
|
+
// F6 in an exit code's coat.
|
|
1617
|
+
return result(stage, {
|
|
1618
|
+
verdict: 'pause',
|
|
1619
|
+
exitCode: null,
|
|
1620
|
+
judged: false,
|
|
1621
|
+
detail: { ask: isNonEmptyString(ask) ? ask : null },
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
if (norm.ruling.decision === 'accept') {
|
|
1625
|
+
return result(stage, { verdict: 'green', exitCode: EXIT_GREEN, judged: true, detail: { ask: ask ?? null, decision: 'accept' } });
|
|
1626
|
+
}
|
|
1627
|
+
if (norm.ruling.decision === 'rerun') {
|
|
1628
|
+
const gap = new Gap(ctx.gapKeep, ctx.gapCap);
|
|
1629
|
+
gap.push(`${stage.name}: the signer reviewed the result and asked for another pass. Their words:`);
|
|
1630
|
+
gap.push(/** @type {string} */ (norm.ruling.text));
|
|
1631
|
+
return result(stage, {
|
|
1632
|
+
verdict: 'red',
|
|
1633
|
+
exitCode: EXIT_RED,
|
|
1634
|
+
judged: true,
|
|
1635
|
+
gapLines: gap.render(),
|
|
1636
|
+
detail: { ask: ask ?? null, decision: 'rerun' },
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" was handed a "${norm.ruling.decision}" decision, which `
|
|
1640
|
+
+ 'is the signer ending the run and is never a verdict about the tree — the runner consumes it before the close',
|
|
1641
|
+
{}, STOP_FAULTS.CRASHED);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* THE JUDGED FLOOR (softgreen module 2) — the one kind that BUYS its measurement.
|
|
1646
|
+
*
|
|
1647
|
+
* It is `src/judged.js`'s two halves wired into the close, and the split is the
|
|
1648
|
+
* whole safety argument: LOCATE is a pinned, toolless model asked for facts with
|
|
1649
|
+
* addresses over the AUTHORITATIVE artifact, and DECIDE is a pure arbiter-owned
|
|
1650
|
+
* function with no model in it. The judge never renders a verdict; this runner
|
|
1651
|
+
* renders it, from an owned rulebook, over quotes the artifact must contain.
|
|
1652
|
+
*
|
|
1653
|
+
* FOUR ROUTES OUT, and which is which is the load-bearing part:
|
|
1654
|
+
*
|
|
1655
|
+
* - GREEN / RED come from `decide()` alone. A red is CONTENT — facts in hand,
|
|
1656
|
+
* and a card item those facts do not satisfy (or cannot be sure about, since
|
|
1657
|
+
* unsure is red). That is evidence about the tree.
|
|
1658
|
+
* - AN INSTRUMENT STOP is everything else: a card this arbiter cannot read, an
|
|
1659
|
+
* artifact that will not open, no judge seam wired, and — after the ladder —
|
|
1660
|
+
* a judge whose emission never parsed or whose call never came back.
|
|
1661
|
+
* Contract (a) names "unreadable or unparseable required output" as a stop in
|
|
1662
|
+
* so many words, and the reason is the casualty rule: our judge producing
|
|
1663
|
+
* garbage says nothing whatsoever about the worker, and grading a tree red on
|
|
1664
|
+
* it would be manufactured evidence (F45).
|
|
1665
|
+
* - A `pricing-red` locate is that same stop with the retry REFUSED: an
|
|
1666
|
+
* unpriced call cannot be summed, and buying a second one only adds more
|
|
1667
|
+
* spend nobody can see (F6).
|
|
1668
|
+
*
|
|
1669
|
+
* The gap is ITEMIZED (ruling 7, F38/F39): named card items, per-file, with the
|
|
1670
|
+
* function names and the instrument's own quotes — never one paragraph. It rides
|
|
1671
|
+
* the same `Gap` bound as every other kind, so the trim is announced.
|
|
1672
|
+
*
|
|
1673
|
+
* EVERY path is judged, not just up to the first red, for the same reason
|
|
1674
|
+
* `decide()` grades every item: calibration reads itemized reds, and a stage that
|
|
1675
|
+
* short-circuits reports the first thing it found rather than what is true.
|
|
1676
|
+
* `firstRed` still names the deciding item in DECLARED order, so the verdict's
|
|
1677
|
+
* reason is stable against whatever order a model emitted anything in.
|
|
1678
|
+
*
|
|
1679
|
+
* NOT WIRED, AND NAMED RATHER THAN SILENTLY SKIPPED — `judgeToAnnotation` →
|
|
1680
|
+
* `gate.annotate` (the slice-2 obligation list, item 4). Two independent reasons,
|
|
1681
|
+
* either of which alone would defer it:
|
|
1682
|
+
* (1) THE ADAPTER TAKES A VERDICT. bare-agent's `judgeToAnnotation` maps a
|
|
1683
|
+
* `judge()` verdict (`{verdict, where:{field, stated, returned}}`) onto the
|
|
1684
|
+
* annotation shape, and this pipe deliberately never buys a verdict from a
|
|
1685
|
+
* model — LOCATE returns facts, and the verdict is `decide()`'s. Feeding it
|
|
1686
|
+
* a synthesised verdict would annotate the gate with a judgement no judge
|
|
1687
|
+
* made, which is the one thing the locate-not-verdict split exists to stop.
|
|
1688
|
+
* (2) THERE IS NO GATE HERE. A Gate is built per WORKER step (`mkWorker`,
|
|
1689
|
+
* src/planrun.js) and is gone by the time the close runs; the close is
|
|
1690
|
+
* deliberately outside every worker's fence. Annotating "the arbiter's own
|
|
1691
|
+
* gate" needs a run-scoped gate that does not exist today, and inventing
|
|
1692
|
+
* one to carry an annotation would be plumbing built for its own sake.
|
|
1693
|
+
* The facts themselves are NOT lost meanwhile: every per-path reading (verdict,
|
|
1694
|
+
* itemized reds, the ladder, the scrubbed raw) rides `detail.perPath`.
|
|
1695
|
+
* @param {any} stage @param {Ctx} ctx @returns {Promise<StageResult>}
|
|
1696
|
+
*/
|
|
1697
|
+
async function runJudgedFloor(stage, ctx) {
|
|
1698
|
+
const p = stage.params;
|
|
1699
|
+
// 1. the CARD, through the one spelling (`validateCard`). A card this arbiter
|
|
1700
|
+
// cannot read is a broken close, never a red about the tree — and it is
|
|
1701
|
+
// checked before the seam and before the artifact, because it costs nothing.
|
|
1702
|
+
const cv = validateCard(p?.card);
|
|
1703
|
+
if (!cv.ok) {
|
|
1704
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" carries a rubric card this arbiter cannot read: ${cv.reds.join('; ')}`,
|
|
1705
|
+
{}, STOP_FAULTS.CRASHED);
|
|
1706
|
+
}
|
|
1707
|
+
const paths = Array.isArray(p?.paths) ? p.paths.filter(isNonEmptyString) : [];
|
|
1708
|
+
if (paths.length === 0) {
|
|
1709
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" names no artifact to judge — a judged stage with no `
|
|
1710
|
+
+ 'paths judges nothing, and nothing is not a pass', {}, STOP_FAULTS.CRASHED);
|
|
1711
|
+
}
|
|
1712
|
+
if (paths.length > MAX_JUDGED_PATHS) {
|
|
1713
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" names ${paths.length} artifacts, over the ceiling of `
|
|
1714
|
+
+ `${MAX_JUDGED_PATHS} — a judged stage buys one call per artifact, and a runaway list is a bill`, {}, STOP_FAULTS.CRASHED);
|
|
1715
|
+
}
|
|
1716
|
+
// 1b. THE TIER THE SIGNED SET WAS GRADED BY, against the tier this run would buy.
|
|
1717
|
+
// `JUDGE_MODEL` has always SAID a bump forces a full recalibration; this is the
|
|
1718
|
+
// detector, because a rule with no wired detector is prose (F45). The stamp comes
|
|
1719
|
+
// off the SIGNED calibration (`calibration.judgeModel`, carried across by
|
|
1720
|
+
// `declaredStages`), so a mismatch means exactly one thing: the pin moved under a
|
|
1721
|
+
// signature, and the floor standing behind this stage was certified by a model
|
|
1722
|
+
// nobody is about to call. That is a FAILED wiring gap and not a crash — the same
|
|
1723
|
+
// fault, and for the same reason, as the absent seam below: nothing is broken,
|
|
1724
|
+
// something was never re-done. It stops rather than degrading in either direction,
|
|
1725
|
+
// because grading on the new tier would mint a verdict against an uncalibrated
|
|
1726
|
+
// floor and grading on the old one is not on offer (the pin is a constant).
|
|
1727
|
+
// AN ABSENT STAMP IS NOT CHECKED HERE, deliberately and for the reason the SET's
|
|
1728
|
+
// own presence is not checked in `validateCloseDecl`: a spec that judges cannot
|
|
1729
|
+
// reach a run without a calibration (the signing gate, module 5) or without a
|
|
1730
|
+
// model on it (`validateCloseDecl`), so the runtime question is only ever WHICH
|
|
1731
|
+
// model — and a stage descriptor handed straight to `runStage` by an adopter or a
|
|
1732
|
+
// test is not a signed spec making a claim about a tier.
|
|
1733
|
+
if (isNonEmptyString(stage.calibrationJudgeModel) && stage.calibrationJudgeModel !== JUDGE_MODEL) {
|
|
1734
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" would grade with ${JUDGE_MODEL}, and the calibration `
|
|
1735
|
+
+ `set signed with this close was graded by ${stage.calibrationJudgeModel} — the judge tier moved under the `
|
|
1736
|
+
+ '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 tier and re-run the calibration gate (recalibrate)',
|
|
1738
|
+
{ signedJudgeModel: stage.calibrationJudgeModel, judgeModel: JUDGE_MODEL }, STOP_FAULTS.FAILED);
|
|
1739
|
+
}
|
|
1740
|
+
if (typeof ctx.judgeLoop !== 'function') {
|
|
1741
|
+
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 ${JUDGE_MODEL}, pinned, and a `
|
|
1743
|
+
+ 'stage that quietly graded on whatever model was lying around would be a floor nobody calibrated',
|
|
1744
|
+
{}, STOP_FAULTS.FAILED);
|
|
1745
|
+
}
|
|
1746
|
+
const loopFactory = /** @type {(o: {system: string}) => any} */ (ctx.judgeLoop);
|
|
1747
|
+
|
|
1748
|
+
const gap = new Gap(ctx.gapKeep, ctx.gapCap);
|
|
1749
|
+
/** @type {any[]} */
|
|
1750
|
+
const perPath = [];
|
|
1751
|
+
/** @type {{path: string, rule: string, text: string, reds: any[]}[]} */
|
|
1752
|
+
const redItems = [];
|
|
1753
|
+
/** the reds that name NO card item: `decide()`'s unsure routes (no usable facts,
|
|
1754
|
+
* an empty function list). They are the fail-safe itself, so the verdict is read
|
|
1755
|
+
* off the per-path verdicts and NEVER off `redItems` — a red with nothing itemized
|
|
1756
|
+
* under it is exactly the shape that would otherwise fall through to green. */
|
|
1757
|
+
/** @type {{path: string, reason: string}[]} */
|
|
1758
|
+
const unsure = [];
|
|
1759
|
+
let redFiles = 0;
|
|
1760
|
+
|
|
1761
|
+
for (const rel of paths) {
|
|
1762
|
+
let artifactText;
|
|
1763
|
+
try {
|
|
1764
|
+
artifactText = await readFile(join(ctx.workdir, rel), 'utf8');
|
|
1765
|
+
} catch (e) {
|
|
1766
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}" could not read the artifact it judges (${rel}): `
|
|
1767
|
+
+ `${String(/** @type {any} */ (e)?.message ?? e)}`, { path: rel }, STOP_FAULTS.CRASHED);
|
|
1768
|
+
}
|
|
1769
|
+
if (artifactText.trim() === '') {
|
|
1770
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}"'s artifact ${rel} is empty — an empty artifact `
|
|
1771
|
+
+ 'yields no facts, and no facts is unknown, never a pass', { path: rel }, STOP_FAULTS.CRASHED);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** @type {any} */
|
|
1775
|
+
let last = null;
|
|
1776
|
+
/** @type {any[]} */
|
|
1777
|
+
const tries = [];
|
|
1778
|
+
for (let attempt = 1; attempt <= JUDGE_ATTEMPTS; attempt += 1) {
|
|
1779
|
+
last = await runLocate({
|
|
1780
|
+
artifactText,
|
|
1781
|
+
card: p.card,
|
|
1782
|
+
loopFactory,
|
|
1783
|
+
attempt,
|
|
1784
|
+
onCost: (c) => {
|
|
1785
|
+
if (typeof ctx.onJudgeCost !== 'function') return;
|
|
1786
|
+
ctx.onJudgeCost({
|
|
1787
|
+
stage: stage?.name ?? null, kind: stage?.kind ?? null, path: rel, attempt,
|
|
1788
|
+
label: LOCATE_LABEL, model: JUDGE_MODEL, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
1789
|
+
});
|
|
1790
|
+
},
|
|
1791
|
+
});
|
|
1792
|
+
tries.push({ attempt, ok: last.ok, axis: last.red?.axis ?? null, costUsd: last.costUsd, truncated: last.truncated, parseError: last.parseError });
|
|
1793
|
+
if (last.ok) break;
|
|
1794
|
+
// the ONE non-retryable axis, and it outranks the ladder: a retry of an
|
|
1795
|
+
// unpriced call buys more spend nobody can see (F6).
|
|
1796
|
+
if (last.red.axis === LOCATE_AXES.PRICING) break;
|
|
1797
|
+
}
|
|
1798
|
+
if (!last.ok) {
|
|
1799
|
+
return stopped(stage, ctx, `INSTRUMENT: stage "${stage.name}"'s judge returned no usable facts for ${rel} after `
|
|
1800
|
+
+ `${tries.length} attempt(s) [${last.red.axis}]: ${last.red.detail}`,
|
|
1801
|
+
{ path: rel, axis: last.red.axis, attempts: tries }, STOP_FAULTS.CRASHED);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
const d = decide(last.facts, p.card, { artifactText });
|
|
1805
|
+
perPath.push({ path: rel, verdict: d.verdict, firstRed: d.firstRed, attempts: tries, raw: last.raw, items: d.items });
|
|
1806
|
+
if (d.verdict === 'red') {
|
|
1807
|
+
redFiles += 1;
|
|
1808
|
+
for (const it of d.items) if (!it.ok) redItems.push({ path: rel, rule: it.rule, text: it.text, reds: it.reds });
|
|
1809
|
+
if (!d.items.some((it) => !it.ok)) unsure.push({ path: rel, reason: String(d.reason ?? 'unsure, and unsure is red') });
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const red = redFiles > 0;
|
|
1814
|
+
if (red) {
|
|
1815
|
+
// FIRST-RED-WINS in the CARD's own signed order, across every artifact: the
|
|
1816
|
+
// card is the arbiter's order, and the file loop's order is the declaration's.
|
|
1817
|
+
const order = p.card.items.map((/** @type {any} */ it) => it.rule);
|
|
1818
|
+
const firstRed = order.find((/** @type {string} */ r) => redItems.some((x) => x.rule === r))
|
|
1819
|
+
?? (redItems[0]?.rule ?? 'unsure');
|
|
1820
|
+
gap.push(`${stage.name}: the judged floor is not met — ${redItems.length} card item(s) red across ${redFiles} of `
|
|
1821
|
+
+ `${paths.length} artifact(s); first red: ${firstRed}`);
|
|
1822
|
+
for (const u of unsure) gap.push(` ${u.path}: ${u.reason}`);
|
|
1823
|
+
for (const it of redItems) {
|
|
1824
|
+
gap.push(` [${it.rule}] ${it.text}`);
|
|
1825
|
+
// the file, the function and the quote are all things the INSTRUMENT itself
|
|
1826
|
+
// reported (contract (b) bars naming a culprit it did not) — and a red with
|
|
1827
|
+
// no address is the semantic genre F38 measured as inert
|
|
1828
|
+
for (const r of it.reds) {
|
|
1829
|
+
gap.push(` ${it.path}: ${r.fn} — ${r.why}`);
|
|
1830
|
+
if (r.quote) gap.push(` > ${String(r.quote).split('\n')[0].trim().slice(0, 200)}`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return result(stage, {
|
|
1835
|
+
verdict: red ? 'red' : 'green',
|
|
1836
|
+
exitCode: red ? EXIT_RED : EXIT_GREEN,
|
|
1837
|
+
gapLines: gap.render(),
|
|
1838
|
+
detail: {
|
|
1839
|
+
model: JUDGE_MODEL,
|
|
1840
|
+
card: p.card.items.map((/** @type {any} */ it) => it.rule),
|
|
1841
|
+
paths,
|
|
1842
|
+
perPath,
|
|
1843
|
+
redItems: redItems.length,
|
|
1844
|
+
unsure,
|
|
1845
|
+
redFiles,
|
|
1846
|
+
},
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1326
1850
|
/** @type {Record<string, (stage: any, ctx: Ctx) => Promise<StageResult>>} */
|
|
1327
1851
|
const RUNNERS = {
|
|
1328
1852
|
'command-exit': runCommandExit,
|
|
1329
1853
|
'count-not-worse': runCountNotWorse,
|
|
1330
1854
|
'pattern-absent-in-diff': runPatternAbsentInDiff,
|
|
1331
1855
|
'files-changed': runFilesChanged,
|
|
1856
|
+
'human-confirms': runHumanConfirms,
|
|
1857
|
+
'judged-floor': runJudgedFloor,
|
|
1332
1858
|
};
|
|
1333
1859
|
|
|
1334
1860
|
/**
|
|
@@ -1387,10 +1913,41 @@ export async function seedRead(declaration, ctx) {
|
|
|
1387
1913
|
const owned = !ctx.seedTrees;
|
|
1388
1914
|
const seedTrees = ctx.seedTrees ?? makeSeedTrees();
|
|
1389
1915
|
const inner = { ...ctx, seedTrees };
|
|
1916
|
+
/** AFTER the stage completes, never before: a line saying a stage STARTED and
|
|
1917
|
+
* a line saying what it RETURNED are two different facts, and only the second
|
|
1918
|
+
* one can carry a verdict. The duration is measured around the call rather
|
|
1919
|
+
* than reported by it, so a stage that stops still gets a real number.
|
|
1920
|
+
* @param {StageResult} row @param {number} startedAt */
|
|
1921
|
+
const told = (row, startedAt) => {
|
|
1922
|
+
if (typeof ctx.onStage === 'function') {
|
|
1923
|
+
ctx.onStage({ stage: row.stage, kind: row.kind, verdict: row.verdict, durationMs: Date.now() - startedAt });
|
|
1924
|
+
}
|
|
1925
|
+
return row;
|
|
1926
|
+
};
|
|
1390
1927
|
try {
|
|
1391
1928
|
/** @type {StageResult[]} */
|
|
1392
1929
|
const rows = [];
|
|
1393
|
-
for (const stage of declaration?.stages ?? [])
|
|
1930
|
+
for (const stage of declaration?.stages ?? []) {
|
|
1931
|
+
// RULING 8 — judged and human stages are EXEMPT from the seed read, and
|
|
1932
|
+
// the exemption is a ROW, not an absence (F59). A person asked at the seed
|
|
1933
|
+
// is being interviewed, not measured, and there is nothing to baseline.
|
|
1934
|
+
const startedAt = Date.now();
|
|
1935
|
+
if (SEED_EXEMPT_KINDS.includes(stage?.kind)) {
|
|
1936
|
+
rows.push(told(result(stage, {
|
|
1937
|
+
verdict: 'skipped',
|
|
1938
|
+
exitCode: null,
|
|
1939
|
+
judged: false,
|
|
1940
|
+
detail: {
|
|
1941
|
+
skipped: true,
|
|
1942
|
+
why: 'ruling 8 (2026-08-07): judged and human stages skip the seed-verdict read — a judged number at seed '
|
|
1943
|
+
+ 'is unstable and a person at seed is an interview, not a measurement. This stage has no seed baseline '
|
|
1944
|
+
+ 'and contributes none.',
|
|
1945
|
+
},
|
|
1946
|
+
}), startedAt));
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
rows.push(told(await runStageIn(stage, inner), startedAt));
|
|
1950
|
+
}
|
|
1394
1951
|
return rows;
|
|
1395
1952
|
} finally {
|
|
1396
1953
|
if (owned) await seedTrees.cleanup();
|
|
@@ -1412,7 +1969,7 @@ export async function seedRead(declaration, ctx) {
|
|
|
1412
1969
|
* room would break a documented adopter contract for nothing; the new name says
|
|
1413
1970
|
* exactly what differs. `src/declaredclose.js` is what bridges the two shapes.
|
|
1414
1971
|
* @param {any} declaration @param {Ctx} ctx
|
|
1415
|
-
* @returns {Promise<{verdict: 'green'|'red'|'instrument-stop', exitCode: number, judged: boolean, firstRed: string|null, stages: StageResult[]}>}
|
|
1972
|
+
* @returns {Promise<{verdict: 'green'|'red'|'instrument-stop'|'pause', exitCode: number|null, judged: boolean, firstRed: string|null, pausedAt: string|null, stages: StageResult[]}>}
|
|
1416
1973
|
*/
|
|
1417
1974
|
export async function runDeclaredClose(declaration, ctx) {
|
|
1418
1975
|
const owned = !ctx.seedTrees;
|
|
@@ -1431,12 +1988,20 @@ export async function runDeclaredClose(declaration, ctx) {
|
|
|
1431
1988
|
stages.push(r);
|
|
1432
1989
|
if (r.verdict !== 'green') ended = true;
|
|
1433
1990
|
}
|
|
1991
|
+
// A PAUSE stops the close exactly like a red does, and is DELIBERATELY not
|
|
1992
|
+
// in this list: `deciding` names the stage that rendered a VERDICT, and a
|
|
1993
|
+
// pause rendered none. Reading it as one would fall through to green — the
|
|
1994
|
+
// close would mint a verdict nobody gave (the defect this line exists to
|
|
1995
|
+
// make impossible).
|
|
1434
1996
|
const deciding = stages.find((s) => s.verdict === 'red' || s.verdict === 'instrument-stop');
|
|
1997
|
+
const paused = deciding ? null : stages.find((s) => s.verdict === 'pause') ?? null;
|
|
1435
1998
|
return {
|
|
1436
|
-
verdict: /** @type {'green'|'red'|'instrument-stop'} */ (deciding ? deciding.verdict : 'green'),
|
|
1437
|
-
exitCode: deciding ? /** @type {number} */ (deciding.exitCode) : EXIT_GREEN,
|
|
1438
|
-
judged: deciding ? deciding.judged : true,
|
|
1999
|
+
verdict: /** @type {'green'|'red'|'instrument-stop'|'pause'} */ (deciding ? deciding.verdict : (paused ? 'pause' : 'green')),
|
|
2000
|
+
exitCode: deciding ? /** @type {number} */ (deciding.exitCode) : (paused ? null : EXIT_GREEN),
|
|
2001
|
+
judged: deciding ? deciding.judged : (paused ? false : true),
|
|
1439
2002
|
firstRed: deciding ? deciding.stage : null,
|
|
2003
|
+
/** the stage the close is WAITING on, when it is waiting on a person */
|
|
2004
|
+
pausedAt: paused ? paused.stage : null,
|
|
1440
2005
|
stages,
|
|
1441
2006
|
};
|
|
1442
2007
|
} finally {
|