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,246 @@
|
|
|
1
|
+
// THE REVIEW DOOR — the signer's three doors at the END of a run (softgreen
|
|
2
|
+
// module 8; PRD v1.71 §3, design record 2026-08-17 §3 + the 2026-08-18 addenda).
|
|
3
|
+
//
|
|
4
|
+
// THE LAW, hamr verbatim: *"it's important not to change the loop self verdict."*
|
|
5
|
+
// The close mints the verdict and the ledger records it before anything here runs.
|
|
6
|
+
// What a door records is a DISPOSITION — what the person wants done about a
|
|
7
|
+
// verdict, never what the verdict is. Nothing in this file returns, writes or
|
|
8
|
+
// implies an outcome, and the run it answers has already ended.
|
|
9
|
+
//
|
|
10
|
+
// WHY THIS IS NOT `runPlan`'s job. A door opens after the run is over: a person
|
|
11
|
+
// may answer minutes or days later, from a different process, and the answer
|
|
12
|
+
// cannot ride the run's own return path (module 6 named this when it left
|
|
13
|
+
// `applyDoorDecision` exported and unwired). So the door is OPENED by the run
|
|
14
|
+
// (one record on its spine, `src/planrun.js`) and ANSWERED here, off that record.
|
|
15
|
+
//
|
|
16
|
+
// THE THREE DOORS, and what each one costs:
|
|
17
|
+
// accept — the close's MECHANICAL stages re-run against the tree as it stands
|
|
18
|
+
// (hamr's N4 ruling: an accept is not a rubber stamp), and only then is
|
|
19
|
+
// the disposition recorded and — for a held judged green — the learning
|
|
20
|
+
// credit released. A red at re-run means the tree changed under the
|
|
21
|
+
// signer: the accept is REFUSED with the stage named. Never the judged
|
|
22
|
+
// or human stages: a machine may re-prove a machine's reading, and a
|
|
23
|
+
// judgment is not re-run behind the person who rendered it.
|
|
24
|
+
// rerun — a fresh engagement (§3.4). The person's words ARE the gap; empty
|
|
25
|
+
// words are refused at the same seam every other door is refused at.
|
|
26
|
+
// It re-proves nothing here — the NEW run's close is what judges.
|
|
27
|
+
// pause — not now. Nothing runs, nothing is spent, and the same three doors are
|
|
28
|
+
// open the next time somebody looks. An unanswered door expires under
|
|
29
|
+
// the existing 60-day checkpoint TTL, and that expiry IS what `cancel`
|
|
30
|
+
// used to do (doors addendum, 2026-08-18).
|
|
31
|
+
//
|
|
32
|
+
// The registry half is module 6's (`applyDoorDecision`), called from exactly one
|
|
33
|
+
// place here so a disposition cannot be recorded twice or by two spellings.
|
|
34
|
+
|
|
35
|
+
import { REVIEW_DOOR, HUMAN_DECISIONS, mechanicalStages, normalizeHumanRuling } from './kinds.js';
|
|
36
|
+
import { closeStagesOf } from './plan.js';
|
|
37
|
+
import { isDeclaredClose, runDeclaredStages, HITL_DECISION_RED } from './declaredclose.js';
|
|
38
|
+
import { runStages } from './ralph.js';
|
|
39
|
+
import { redactSecrets } from './validate.js';
|
|
40
|
+
import { applyDoorDecision, PAUSE_TTL_MS } from './reuse.js';
|
|
41
|
+
|
|
42
|
+
/** the door's own record, off the run's spine — the LAST one, because a run that
|
|
43
|
+
* ends at a door writes exactly one and a reader that guesses which is which is a
|
|
44
|
+
* reader that will one day answer the wrong question.
|
|
45
|
+
* @param {any[]} events the run's spine, parsed
|
|
46
|
+
* @returns {any|null} */
|
|
47
|
+
export function doorRecordOf(events) {
|
|
48
|
+
if (!Array.isArray(events)) return null;
|
|
49
|
+
return events.findLast?.((e) => e?.type === REVIEW_DOOR) ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* IS THE DOOR STILL OPEN? The same 60-day TTL a hitl checkpoint keeps
|
|
54
|
+
* (`PAUSE_TTL_MS`) and deliberately the same NUMBER — the door replaced `cancel`
|
|
55
|
+
* precisely because an unanswered door and an unresumed pause are one state, and
|
|
56
|
+
* two TTLs for one state is how they come to disagree.
|
|
57
|
+
*
|
|
58
|
+
* An UNREADABLE stamp refuses too: unknown is not young (F6). Reading the age is
|
|
59
|
+
* not the same as deciding on it, so this states both the answer and the arithmetic.
|
|
60
|
+
* @param {any} door the door record
|
|
61
|
+
* @param {{now?: () => number, ttlMs?: number}} [opts]
|
|
62
|
+
* @returns {{ok: boolean, ageMs: number|null, ttlMs: number, detail: string}}
|
|
63
|
+
*/
|
|
64
|
+
export function doorAgeGate(door, { now = Date.now, ttlMs = PAUSE_TTL_MS } = {}) {
|
|
65
|
+
const days = (/** @type {number} */ ms) => (ms / 86_400_000).toFixed(1);
|
|
66
|
+
const at = typeof door?.ts === 'string' ? Date.parse(door.ts) : NaN;
|
|
67
|
+
if (!Number.isFinite(at)) {
|
|
68
|
+
return { ok: false, ageMs: null, ttlMs, detail: 'the door carries no readable timestamp, and an unknown age is not a young one — the decision cannot be honoured on a record nobody can date' };
|
|
69
|
+
}
|
|
70
|
+
const ageMs = now() - at;
|
|
71
|
+
if (ageMs > ttlMs) {
|
|
72
|
+
return { ok: false, ageMs, ttlMs, detail: `the door was opened ${days(ageMs)} days ago and a door keeps for ${days(ttlMs)} days — it has expired on its own, which is exactly what nobody having to decide looks like` };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, ageMs, ttlMs, detail: `${days(ageMs)} days old, of ${days(ttlMs)}` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** one red, one spelling @param {string} code @param {string} detail @param {string} [path] */
|
|
78
|
+
const red = (code, detail, path = 'decision') => [{ code, path, detail }];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Answer a run's review door.
|
|
82
|
+
*
|
|
83
|
+
* Never throws and never conjures: a malformed decision, a run that opened no
|
|
84
|
+
* door, an expired one, a tree that moved and a registry that is not there all
|
|
85
|
+
* come back as named reds. The loop's verdict is untouched on every path — this
|
|
86
|
+
* function has no way to write one.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {any} opts.job the SIGNED spec the run was judged by (its close is what an accept re-proves)
|
|
90
|
+
* @param {string} opts.workdir the patient, as it stands NOW
|
|
91
|
+
* @param {any[]} opts.events the run's own spine, parsed
|
|
92
|
+
* @param {string} opts.decision one of the three doors
|
|
93
|
+
* @param {string|null} [opts.text] the rerun's words — the gap, and refused when empty
|
|
94
|
+
* @param {number} [opts.closeTimeoutMs] the per-stage bound for the accept's re-run
|
|
95
|
+
* @param {string|null} [opts.registryDir] the workflow registry, when the runner keeps one
|
|
96
|
+
* @param {string|null} [opts.name] the workflow this run wrote its row against
|
|
97
|
+
* @param {string|null} [opts.runid] the RUN's id, exactly as the bridge row records it
|
|
98
|
+
* @param {string|null} [opts.at] when the person answered
|
|
99
|
+
* @param {() => number} [opts.now] the clock, injected for the TTL
|
|
100
|
+
* @param {number} [opts.ttlMs]
|
|
101
|
+
* @param {((type: string, data: any) => any)|null} [opts.emit] the run's spine, to record the disposition on
|
|
102
|
+
* @returns {Promise<{ok: boolean, decision: string|null, next: string|null, released: boolean,
|
|
103
|
+
* doorRecorded: boolean, reds: any[], mechanical: any, door: any, text: string|null,
|
|
104
|
+
* options: string[], ttlMs: number, note: string|null}>}
|
|
105
|
+
*/
|
|
106
|
+
export async function answerReviewDoor({
|
|
107
|
+
job, workdir, events, decision, text = null, closeTimeoutMs,
|
|
108
|
+
registryDir = null, name = null, runid = null, at = null,
|
|
109
|
+
now = Date.now, ttlMs = PAUSE_TTL_MS, emit = null,
|
|
110
|
+
}) {
|
|
111
|
+
/** @type {any} */
|
|
112
|
+
const out = {
|
|
113
|
+
ok: false, decision: null, next: null, released: false, doorRecorded: false,
|
|
114
|
+
reds: [], mechanical: null, door: null, text: null, options: [...HUMAN_DECISIONS], ttlMs, note: null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ── 1. THE OPERATOR'S OWN INPUT, at the LIBRARY's one seam. A fourth door and
|
|
118
|
+
// an empty rerun are refused by `normalizeHumanRuling` — the same rulebook the
|
|
119
|
+
// close's human stage is answered through, so a surface cannot admit what a run
|
|
120
|
+
// refuses.
|
|
121
|
+
const norm = normalizeHumanRuling({ decision, ...(text === null ? {} : { text }) });
|
|
122
|
+
if (!norm.ok) {
|
|
123
|
+
out.reds = red(HITL_DECISION_RED, /** @type {string} */ (norm.why));
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
const ruling = /** @type {{decision: string, text: string|null}} */ (norm.ruling);
|
|
127
|
+
out.decision = ruling.decision;
|
|
128
|
+
out.text = ruling.text;
|
|
129
|
+
|
|
130
|
+
// ── 2. IS THERE A DOOR? A run that opened none has nothing to answer, and
|
|
131
|
+
// conjuring one would let a decision be recorded against a verdict nobody was
|
|
132
|
+
// ever shown the evidence for.
|
|
133
|
+
const door = doorRecordOf(events);
|
|
134
|
+
if (door === null) {
|
|
135
|
+
out.reds = red('no-door', 'this run opened no review door — its verdict stands on its own, and there is nothing here to dispose of (a green-class run opens one only when the runner asks: --review-door)', 'events');
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
out.door = door;
|
|
139
|
+
|
|
140
|
+
// ── 3. IS IT STILL OPEN? The TTL is the cancel case: nobody has to decide, and
|
|
141
|
+
// a door nobody answered simply expires.
|
|
142
|
+
const age = doorAgeGate(door, { now, ttlMs });
|
|
143
|
+
if (!age.ok) {
|
|
144
|
+
out.reds = red('door-expired', age.detail, 'door.ts');
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 4. ACCEPT RE-PROVES THE TREE. Everything above is $0 and deterministic;
|
|
149
|
+
// this is the only branch that runs anything, and it runs the close's own
|
|
150
|
+
// mechanical stages — never a second, weaker spelling of "is it still green".
|
|
151
|
+
if (out.decision === 'accept') {
|
|
152
|
+
const proof = await proveMechanically({ job, workdir, events, closeTimeoutMs });
|
|
153
|
+
out.mechanical = proof.reading;
|
|
154
|
+
if (!proof.ok) {
|
|
155
|
+
out.reds = red('door-accept-red', proof.detail, proof.reading.stage ? `stage.${proof.reading.stage}` : 'close');
|
|
156
|
+
// NOTHING IS RECORDED on a refusal. A disposition the machine would not
|
|
157
|
+
// honour is not a disposition the record should carry: the person's answer
|
|
158
|
+
// was about a tree that no longer exists.
|
|
159
|
+
if (emit) emit('door-decision', { decision: 'accept', honoured: false, stage: proof.reading.stage ?? null, reason: 'the tree moved under the signer' });
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── 5. THE DISPOSITION IS RECORDED, and only accept releases (module 6's
|
|
165
|
+
// `recordDoor`, reached through its one on-disk seam). Every door is a datum:
|
|
166
|
+
// *"the signer's accepts double as the judge's ongoing report card"* (v1.71 §3),
|
|
167
|
+
// and a rerun over a judged green is exactly as informative as an accept.
|
|
168
|
+
if (registryDir && name && runid) {
|
|
169
|
+
// ALREADY-GREEN MINTS NOTHING, from the other side: a run that changed
|
|
170
|
+
// nothing wrote no row of its own, so there is nothing to record a door on and
|
|
171
|
+
// nothing to release. Stated rather than surfaced as a `no-row-for-run` red —
|
|
172
|
+
// the rule is the design, not a failure.
|
|
173
|
+
if (door.outcome === 'already-green') {
|
|
174
|
+
out.note = 'already-green: the run minted no row of its own, so the door records nothing and releases nothing — accept confirms a verdict, it never mints one';
|
|
175
|
+
} else {
|
|
176
|
+
const r = applyDoorDecision({ registryDir, name, runid, decision: out.decision, at });
|
|
177
|
+
if (!r.ok) {
|
|
178
|
+
out.reds = r.reds;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
out.released = r.released;
|
|
182
|
+
out.doorRecorded = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
out.ok = true;
|
|
187
|
+
out.next = out.decision === 'accept' ? 'accepted' : (out.decision === 'rerun' ? 'rerun' : 'paused');
|
|
188
|
+
if (emit) {
|
|
189
|
+
emit('door-decision', {
|
|
190
|
+
decision: out.decision,
|
|
191
|
+
honoured: true,
|
|
192
|
+
...(out.text === null ? {} : { text: redactSecrets(out.text) }),
|
|
193
|
+
released: out.released,
|
|
194
|
+
recorded: out.doorRecorded,
|
|
195
|
+
meaning: 'a disposition — the verdict this run minted is untouched, and nothing here can change it',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The accept's re-proof: the close's MECHANICAL stages, run against the tree as it
|
|
203
|
+
* stands now, through the SAME executors the run itself was judged by.
|
|
204
|
+
*
|
|
205
|
+
* The declared path measures against the run's OWN seed (`close-decl.seedRef` off
|
|
206
|
+
* the spine), never a fresh one: re-basing the baseline onto today's HEAD would
|
|
207
|
+
* compare the tree to itself and turn "nothing has changed since the run" into a
|
|
208
|
+
* green for a `files-changed` stage that means the opposite. A run whose seed is
|
|
209
|
+
* not on its spine is refused rather than guessed at.
|
|
210
|
+
* @param {{job: any, workdir: string, events: any[], closeTimeoutMs?: number}} o
|
|
211
|
+
*/
|
|
212
|
+
async function proveMechanically({ job, workdir, events, closeTimeoutMs }) {
|
|
213
|
+
const stages = mechanicalStages(closeStagesOf(job) ?? []);
|
|
214
|
+
if (!stages.length) {
|
|
215
|
+
// Honest, and named: a close with no mechanical stage has nothing a machine
|
|
216
|
+
// can re-prove, so the accept rests on the person alone. Never a silent pass
|
|
217
|
+
// dressed as a re-run.
|
|
218
|
+
return { ok: true, detail: '', reading: { ran: 0, verdict: null, stage: null, stages: [], note: 'this close has no mechanical stage — there is nothing a machine can re-prove, so the accept rests on the person' } };
|
|
219
|
+
}
|
|
220
|
+
/** @type {any} */
|
|
221
|
+
let v;
|
|
222
|
+
if (isDeclaredClose(job)) {
|
|
223
|
+
const seedRef = events.findLast?.((e) => e?.type === 'close-decl' && typeof e.seedRef === 'string')?.seedRef ?? null;
|
|
224
|
+
if (!seedRef) {
|
|
225
|
+
return { ok: false, detail: 'this run left no seed on its spine (`close-decl.seedRef`), and a declared close measures against a seed — re-proving against a guessed baseline would grade a tree nobody chose', reading: { ran: 0, verdict: null, stage: null, stages: [] } };
|
|
226
|
+
}
|
|
227
|
+
v = await runDeclaredStages(stages, redactSecrets, { timeoutMs: closeTimeoutMs, cwd: workdir, seedRef });
|
|
228
|
+
} else {
|
|
229
|
+
v = await runStages(stages, redactSecrets, { timeoutMs: closeTimeoutMs, cwd: workdir });
|
|
230
|
+
}
|
|
231
|
+
const reading = {
|
|
232
|
+
ran: stages.length,
|
|
233
|
+
verdict: v.verdict ?? null,
|
|
234
|
+
stage: v.stage ?? null,
|
|
235
|
+
stages: v.stages ?? [],
|
|
236
|
+
...(v.gap ? { gap: v.gap } : {}),
|
|
237
|
+
};
|
|
238
|
+
if (v.verdict === 'satisfied') return { ok: true, detail: '', reading };
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
reading,
|
|
242
|
+
detail: `the close's mechanical stage "${v.stage ?? 'unknown'}" no longer passes on this tree (${v.verdict}) — `
|
|
243
|
+
+ 'the tree moved under you, so the accept is refused rather than applied to work you did not read. '
|
|
244
|
+
+ `${redactSecrets(String(v.gap ?? v.detail ?? '')).split('\n').slice(0, 3).join(' ')}`.trim(),
|
|
245
|
+
};
|
|
246
|
+
}
|
package/src/run.js
CHANGED
|
@@ -15,10 +15,26 @@ import { join } from 'node:path';
|
|
|
15
15
|
import { LiteCtx } from 'litectx';
|
|
16
16
|
import { validateJob, jobSpecHash, checkApproval } from './job.js';
|
|
17
17
|
import { runPlan } from './planrun.js';
|
|
18
|
-
import { SMOKE_STORE } from './kinds.js';
|
|
18
|
+
import { SMOKE_STORE, resolveHumanRuling } from './kinds.js';
|
|
19
19
|
|
|
20
20
|
/** @typedef {{code: string, path: string, detail?: string}} Red */
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* THE ROUND RECORDS THE ONE LEDGER SUMS — as data, because two of them now exist
|
|
24
|
+
* and a hand-spelled condition per record is how a paid call comes to be free.
|
|
25
|
+
*
|
|
26
|
+
* `worker-round` is the attempt's own spend, metered per ROUND rather than per
|
|
27
|
+
* attempt-return (F12: a halted attempt's spend was invisible by 300×).
|
|
28
|
+
* `judge-round` is the CLOSE's, and it is the first spend a close has ever had:
|
|
29
|
+
* the judged floor (softgreen) buys a pinned locate call per artifact, and the
|
|
30
|
+
* budget-identity rule is that a budget funds the attempt PLUS its close. Both
|
|
31
|
+
* carry the honest `costUsd` — a null lands as `unpriced` and halts, never as $0.
|
|
32
|
+
*
|
|
33
|
+
* What is deliberately NOT here: `worker-result` / `worker-plan`, which are
|
|
34
|
+
* attempt TOTALS of the same rounds and would double-count.
|
|
35
|
+
*/
|
|
36
|
+
export const ACCOUNTED_ROUND_TYPES = Object.freeze(['worker-round', 'judge-round']);
|
|
37
|
+
|
|
22
38
|
|
|
23
39
|
/**
|
|
24
40
|
* Known-answer smoke on the primitive the run is about to trust (PRD v1.5 §4,
|
|
@@ -77,6 +93,13 @@ async function primitiveSmoke(workdir) {
|
|
|
77
93
|
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
78
94
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
79
95
|
* @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
|
|
96
|
+
* @param {any} [opts.judgeProvider] SOFTGREEN — the provider a JUDGED close stage runs its
|
|
97
|
+
* locate call through, wired by the operator and PINNED to `JUDGE_MODEL` (src/judged.js:
|
|
98
|
+
* the only tier with established injection resistance upstream). Forwarded to the plan
|
|
99
|
+
* flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
|
|
100
|
+
* never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
|
|
101
|
+
* a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
|
|
102
|
+
* lands on this ledger as `judge-round` (see `ACCOUNTED_ROUND_TYPES`).
|
|
80
103
|
* @param {number} [opts.capRuns] shell-owned iteration cap for the plan flow's
|
|
81
104
|
* CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
|
|
82
105
|
* @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
|
|
@@ -145,13 +168,26 @@ async function primitiveSmoke(workdir) {
|
|
|
145
168
|
* RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
|
|
146
169
|
* ordinary run; never authored and never defaulted (a defaulted answer to "is this
|
|
147
170
|
* done?" is the rubber-stamp the class exists to prevent, 2026-08-12 §4).
|
|
171
|
+
* @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
|
|
172
|
+
* F102 — the answer the CHECKPOINT holds (`readResume`'s `restart.pendingDecision`):
|
|
173
|
+
* a decision a person already gave on a leg that stopped before a single round was
|
|
174
|
+
* bought for it. Forwarded verbatim; the plan flow applies it exactly as a fresh one
|
|
175
|
+
* and refuses a leg handed both. It is also what decides whether the TIME fold this
|
|
176
|
+
* run declares is the chain's or this engagement's (F103).
|
|
177
|
+
* @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
|
|
178
|
+
* door (PRD v1.71 §3)? Forwarded verbatim to the plan flow, which owns the class
|
|
179
|
+
* default. It changes what the run RECORDS and never what it returns: the door is a
|
|
180
|
+
* disposition and the loop's verdict is not the door's to touch.
|
|
181
|
+
* @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
|
|
182
|
+
* MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
|
|
183
|
+
* run's review door, carrying their words. Forwarded verbatim.
|
|
148
184
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
149
185
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
150
186
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
151
187
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
152
188
|
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
153
189
|
*/
|
|
154
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null }) {
|
|
190
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null }) {
|
|
155
191
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
156
192
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
157
193
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -162,6 +198,11 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
162
198
|
// number that enters the arbiter's arithmetic: a garbage fold is 0, never a NaN
|
|
163
199
|
// that would poison every later comparison into "no cap".
|
|
164
200
|
let spentUsd = typeof priorSpentUsd === 'number' && Number.isFinite(priorSpentUsd) && priorSpentUsd > 0 ? priorSpentUsd : 0;
|
|
201
|
+
/** F103's money half — the CHAIN's fold, kept as its own figure so `spentUsd`
|
|
202
|
+
* (the chain total, what a person reads and what the ceiling governs) and THIS
|
|
203
|
+
* ENGAGEMENT's spend can be stated side by side on every terminal. Two things
|
|
204
|
+
* counted by one number will eventually be asked to disagree. */
|
|
205
|
+
const chainFoldUsd = spentUsd;
|
|
165
206
|
let unpriced = false;
|
|
166
207
|
// RESUME (v1.46 §3): was the FOLD itself exact? `readResume` marks it false when any
|
|
167
208
|
// round inside the dead attempt came back unpriced, and that unknown does not heal by
|
|
@@ -201,7 +242,16 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
201
242
|
// ONE spelling of the four causes, so the terminal and the in-flight money readout can
|
|
202
243
|
// never disagree about whether the same run's figure is exact.
|
|
203
244
|
const spendComplete = () => !unpriced && !stalled && !cutMidCall && !priorFloor;
|
|
204
|
-
const spend = () => ({
|
|
245
|
+
const spend = () => ({
|
|
246
|
+
spentUsd,
|
|
247
|
+
spendComplete: spendComplete(),
|
|
248
|
+
// F103's second counter, on every terminal beside the chain total it is NOT.
|
|
249
|
+
// `spentUsd` is cumulative-so-far (this job, across engagements) and governs the
|
|
250
|
+
// ceiling; this is what THIS engagement spent. Always emitted, never conditional
|
|
251
|
+
// on there being a fold: a consumer that has to branch on field presence is a
|
|
252
|
+
// consumer that will read absence as zero.
|
|
253
|
+
engagementSpentUsd: Math.max(0, spentUsd - chainFoldUsd),
|
|
254
|
+
});
|
|
205
255
|
// 1. human-signs-always — before ANY provider call (N1 decision #1)
|
|
206
256
|
if (!checkApproval(rawSpec, approvals)) {
|
|
207
257
|
emit('job-end', { outcome: 'unapproved-spec', detail: 'no approval record matches this exact spec version', ...spend() });
|
|
@@ -214,6 +264,11 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
214
264
|
return 'job-red';
|
|
215
265
|
}
|
|
216
266
|
const job = /** @type {any} */ (jv.job);
|
|
267
|
+
/** F103 — the two TIME figures, resolved once here and read the same way by
|
|
268
|
+
* `runPlan`'s clock. An unreadable ruling resolves to `fresh: false` and folds as
|
|
269
|
+
* it always did; `runPlan` is where it is refused, so this never has to. */
|
|
270
|
+
const chainWallMs = typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? priorWallMs : 0;
|
|
271
|
+
const wallFold = resolveHumanRuling(humanRuling, heldRuling, doorRerun).fresh ? 0 : chainWallMs;
|
|
217
272
|
emit('job-start', {
|
|
218
273
|
job: job.job, specHash: jobSpecHash(job), budgetUsd: job.budgetUsd,
|
|
219
274
|
shape: 'plan', goal: job.goal,
|
|
@@ -226,7 +281,17 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
226
281
|
// A fold of $0 that is NOT exact is still a fold — the unknown is the whole of what
|
|
227
282
|
// it inherited, and it is the only field that can carry it forward.
|
|
228
283
|
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
229
|
-
|
|
284
|
+
// …and the TIME fold, which F103 splits in two. `priorWallMs` is what the next
|
|
285
|
+
// reader ADDS to (the bound), and it is engagement-scoped: a rerun opens a fresh
|
|
286
|
+
// engagement, so the minutes the person did not commission are not a bound on
|
|
287
|
+
// anybody. `chainWallMs` is the cumulative view and is declared whenever there is
|
|
288
|
+
// one, so nothing is lost — the readout keeps spanning the chain (the halt readout
|
|
289
|
+
// and the leg's own governor answer different questions and are never mixed).
|
|
290
|
+
// The `fresh` reading comes from the ONE resolver `runPlan`'s clock reads, never a
|
|
291
|
+
// second spelling of "is this a rerun" (that is how a record and a clock come to
|
|
292
|
+
// disagree about the same leg).
|
|
293
|
+
...(wallFold > 0 ? { priorWallMs: wallFold } : {}),
|
|
294
|
+
...(chainWallMs > 0 && chainWallMs !== wallFold ? { chainWallMs } : {}),
|
|
230
295
|
// the REPLAN ledger, on the same declaration and for the same reason — a chain of
|
|
231
296
|
// resumes must fold once. This is the DIRECT-spine half of it: `readResume`'s
|
|
232
297
|
// `direct` mode opens its one implicit window on `job-start` and reads the fold off
|
|
@@ -265,7 +330,9 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
265
330
|
// `account` reds the run on ANY unpriced round (a null cost is the honest
|
|
266
331
|
// unknown, never $0 — F6): per-round metering means a partially-unpriced run
|
|
267
332
|
// is caught natively, round by round, with no separate unpricedRounds tally.
|
|
268
|
-
|
|
333
|
+
// …and the CLOSE's own rounds sum into the same wallet the moment a close can
|
|
334
|
+
// spend at all (the judged floor): one ledger, never a second arithmetic.
|
|
335
|
+
if (ACCOUNTED_ROUND_TYPES.includes(type)) account(/** @type {any} */ (data)?.costUsd);
|
|
269
336
|
// W1: a `stall` is the plan flow announcing it ABANDONED a call and reissued
|
|
270
337
|
// it. The abandoned call's spend is unknowable from here (billed or not,
|
|
271
338
|
// indistinguishable), so from this point the priced sum is a floor no matter
|
|
@@ -289,7 +356,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
289
356
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
290
357
|
{
|
|
291
358
|
const outcome = await runPlan(job, {
|
|
292
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling,
|
|
359
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd,
|
|
293
360
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
294
361
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
295
362
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
package/src/selection.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// in either direction, a percentage over 2–3 runs is fake precision, and if a score ever
|
|
27
27
|
// earns its way in its threshold is hamr's, from a measured base rate.
|
|
28
28
|
|
|
29
|
-
import { listingRow } from './bridges.js';
|
|
29
|
+
import { listingRow, QUARANTINED_CODE } from './bridges.js';
|
|
30
30
|
|
|
31
31
|
/** minutes, one decimal — the granularity a human picks a workflow at (the unit is
|
|
32
32
|
* written once, by the band) */
|
|
@@ -74,10 +74,22 @@ export function renderListing(registry) {
|
|
|
74
74
|
const reds = Array.isArray(r?.reds) ? r.reds : [];
|
|
75
75
|
const rows = bridges.map(listingRow).sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// TWO KINDS OF SKIP, and they are never written as one. An unreadable entry is a
|
|
78
|
+
// broken file; a HELD one (softgreen module 6) is a perfectly good workflow whose
|
|
79
|
+
// judged green no signer has accepted yet. Saying "could not be read" about the
|
|
80
|
+
// second would send the operator to fix a file that is not broken.
|
|
81
|
+
const broken = reds.filter((/** @type {any} */ x) => x?.code !== QUARANTINED_CODE);
|
|
82
|
+
const held = reds.filter((/** @type {any} */ x) => x?.code === QUARANTINED_CODE);
|
|
83
|
+
const skipped = [
|
|
84
|
+
broken.length
|
|
85
|
+
? [`\n\nNOTE: ${broken.length} registry entr${broken.length === 1 ? 'y' : 'ies'} could not be read and ${broken.length === 1 ? 'is' : 'are'} NOT listed — this listing is smaller than the directory:`,
|
|
86
|
+
...broken.map((/** @type {any} */ x) => ` ${x.path ?? '(unknown file)'}${x.detail ? ` — ${x.detail}` : ''}`)].join('\n')
|
|
87
|
+
: '',
|
|
88
|
+
held.length
|
|
89
|
+
? [`\n\nNOTE: ${held.length} workflow${held.length === 1 ? ' is' : 's are'} HELD and NOT offered — a judged green earns no reuse until the person who owns the job accepts it:`,
|
|
90
|
+
...held.map((/** @type {any} */ x) => ` ${x.path ?? '(unnamed)'}${x.detail ? ` — ${x.detail}` : ''}`)].join('\n')
|
|
91
|
+
: '',
|
|
92
|
+
].join('');
|
|
81
93
|
|
|
82
94
|
if (!rows.length) {
|
|
83
95
|
return `WORKFLOWS ON RECORD — none.\nThere are no workflows to reuse; a plan has to be drafted from scratch.${skipped}`;
|
|
@@ -86,7 +98,11 @@ export function renderListing(registry) {
|
|
|
86
98
|
const blocks = rows.map((row) => {
|
|
87
99
|
const g = row.greenCost;
|
|
88
100
|
return [
|
|
89
|
-
|
|
101
|
+
// an entry whose only greens are HELD has no status — but "NO-GREEN" would be
|
|
102
|
+
// false about it: it greened, and a person has simply not accepted the judged
|
|
103
|
+
// verdict yet. The distinct word is the whole difference between "this never
|
|
104
|
+
// worked" and "this is waiting on you".
|
|
105
|
+
`[${row.name ?? '(unnamed)'}] ${(row.status ?? (row.quarantinedGreens > 0 ? 'held' : 'no-green')).toUpperCase()}`,
|
|
90
106
|
` goal ${row.goal ?? '(no goal recorded)'}`,
|
|
91
107
|
` runs ${row.greens} green · ${row.reds} red${row.lastOutcome ? ` (last: ${row.lastOutcome})` : ''}`,
|
|
92
108
|
` green cost ${band(g.minUsd, g.maxUsd, usd, g.unpricedCount, row.greens, 'unpriced')}`
|
package/types/authorflow.d.ts
CHANGED
|
@@ -32,12 +32,21 @@ export function schemaCoverage(catalogue?: Record<string, any>): {
|
|
|
32
32
|
* and they arrive as validation reds through the revise loop — a schema that
|
|
33
33
|
* tried to restate them would be a second validator.
|
|
34
34
|
*
|
|
35
|
+
* `verdictType` narrows the branches to the kinds the picked class may express
|
|
36
|
+
* (softgreen module 3, `classMenu`): a green job cannot SPELL `judged-floor`,
|
|
37
|
+
* rather than spelling it and being told afterwards. COVERAGE is still computed
|
|
38
|
+
* over the WHOLE catalogue — the class filter must never make a parameter look
|
|
39
|
+
* orphaned and turn a menu narrowing into a catalogue-drift alarm.
|
|
40
|
+
*
|
|
35
41
|
* THROWS on a catalogue it cannot cover: a malformed catalogue is a bareloop bug,
|
|
36
42
|
* and failing loudly beats emitting a schema that quietly cannot say something
|
|
37
43
|
* the validator will then demand.
|
|
38
44
|
* @param {Record<string, any>} [catalogue]
|
|
45
|
+
* @param {{verdictType?: string|null}} [o]
|
|
39
46
|
*/
|
|
40
|
-
export function declarationSchema(catalogue?: Record<string, any
|
|
47
|
+
export function declarationSchema(catalogue?: Record<string, any>, { verdictType }?: {
|
|
48
|
+
verdictType?: string | null;
|
|
49
|
+
}): {
|
|
41
50
|
type: string;
|
|
42
51
|
properties: {
|
|
43
52
|
stages: {
|
|
@@ -86,12 +95,13 @@ export function declarationSchema(catalogue?: Record<string, any>): {
|
|
|
86
95
|
* The output channel. It records and acknowledges; it takes no action, which is
|
|
87
96
|
* why handing it to a toolless authoring call does not reopen D3.
|
|
88
97
|
* @param {{calls: any[]}} box
|
|
89
|
-
* @param {{catalogue?: Record<string, any
|
|
98
|
+
* @param {{catalogue?: Record<string, any>, verdictType?: string|null}} [o]
|
|
90
99
|
*/
|
|
91
100
|
export function declarationTool(box: {
|
|
92
101
|
calls: any[];
|
|
93
|
-
}, { catalogue }?: {
|
|
102
|
+
}, { catalogue, verdictType }?: {
|
|
94
103
|
catalogue?: Record<string, any>;
|
|
104
|
+
verdictType?: string | null;
|
|
95
105
|
}): {
|
|
96
106
|
name: string;
|
|
97
107
|
description: string;
|
|
@@ -146,9 +156,19 @@ export function declarationTool(box: {
|
|
|
146
156
|
* The catalogue, as the authoring call sees it — rendered FROM the catalogue, so
|
|
147
157
|
* the prompt and the validator judge one set of facts. `shape` and `asserts` are
|
|
148
158
|
* M2's own strings.
|
|
159
|
+
*
|
|
160
|
+
* `verdictType` narrows it to the picked class's menu, exactly as it narrows the
|
|
161
|
+
* schema — the prompt and the tool must offer ONE set of kinds, or the composer
|
|
162
|
+
* reads about a kind it cannot call. A kind above the ceiling is omitted rather
|
|
163
|
+
* than rendered as unavailable: a LOCKED row says "this does not run in v1",
|
|
164
|
+
* which is a fact about the build; "not for your class" is a fact about a choice
|
|
165
|
+
* the person already made, and printing it invites the composer to argue with it.
|
|
149
166
|
* @param {Record<string, any>} [catalogue]
|
|
167
|
+
* @param {{verdictType?: string|null}} [o]
|
|
150
168
|
*/
|
|
151
|
-
export function catalogueBlock(catalogue?: Record<string, any
|
|
169
|
+
export function catalogueBlock(catalogue?: Record<string, any>, { verdictType }?: {
|
|
170
|
+
verdictType?: string | null;
|
|
171
|
+
}): string;
|
|
152
172
|
/**
|
|
153
173
|
* HOW THE GENRE'S OWN TOOLS PRINT — rendered FROM `genreInstruments`, so the
|
|
154
174
|
* pattern the author is handed and the pattern the tree already grades with are
|
|
@@ -433,6 +453,48 @@ export function makeLoopGenerate(provider: any, { system, maxTokens }?: {
|
|
|
433
453
|
system?: string;
|
|
434
454
|
maxTokens?: number;
|
|
435
455
|
}): (messages: any[], tools: any[], opts?: any) => Promise<any>;
|
|
456
|
+
/**
|
|
457
|
+
* ONE structured ask, with the malformed-emission retry bounded on its OWN axis.
|
|
458
|
+
* A reply that carries no artifact is an ARTIFACT-RED — name the axis, retry
|
|
459
|
+
* under a cap, write nothing — and it never consumes a revision, because a
|
|
460
|
+
* transport fault is not a defect in a close.
|
|
461
|
+
*
|
|
462
|
+
* THE CHANNEL IS A PARAMETER, and that is softgreen module 4's only change here:
|
|
463
|
+
* the rubric card and the calibration set are a SECOND schema-forced emission
|
|
464
|
+
* with the same failure modes, and a second ladder is a second place for the cap,
|
|
465
|
+
* the reject block and the re-ask to drift. The declaration channel below is the
|
|
466
|
+
* one this loop has always used, spelled out rather than inlined — nothing about
|
|
467
|
+
* the authoring path's behaviour moves.
|
|
468
|
+
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
469
|
+
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
470
|
+
* channel: {name: string, instruction: string, textPath?: string, tool: (box: {calls: any[]}) => any}}} o
|
|
471
|
+
* @returns {Promise<{artifact: any, attempts: number, convo: any[], raw: string,
|
|
472
|
+
* providerError: string|null, red: Red|null, budget: 'cap-halt'|'pricing-red'|null}>}
|
|
473
|
+
*/
|
|
474
|
+
export function askStructured({ messages, generate, mode, retries, label, book, channel }: {
|
|
475
|
+
messages: any[];
|
|
476
|
+
generate: Function;
|
|
477
|
+
mode: "tool" | "text";
|
|
478
|
+
retries: number;
|
|
479
|
+
label: string;
|
|
480
|
+
book: ReturnType<typeof makeCostBook>;
|
|
481
|
+
channel: {
|
|
482
|
+
name: string;
|
|
483
|
+
instruction: string;
|
|
484
|
+
textPath?: string;
|
|
485
|
+
tool: (box: {
|
|
486
|
+
calls: any[];
|
|
487
|
+
}) => any;
|
|
488
|
+
};
|
|
489
|
+
}): Promise<{
|
|
490
|
+
artifact: any;
|
|
491
|
+
attempts: number;
|
|
492
|
+
convo: any[];
|
|
493
|
+
raw: string;
|
|
494
|
+
providerError: string | null;
|
|
495
|
+
red: Red | null;
|
|
496
|
+
budget: "cap-halt" | "pricing-red" | null;
|
|
497
|
+
}>;
|
|
436
498
|
/**
|
|
437
499
|
* THE AUTHORING FLOW.
|
|
438
500
|
*
|
|
@@ -615,11 +677,50 @@ export const HITL_QUESTIONS: Readonly<{
|
|
|
615
677
|
5: "What would make you say this came back worse than before?";
|
|
616
678
|
}>;
|
|
617
679
|
/**
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
680
|
+
* THE SOFTGREEN SET (module 3, design record §4.6) — the green questions BYTE
|
|
681
|
+
* FOR BYTE, plus TWO.
|
|
682
|
+
*
|
|
683
|
+
* Same discipline as the hitl set, and for the same reason: a softgreen close is
|
|
684
|
+
* *mechanical stages first, the judged stage after* (§4.5), so a softgreen job
|
|
685
|
+
* needs every mechanical fact a green one does. No renumbering of the green set,
|
|
686
|
+
* no genre question, nothing reworded.
|
|
687
|
+
*
|
|
688
|
+
* The two additions are the only things the green set cannot supply, and neither
|
|
689
|
+
* is a flourish — they are the judged floor's two REQUIRED inputs:
|
|
690
|
+
* - Q6 becomes the RUBRIC CARD (§4.3): the person's own pass/fail lines,
|
|
691
|
+
* compiled into enumerated items, signed, and enumerated in the spec hash.
|
|
692
|
+
* The documented ceiling rides with it — the judge catches violations of
|
|
693
|
+
* STATED card items only, and the fix for a miss is a new card line and a
|
|
694
|
+
* re-sign, never a smarter judge.
|
|
695
|
+
* - Q7 becomes the frozen CALIBRATION SET (§4.4): the whole pipe must grade it
|
|
696
|
+
* correctly before the close is signable.
|
|
697
|
+
* Both are asked in the person's own terms — an example they would pass and one
|
|
698
|
+
* they would fail, not a rubric they are asked to write — because compiling a lay
|
|
699
|
+
* answer into extractable items is the LLM's job (D5, proposed-and-signed) and
|
|
700
|
+
* asking the person to do it is the SWE tax this product refuses.
|
|
701
|
+
*
|
|
702
|
+
* What the two answers COMPILE INTO is module 4. This module carries them
|
|
703
|
+
* exactly as the hitl set carries its own last answer: they ride in `answers`,
|
|
704
|
+
* they reach the composer verbatim through the prompt's interview block, and
|
|
705
|
+
* nothing here derives anything from them.
|
|
706
|
+
*/
|
|
707
|
+
export const SOFTGREEN_QUESTIONS: Readonly<{
|
|
708
|
+
6: "When you judge the result yourself, what separates a pass from a fail? Name the few things you actually look for.";
|
|
709
|
+
7: "Give one example you'd pass and one you'd fail, and say why.";
|
|
710
|
+
1: "What do you want done?";
|
|
711
|
+
2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
|
|
712
|
+
3: "Is there anything that must not change?";
|
|
713
|
+
4: "How do you check today whether it's working?";
|
|
714
|
+
5: "What would make you say this came back worse than before?";
|
|
715
|
+
}>;
|
|
716
|
+
/**
|
|
717
|
+
* The three sets, all LIVE as of softgreen module 3. The locked shape stays
|
|
718
|
+
* spelled out in the readers below rather than deleted with the last locked
|
|
719
|
+
* class: `questions: null` is ABSENT, never `{}` — an empty set would read as
|
|
720
|
+
* "this class needs no questions", a different and false statement (F59's
|
|
721
|
+
* distinction, at the interview layer) — and a locked class refuses at ADMISSION,
|
|
722
|
+
* before its questions run, so the two readers throw rather than hand back
|
|
723
|
+
* nothing. A fourth class arrives locked, and the machinery is here for it.
|
|
623
724
|
* @type {Record<string, {locked: boolean, questions: Record<string|number, string>|null, required: readonly number[]|null}>}
|
|
624
725
|
*/
|
|
625
726
|
export const QUESTION_SETS: Record<string, {
|
|
@@ -823,6 +924,43 @@ export const PARAM_SCHEMAS: Readonly<{
|
|
|
823
924
|
requireNonEmpty: {
|
|
824
925
|
const: boolean;
|
|
825
926
|
};
|
|
927
|
+
card: {
|
|
928
|
+
type: string;
|
|
929
|
+
description: string;
|
|
930
|
+
properties: {
|
|
931
|
+
items: {
|
|
932
|
+
type: string;
|
|
933
|
+
minItems: number;
|
|
934
|
+
items: {
|
|
935
|
+
type: string;
|
|
936
|
+
properties: {
|
|
937
|
+
rule: {
|
|
938
|
+
enum: string[];
|
|
939
|
+
description: string;
|
|
940
|
+
};
|
|
941
|
+
text: {
|
|
942
|
+
type: string;
|
|
943
|
+
minLength: number;
|
|
944
|
+
description: string;
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
required: string[];
|
|
948
|
+
additionalProperties: boolean;
|
|
949
|
+
};
|
|
950
|
+
};
|
|
951
|
+
};
|
|
952
|
+
required: string[];
|
|
953
|
+
additionalProperties: boolean;
|
|
954
|
+
};
|
|
955
|
+
paths: {
|
|
956
|
+
type: string;
|
|
957
|
+
minItems: number;
|
|
958
|
+
maxItems: number;
|
|
959
|
+
items: {
|
|
960
|
+
type: string;
|
|
961
|
+
};
|
|
962
|
+
description: string;
|
|
963
|
+
};
|
|
826
964
|
ask: {
|
|
827
965
|
type: string;
|
|
828
966
|
minLength: number;
|