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
|
@@ -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/clock.js
CHANGED
|
@@ -43,6 +43,21 @@
|
|
|
43
43
|
// it from the remaining budget is what stops the LAST round outliving the run.
|
|
44
44
|
// 3. ~90% accuracy is the bar, by hamr's ruling (addendum 3). This module does
|
|
45
45
|
// not chase precision; it reports honestly at the precision it has.
|
|
46
|
+
// 4. SUSPENDED MINUTES NEVER CHARGE A SIGNED WALL (hamr's ruling, 2026-08-15:
|
|
47
|
+
// "if computer suspended, time shouldn't count"). The default source is
|
|
48
|
+
// therefore MONOTONIC — `performance.now()`, which reads libuv's `uv_hrtime`
|
|
49
|
+
// and so CLOCK_MONOTONIC, which per clock_gettime(2) "does not count time
|
|
50
|
+
// that the system is suspended" on Linux (CLOCK_BOOTTIME is the one that
|
|
51
|
+
// would). `Date.now()` is CLOCK_REALTIME and counts every suspended minute:
|
|
52
|
+
// that was not a hypothetical, it charged 45 phantom minutes against a
|
|
53
|
+
// 45-minute wall when an s2idle suspend landed mid-run, and it is the same
|
|
54
|
+
// exposure for an NTP step or a hand-set date. Nothing here corrects for a
|
|
55
|
+
// suspend after the fact; the reading simply never includes one.
|
|
56
|
+
// The epoch change is safe by construction: every use of `now()` below is
|
|
57
|
+
// RELATIVE (`now() - startedAt`), and no value derived from this clock is
|
|
58
|
+
// ever published as a calendar timestamp — `report()` carries durations only,
|
|
59
|
+
// and the `at:`/`ts:` stamps a run writes come from their own `Date.now`
|
|
60
|
+
// seams (src/reuse.js), which stay calendar-correct on purpose.
|
|
46
61
|
//
|
|
47
62
|
// Unbounded is a REAL state, not a large number: hamr ruled `maxWallMs` has no
|
|
48
63
|
// default, so a run without one is time-unbounded by explicit operator choice and
|
|
@@ -135,11 +150,14 @@ export function isWallTimeout(err, clock) {
|
|
|
135
150
|
* run. `closeStages`: how many stages the job's close runs (W5) — each one gets
|
|
136
151
|
* the FULL `closeTimeoutMs`, so this is the multiplier on the overshoot, not a
|
|
137
152
|
* decoration. Defaults to 1, the single-predicate close. `now`: injected for
|
|
138
|
-
* tests — the default is the real clock
|
|
139
|
-
*
|
|
153
|
+
* tests — the default is the real clock, and it stays injectable precisely so
|
|
154
|
+
* every test here can drive time deterministically without a sleep. That default
|
|
155
|
+
* is MONOTONIC (fact 4 above), not `Date.now`: a wall-clock jump — a suspend, an
|
|
156
|
+
* NTP step, a hand-set date — must not be able to reach a signed deadline.
|
|
157
|
+
* `priorElapsedMs`: time this attempt's PREDECESSOR already consumed (see below).
|
|
140
158
|
* @returns {Clock}
|
|
141
159
|
*/
|
|
142
|
-
export function createClock({ maxWallMs = null, closeStages = 1, now = () =>
|
|
160
|
+
export function createClock({ maxWallMs = null, closeStages = 1, now = () => performance.now(), priorElapsedMs = 0 } = {}) {
|
|
143
161
|
// RESUME (module C) — hamr's checkpoint ruling in a time coat: a run killed
|
|
144
162
|
// mid-try restarts THAT try under the REMAINDER of its signed per-try numbers,
|
|
145
163
|
// because "a budget ceiling folds in prior spend so re-invoking cannot silently
|