bareloop 0.6.0 → 0.7.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 +407 -0
- package/README.md +28 -11
- package/bareloop.context.md +510 -28
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/index.js +29 -0
- package/src/ladder.js +196 -0
- package/src/ledger.js +27 -0
- package/src/plan.js +75 -7
- package/src/planrun.js +762 -146
- package/src/ralph.js +148 -28
- package/src/reuse.js +1602 -0
- package/src/run.js +73 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/index.d.ts +3 -0
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +18 -3
- package/types/planrun.d.ts +108 -9
- package/types/ralph.d.ts +82 -4
- package/types/reuse.d.ts +388 -0
- package/types/run.d.ts +50 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
package/src/trend.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// The close TREND — one instrument, two consumers (PRD v1.46 §2 and §4).
|
|
2
|
+
//
|
|
3
|
+
// It answers ONE question about a run's own close grades: **is this getting
|
|
4
|
+
// better?** The money halt's decision-ready readout asks it to pick the human's
|
|
5
|
+
// lever (a top-up finishes converging work; it is waste on a run that is out of
|
|
6
|
+
// ideas), and the close-fix loop's governor asks it whether to keep paying for
|
|
7
|
+
// another attempt. Both readings come from here so the two can never disagree.
|
|
8
|
+
//
|
|
9
|
+
// THE ACCURACY LAW, and it is the whole reason this module exists as its own
|
|
10
|
+
// file: **a series is PER STAGE, compared only against itself.** The operator's
|
|
11
|
+
// own $0 sweep instrument flattened a staged close's grades into one list and
|
|
12
|
+
// read `12, 2, 5, 0` as a regression — the 2 → 5 step. What actually happened
|
|
13
|
+
// (u-msew1uy5) was two axes falling at once: suppressions 12 → 5 while the type
|
|
14
|
+
// errors that scrub re-exposed went 2 → 0. Merging the axes did not make the read
|
|
15
|
+
// noisy, it made it WRONG in the opposite direction, and it would have told hamr
|
|
16
|
+
// to abandon a converging run. Here the stages are separate buckets by
|
|
17
|
+
// construction, so the mistake is inexpressible ACROSS stages rather than avoided
|
|
18
|
+
// by care — and expressible WITHIN one, which the second KNOWN LIMIT states.
|
|
19
|
+
//
|
|
20
|
+
// Two signals, both mechanical, both off the close's own output:
|
|
21
|
+
//
|
|
22
|
+
// count the first number on the first RED-marked line of the stage that
|
|
23
|
+
// rendered the verdict. Lower is better; the comparison is against the
|
|
24
|
+
// BEST that stage has ever reported, never the last (an A→B→A
|
|
25
|
+
// oscillator repays its own strike forever under last-only — the same
|
|
26
|
+
// reason src/ladder.js keeps a seen-set).
|
|
27
|
+
// stage WHERE in the ordered close the run now stops. `runStages` is
|
|
28
|
+
// first-red-wins, so reaching a LATER stage than ever before means the
|
|
29
|
+
// wall the run was stuck behind is gone — progress no per-stage number
|
|
30
|
+
// can see. Falling back to an EARLIER stage is the opposite, and it is
|
|
31
|
+
// a reading, not an unknown.
|
|
32
|
+
//
|
|
33
|
+
// F6 governs the third answer. A stage whose output carries no number donates
|
|
34
|
+
// NOTHING — not a zero, not a strike. A governor that struck on ignorance would
|
|
35
|
+
// be minting a verdict out of a blind instrument, which is the defect class this
|
|
36
|
+
// repo has shipped six times. What bounds the loop while the instrument is blind
|
|
37
|
+
// is `blindCap`: the fixed count this rule replaces, kept as the fallback for
|
|
38
|
+
// exactly the case the new instrument cannot read, and lifted the moment it can.
|
|
39
|
+
//
|
|
40
|
+
// It bounds the CONSECUTIVE RUN of unreadable gradings, not the run's opening. The
|
|
41
|
+
// first spelling asked "has this leg ever compared anything", a one-way latch — and
|
|
42
|
+
// a stage ADVANCE satisfies it on essentially every run that does any work (the
|
|
43
|
+
// verdict() comment below says so about itself). A 27-iteration repro: one advance
|
|
44
|
+
// flipped the latch, every later grading was a numberless red at the same stage
|
|
45
|
+
// (the shipped aurora TESTGEN `verdict` stage), and from there the loop accrued
|
|
46
|
+
// neither a strike nor a blind tick — bounded by money and the wall alone, where the
|
|
47
|
+
// retired count would have stopped it. "Lifted the moment it can read" is the rule,
|
|
48
|
+
// so it must re-arm the moment it cannot: a streak carries its own baseline and
|
|
49
|
+
// binds at exactly the length a blind-from-birth run does (pinned by test).
|
|
50
|
+
//
|
|
51
|
+
// MOTION — the third signal, and it is deliberately not a fourth verdict. W-2's
|
|
52
|
+
// wall halt used to answer "was it progressing" from its own instrument: byte
|
|
53
|
+
// equality of the last two close gaps, reading `stalled` / `moving` / `unknown`
|
|
54
|
+
// beside this module's `flat` / `converging` / `unknown`. Two instruments for one
|
|
55
|
+
// question is how the two come to disagree, so the byte read moved in here and the
|
|
56
|
+
// wall and the money halt now read ONE reader. It is strictly SUBORDINATE:
|
|
57
|
+
//
|
|
58
|
+
// * where a stage reported a comparable NUMBER, motion is not consulted at all —
|
|
59
|
+
// a number is a direction and bytes are not, and a second opinion next to a
|
|
60
|
+
// real reading is only ever a chance to contradict it.
|
|
61
|
+
// * where no number exists, motion rides INSIDE `unknown` as a reading, never as
|
|
62
|
+
// the headline. "The close's output changed" is not "the run got better": it
|
|
63
|
+
// is compatible with thrash, with an oscillation, and with a worker rewriting
|
|
64
|
+
// the same file two ways. Promoting it to a direction is exactly the F6 error
|
|
65
|
+
// in a trend's coat, and the old vocabulary (`moving`) invited it.
|
|
66
|
+
//
|
|
67
|
+
// The gaps it compares are held IN MEMORY for one comparison and for nothing else.
|
|
68
|
+
// Nothing this module emits or reports ever carries a close byte — the spine is
|
|
69
|
+
// append-only forever, so a gap that leaks into a record leaks for good.
|
|
70
|
+
//
|
|
71
|
+
// THE SEED (PRD v1.46 §3, the resume chain). A resumed leg is the same run
|
|
72
|
+
// continuing, so its readout must span the CHAIN rather than restart at its own
|
|
73
|
+
// first grade: a leg that halted after one reading would otherwise read `unknown`
|
|
74
|
+
// while the run it continues had a measured direction. `seed` folds the dead leg's
|
|
75
|
+
// recorded per-stage numbers into the BASELINES — `series` (so the readout shows
|
|
76
|
+
// the whole chain) and `best` (so this leg's first reading is a comparison rather
|
|
77
|
+
// than an unknown). It folds into nothing else, and each omission is load-bearing:
|
|
78
|
+
//
|
|
79
|
+
// * no ITERATION, because the leg's own bounds must not be spent by history.
|
|
80
|
+
// * no STRIKE, because a strike is a judgment on an attempt and the seed contains
|
|
81
|
+
// none of this leg's attempts.
|
|
82
|
+
// * no `comparableEver`, because that flag answers "can the instrument see
|
|
83
|
+
// anything THIS leg produced" — the blind cap must still govern a leg whose own
|
|
84
|
+
// readings never compare, and a seed that flipped it would disarm the backstop
|
|
85
|
+
// on a run the instrument cannot read.
|
|
86
|
+
// * no STAGE POSITION, because "further than ever before" is an ordering claim
|
|
87
|
+
// about one leg's walk through the close. Seeding it would make the resumed
|
|
88
|
+
// leg's first grade a stage REGRESSION and strike it for arriving.
|
|
89
|
+
//
|
|
90
|
+
// And it belongs to ONE of the two readers, never both — the "ONE PER SERIES" rule
|
|
91
|
+
// below, now with a second reason. The halt readout asks "was the RUN converging
|
|
92
|
+
// when its allowance cut it", which is a question about the chain; the fix loop's
|
|
93
|
+
// governor asks "is THIS loop out of ideas", which is leg-local by definition. A
|
|
94
|
+
// seeded governor was built and measured: a resumed loop that struck out dead flat
|
|
95
|
+
// rendered its own terminal as "still progressing", because the chain had fallen
|
|
96
|
+
// before the handover. One instance answering two questions, exactly as warned.
|
|
97
|
+
//
|
|
98
|
+
// KNOWN LIMIT, stated rather than discovered later. "Lower is better" is a
|
|
99
|
+
// direction this module cannot derive from prose, and a minority of close stages
|
|
100
|
+
// report a FLOOR instead of a fault count (`N tests executed, below the seed's
|
|
101
|
+
// M`) where a rising number is the improvement. Those read as "not improving",
|
|
102
|
+
// never as converging — the fail-safe direction, and deliberately so: a false
|
|
103
|
+
// "flat" costs one conservative stop, a false "converging" costs a top-up spent
|
|
104
|
+
// on a dead run. Never sharpened to chase it (the F49 precedent): the dangerous
|
|
105
|
+
// direction is the other one.
|
|
106
|
+
//
|
|
107
|
+
// THE LAW FOR CLOSE AUTHORS, which is where this module's boundary is enforced:
|
|
108
|
+
// **one POPULATION per stage.** A series is one bucket per stage NAME, and a
|
|
109
|
+
// stage name is not the same thing as an axis — nothing here can stop a close
|
|
110
|
+
// from redding on two structurally different measurements under one name, and
|
|
111
|
+
// the shipped ones once did. `typecheck` red either `reports N error(s) in
|
|
112
|
+
// <scope>` (the in-scope faults) or `the target files are clean but M strict
|
|
113
|
+
// error(s) exist outside them` (a different population entirely, reached only
|
|
114
|
+
// once the first is zero), and `suite-green` had the same shape (`N test(s) now
|
|
115
|
+
// fail` beside an executed-count floor). A run crossing that seam donated both
|
|
116
|
+
// genres to one series, so a 29 → 4 across it read CONVERGING and would have
|
|
117
|
+
// recommended a top-up on work that had only swapped which wall it was behind —
|
|
118
|
+
// the u-msew1uy5 error, in the one place the bucketing could not see it.
|
|
119
|
+
//
|
|
120
|
+
// That shipped instance is RETIRED (F84, hamr-signed 2026-08-05): every u-*-close
|
|
121
|
+
// stage that carried two populations was SPLIT in the close — `typecheck` keeps
|
|
122
|
+
// the in-scope faults with `typecheck-outside` immediately after it, and
|
|
123
|
+
// `tests-kept` carries the floor immediately before `suite-green` — each new
|
|
124
|
+
// stage sitting exactly where its branch already ran, so first-red-wins holds the
|
|
125
|
+
// same gate sequence. The record stays here because the fix belongs to the close
|
|
126
|
+
// and must keep belonging to it: a new close that mixes two populations under one
|
|
127
|
+
// name re-opens this, and the remedy is that close's stage list, never a detector
|
|
128
|
+
// taught to tell two prose shapes apart (the same F49 precedent as above — a
|
|
129
|
+
// per-close sharpening is how this reader stops being one reader).
|
|
130
|
+
|
|
131
|
+
/** The strikes that end the close-fix loop. Shell-owned, exactly where the fixed
|
|
132
|
+
* count was, and the SAME number the step ladder uses — hamr's 2. Validated by a
|
|
133
|
+
* $0 replay over all 8 archived fix loops: 0 greens harmed (every historical
|
|
134
|
+
* fix-loop green converted in <= 2 verdicts) and 1 real waste case caught
|
|
135
|
+
* (reuse-msc6w93z, dead flat at 2 errors for 7 verdicts until the wall). */
|
|
136
|
+
export const FIX_STRIKE_LIMIT = 2;
|
|
137
|
+
|
|
138
|
+
/** `runStages` prefixes every staged gap with its own header — the stage name is
|
|
139
|
+
* therefore already IN the gap and needs no second seam to travel beside it. */
|
|
140
|
+
const STAGE_HEADER = /^close stage "([^"]+)" failed:[ \t]*\r?\n?/;
|
|
141
|
+
|
|
142
|
+
/** A line the close itself marked as a judgment. Every shipped close spells it
|
|
143
|
+
* `red:`, `gate-red:`, `form-red:` or `clean-red:`, so `\bred\b` reads all four
|
|
144
|
+
* without a per-close pattern. Deliberately NOT a bare "first number anywhere":
|
|
145
|
+
* a path, a timestamp or an echoed tool line would donate a number that means
|
|
146
|
+
* nothing, and a wrong number is worse than none (F6 — the honest answer is a
|
|
147
|
+
* null, and it has one). */
|
|
148
|
+
const RED_LINE = /\bred\b/i;
|
|
149
|
+
|
|
150
|
+
/** the count on that line — the first STANDALONE number. No sign: a failure count
|
|
151
|
+
* is never negative, and a leading `-?` turns `foo-3` into -3. Decimals kept
|
|
152
|
+
* (`fault-detection rate 27.5%`). Standalone means not embedded in a word or a
|
|
153
|
+
* dotted token: the digit in a rule id (`D1a`) or a version (`v1.2`) is not a
|
|
154
|
+
* count, and reading one donated a constant wrong number to the governor (review
|
|
155
|
+
* F1 — the live repro was types-close's `D1a — … went 2 → 5` reading as 1). */
|
|
156
|
+
const COUNT = /(?<![\w.])\d+(?:\.\d+)?(?!\w)/g;
|
|
157
|
+
|
|
158
|
+
/** A before→after line (`… went 2 → 5`) states its own trend; the CURRENT value
|
|
159
|
+
* is the last number, and reading the first would grade every iteration with the
|
|
160
|
+
* constant seed baseline — flat by construction. Only such lines read last; every
|
|
161
|
+
* other line keeps first-number (the live fleet's reference-suffixed lines —
|
|
162
|
+
* `5 tests executed, below the seed's 12` — carry the truth up front).
|
|
163
|
+
* KNOWN LIMIT (same rule as the header's): an arrow line with a TRAILING reference
|
|
164
|
+
* number (`went 5 → 2 (seed was 30)`) would read the reference. No shipped close
|
|
165
|
+
* emits that shape; if one appears, the fix is that close's line format — this
|
|
166
|
+
* detector is never sharpened per-close (the F49 precedent). */
|
|
167
|
+
const ARROW = /→/;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read one close gap into its stage and its count. Pure, and both halves are
|
|
171
|
+
* independently nullable: a gap with no header still yields its count, a gap
|
|
172
|
+
* with no red-marked line still yields its stage.
|
|
173
|
+
* @param {string} [gap] the close's gap, exactly as `runStages` built it
|
|
174
|
+
* @returns {{stage: string|null, value: number|null}}
|
|
175
|
+
*/
|
|
176
|
+
export function readGrade(gap) {
|
|
177
|
+
const text = String(gap ?? '');
|
|
178
|
+
const h = STAGE_HEADER.exec(text);
|
|
179
|
+
// The header is REMOVED before the red scan, never merely skipped over: a stage
|
|
180
|
+
// whose NAME contains "red" would otherwise donate its own (numberless) header
|
|
181
|
+
// line as the reading and mask the real count two lines down.
|
|
182
|
+
const body = h ? text.slice(h[0].length) : text;
|
|
183
|
+
const line = body.split('\n').find((l) => RED_LINE.test(l));
|
|
184
|
+
const nums = line ? line.match(COUNT) : null;
|
|
185
|
+
const pick = nums && nums.length ? (ARROW.test(/** @type {string} */ (line)) ? nums[nums.length - 1] : nums[0]) : null;
|
|
186
|
+
const n = pick !== null ? Number(pick) : NaN;
|
|
187
|
+
return { stage: h ? h[1] : null, value: Number.isFinite(n) ? n : null };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** the bucket key for a gap that named no stage — its own series, never merged
|
|
191
|
+
* into a named stage's (that would be the flattening this module forbids) */
|
|
192
|
+
const UNSTAGED = '(unstaged)';
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build one trend reader. ONE PER SERIES: the money readout wants the whole run's
|
|
196
|
+
* close grades (the precheck is the seed the work is measured against), while the
|
|
197
|
+
* fix governor wants only the grades its own loop produced. Two instances, one
|
|
198
|
+
* rule — never one instance with a phase field, which is how a shared reader comes
|
|
199
|
+
* to answer two questions with one number.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} [opts]
|
|
202
|
+
* @param {string[]} [opts.stageOrder] the staged close's stage names IN ORDER, so
|
|
203
|
+
* "the run got further than it ever had" is readable. Empty disables that signal
|
|
204
|
+
* entirely (an unknown ordering is unknown, never assumed).
|
|
205
|
+
* @param {number} [opts.limit] consecutive no-progress readings that strike the
|
|
206
|
+
* loop out (default `FIX_STRIKE_LIMIT`).
|
|
207
|
+
* @param {number|null} [opts.blindCap] the RETIRED count, kept as the bound for
|
|
208
|
+
* the case the instrument cannot read at all. Counted in iterations PAST the
|
|
209
|
+
* baseline, so a blind loop runs exactly the number of attempts the old fixed
|
|
210
|
+
* count bought it. `null` = no fallback bound (money and the wall still apply).
|
|
211
|
+
* @param {{stage?: string|null, value?: number|null}[]} [opts.seed] RESUME — the
|
|
212
|
+
* grades a PREVIOUS leg of this same run already recorded, oldest first, read off
|
|
213
|
+
* its own spine. Baselines only (see THE SEED above). Empty/omitted is the cold
|
|
214
|
+
* path and is byte-identical to the pre-resume reader.
|
|
215
|
+
*/
|
|
216
|
+
export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCap = null, seed = [] } = {}) {
|
|
217
|
+
/** @type {Map<string, number[]>} stage → every count it reported, in order */
|
|
218
|
+
const series = new Map();
|
|
219
|
+
/** @type {Map<string, number>} stage → the BEST (lowest) count it ever reported */
|
|
220
|
+
const best = new Map();
|
|
221
|
+
/** the furthest stage index reached so far; null until a KNOWN stage is seen */
|
|
222
|
+
let bestStageIdx = null;
|
|
223
|
+
let iterations = 0;
|
|
224
|
+
let noProgress = 0;
|
|
225
|
+
let comparableEver = false;
|
|
226
|
+
/** consecutive readings the instrument could not compare, reset by any it could.
|
|
227
|
+
* CONSECUTIVE and not cumulative: a run that reads, goes blind for a stage, then
|
|
228
|
+
* reads again is a governed run, and spending its bound on a healed patch would be
|
|
229
|
+
* the retired count returning through the back door. */
|
|
230
|
+
let uncomparableRun = 0;
|
|
231
|
+
/** did the close ever get FURTHER through its stages than it had before? Kept
|
|
232
|
+
* separately from the counts because it is the one kind of progress no per-stage
|
|
233
|
+
* number can express — and it is the only evidence a run that never reported the
|
|
234
|
+
* same stage twice has. */
|
|
235
|
+
let advancedEver = false;
|
|
236
|
+
/** the last two gaps RECORDED, for the motion fallback and for nothing else. In
|
|
237
|
+
* memory only: never reported, never emitted, never returned (see MOTION above). */
|
|
238
|
+
const lastGaps = [];
|
|
239
|
+
|
|
240
|
+
const idxOf = (/** @type {string} */ stage) => stageOrder.indexOf(stage);
|
|
241
|
+
|
|
242
|
+
// ── the seed, folded in BEFORE any reading of this leg: baselines only ──
|
|
243
|
+
// An unreadable seed entry donates nothing, exactly as an unreadable gap does —
|
|
244
|
+
// the same F6 rule, applied one seam earlier.
|
|
245
|
+
for (const s of Array.isArray(seed) ? seed : []) {
|
|
246
|
+
const v = typeof s?.value === 'number' && Number.isFinite(s.value) ? s.value : null;
|
|
247
|
+
if (v === null) continue;
|
|
248
|
+
const st = s.stage ?? UNSTAGED;
|
|
249
|
+
if (!series.has(st)) series.set(st, []);
|
|
250
|
+
/** @type {number[]} */ (series.get(st)).push(v);
|
|
251
|
+
const b = best.get(st);
|
|
252
|
+
if (b === undefined || v < b) best.set(st, v);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
/**
|
|
257
|
+
* Read one RED close grade. A green never reaches here (both callers stop on
|
|
258
|
+
* satisfied), so every reading is a failing grade by construction.
|
|
259
|
+
* @param {{gap?: string, stage?: string|null, value?: number|null}} o either a
|
|
260
|
+
* raw gap (parsed here) or an already-read `{stage, value}` — never both
|
|
261
|
+
* spellings of the same reading in one call.
|
|
262
|
+
* @returns {{iteration: number, stage: string, value: number|null, improved: boolean,
|
|
263
|
+
* comparable: boolean, noProgress: number, limit: number, stageIndex: number}}
|
|
264
|
+
*/
|
|
265
|
+
record({ gap, stage, value } = {}) {
|
|
266
|
+
const read = stage === undefined && value === undefined ? readGrade(gap) : { stage: stage ?? null, value: value ?? null };
|
|
267
|
+
// the motion fallback's only input, held for one comparison (MOTION above)
|
|
268
|
+
if (typeof gap === 'string') { lastGaps.push(gap); if (lastGaps.length > 2) lastGaps.shift(); }
|
|
269
|
+
const st = read.stage ?? UNSTAGED;
|
|
270
|
+
const v = typeof read.value === 'number' && Number.isFinite(read.value) ? read.value : null;
|
|
271
|
+
const idx = idxOf(st);
|
|
272
|
+
|
|
273
|
+
// ── the two comparisons, each against this stage's OWN history ──
|
|
274
|
+
const priorBest = best.get(st);
|
|
275
|
+
const countComparable = v !== null && priorBest !== undefined;
|
|
276
|
+
const countImproved = countComparable && v < /** @type {number} */ (priorBest);
|
|
277
|
+
// the ordering signal only exists between two KNOWN positions that DIFFER —
|
|
278
|
+
// an unnamed stage has no position, and standing still says nothing
|
|
279
|
+
const stageComparable = idx >= 0 && bestStageIdx !== null && idx !== bestStageIdx;
|
|
280
|
+
const stageImproved = stageComparable && idx > /** @type {number} */ (bestStageIdx);
|
|
281
|
+
|
|
282
|
+
const comparable = countComparable || stageComparable;
|
|
283
|
+
const improved = countImproved || stageImproved;
|
|
284
|
+
|
|
285
|
+
// ── fold the reading in AFTER the comparison (a reading never beats itself) ──
|
|
286
|
+
if (v !== null) {
|
|
287
|
+
if (!series.has(st)) series.set(st, []);
|
|
288
|
+
/** @type {number[]} */ (series.get(st)).push(v);
|
|
289
|
+
if (priorBest === undefined || v < priorBest) best.set(st, v);
|
|
290
|
+
}
|
|
291
|
+
if (idx >= 0 && (bestStageIdx === null || idx > bestStageIdx)) bestStageIdx = idx;
|
|
292
|
+
if (stageImproved) advancedEver = true;
|
|
293
|
+
iterations += 1;
|
|
294
|
+
if (comparable) {
|
|
295
|
+
comparableEver = true;
|
|
296
|
+
uncomparableRun = 0;
|
|
297
|
+
noProgress = improved ? 0 : noProgress + 1;
|
|
298
|
+
} else {
|
|
299
|
+
uncomparableRun += 1;
|
|
300
|
+
}
|
|
301
|
+
// an UNCOMPARABLE reading neither strikes nor repays: it is an absence of
|
|
302
|
+
// evidence, and the governor must not mint either verdict from one (F6). It
|
|
303
|
+
// does lengthen the blind streak, which is a count of what the instrument
|
|
304
|
+
// could not read — never a judgment on what the attempt did.
|
|
305
|
+
|
|
306
|
+
return { iteration: iterations, stage: st, value: v, improved, comparable, noProgress, limit, stageIndex: idx };
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/** has the loop run out of ideas — or, while the instrument is blind, out of
|
|
310
|
+
* the count it replaced? Money and the wall are unchanged and bound it either way.
|
|
311
|
+
* The blind arm counts the streak PAST its own first reading, the same `- 1` the
|
|
312
|
+
* blind-from-birth path always used: a streak's opening grade is the baseline the
|
|
313
|
+
* blind iterations are measured from, so a never-comparable run binds on exactly
|
|
314
|
+
* the reading it always did. */
|
|
315
|
+
struckOut: () => noProgress >= limit
|
|
316
|
+
|| (blindCap !== null && uncomparableRun - 1 >= blindCap),
|
|
317
|
+
|
|
318
|
+
/** the reader's own books. COUNTS AND STAGE NAMES ONLY — no close bytes ever
|
|
319
|
+
* enter a record from here, because the spine is append-only forever. */
|
|
320
|
+
report: () => ({
|
|
321
|
+
iterations, strikes: noProgress, limit,
|
|
322
|
+
// "the instrument has nothing comparable IN PLAY", which is the question the
|
|
323
|
+
// terminal prose branches on — is the blind cap what bounded this loop, or the
|
|
324
|
+
// strikes? `!comparableEver` alone answered it only for a run blind from birth;
|
|
325
|
+
// on a run that went blind after one advance it read `false` and the terminal
|
|
326
|
+
// would have offered "0/2 strikes" as the reason a loop had just been stopped.
|
|
327
|
+
// A live streak is the same blindness and says so; a leg with no readings yet
|
|
328
|
+
// is blind on the first clause, exactly as the seed rule requires.
|
|
329
|
+
blind: !comparableEver || uncomparableRun > 0,
|
|
330
|
+
stages: [...series.entries()].map(([name, vals]) => ({ stage: name, values: [...vals] })),
|
|
331
|
+
}),
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The DECISION-READY readout (PRD v1.46 §2): which way the run was going when
|
|
335
|
+
* something cut it, in the three verdicts hamr named, and the lever each
|
|
336
|
+
* implies. The library only reports — it never adjusts a budget (hard line).
|
|
337
|
+
* @returns {{trend: 'converging'|'flat'|'unknown', motion: 'changed'|'unchanged'|null,
|
|
338
|
+
* reading: string, lever: string, series: {stage: string, values: number[]}[]}}
|
|
339
|
+
*/
|
|
340
|
+
verdict() {
|
|
341
|
+
const rows = [...series.entries()].map(([name, vals]) => ({ stage: name, values: [...vals] }));
|
|
342
|
+
// The HEADLINE verdict rests on numbers only, and the reason is not purity: a
|
|
343
|
+
// stage advance is guaranteed on essentially every run that does any work at
|
|
344
|
+
// all (the precheck reds at stage 1, the plan fixes stage 1, the close walks
|
|
345
|
+
// on), so an ordering-only "converging" would be true of almost every run and
|
|
346
|
+
// could not discriminate the one thing hamr asked it to — whether another
|
|
347
|
+
// dollar finishes this. It stays a real signal for the STRIKE rule, where the
|
|
348
|
+
// question is the different one of whether the LAST attempt helped.
|
|
349
|
+
const numeric = rows.filter((r) => r.values.length > 1);
|
|
350
|
+
const render = numeric.map((r) => `${r.stage} ${r.values.join(' → ')}`).join('; ');
|
|
351
|
+
// convergence is defined PER STAGE and per stage only: some axis is lower than
|
|
352
|
+
// where it started. First-vs-last, because the question a halt asks is "did
|
|
353
|
+
// this run make net progress", not "did the last attempt help".
|
|
354
|
+
const fell = numeric.some((r) => r.values[r.values.length - 1] < r.values[0]);
|
|
355
|
+
|
|
356
|
+
// No stage ever reported a comparable number: the honest answer, stated rather
|
|
357
|
+
// than rounded up to either direction. A "flat" here would recommend rewriting
|
|
358
|
+
// a goal on no evidence at all; a "converging" would recommend spending on the
|
|
359
|
+
// same. The ordering movement, where there was any, rides in the READING as
|
|
360
|
+
// context — it just does not get to decide (F6: an unknown stays an unknown,
|
|
361
|
+
// even when something adjacent to it is known).
|
|
362
|
+
if (numeric.length === 0) {
|
|
363
|
+
// MOTION rides here and ONLY here: inside the unknown, as a reading. Two
|
|
364
|
+
// gaps are the minimum a comparison needs — one grade matching nothing is
|
|
365
|
+
// an absent comparison, and reporting that as "unchanged" would be the
|
|
366
|
+
// same rounding-up of an absence this branch exists to refuse.
|
|
367
|
+
const motion = lastGaps.length < 2 ? null : (lastGaps[0] === lastGaps[1] ? 'unchanged' : 'changed');
|
|
368
|
+
const base = advancedEver
|
|
369
|
+
? 'no stage reported a comparable number twice — nothing the instrument can compare, though the close did reach a later stage than it had before'
|
|
370
|
+
: 'no stage reported a comparable number twice — nothing the instrument can compare, so the direction is unknown rather than assumed';
|
|
371
|
+
return {
|
|
372
|
+
trend: 'unknown',
|
|
373
|
+
motion,
|
|
374
|
+
reading: motion === 'unchanged'
|
|
375
|
+
? `${base}; the close's output was byte-identical the last 2 times, so the previous attempt moved nothing the close can see`
|
|
376
|
+
: motion === 'changed'
|
|
377
|
+
? `${base}; the close's output was still changing between grades, so something moved that the numbers cannot see`
|
|
378
|
+
: base,
|
|
379
|
+
lever: motion === 'unchanged'
|
|
380
|
+
? 'a frozen close output strongly suggests the goal, not the allowance: revise the goal/spec first (a spec edit, so the new hash needs re-approval)'
|
|
381
|
+
: 'read the last close output before choosing between a top-up and a different goal',
|
|
382
|
+
series: rows,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Past this point a real number decided it, so motion is not consulted and is
|
|
386
|
+
// not reported: a byte comparison beside a measured direction can only ever
|
|
387
|
+
// contradict it, and the contradiction would be the weaker signal winning.
|
|
388
|
+
return fell
|
|
389
|
+
? {
|
|
390
|
+
trend: 'converging',
|
|
391
|
+
motion: null,
|
|
392
|
+
reading: `still progressing — ${render}`,
|
|
393
|
+
lever: 'top up budgetUsd and resume; the work was still converging when the money ran out (a spec edit, so the new hash needs re-approval)',
|
|
394
|
+
series: rows,
|
|
395
|
+
}
|
|
396
|
+
: {
|
|
397
|
+
trend: 'flat',
|
|
398
|
+
motion: null,
|
|
399
|
+
reading: `no stage improved — ${render}`,
|
|
400
|
+
lever: 'more money is unlikely to help; revise the goal/spec first (same re-approval)',
|
|
401
|
+
series: rows,
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D6's status ladder, walked in history order. Pure, total, and never throws —
|
|
3
|
+
* it is called on whatever a file happens to contain.
|
|
4
|
+
*
|
|
5
|
+
* - **candidate** — at least one green.
|
|
6
|
+
* - **proven** — greens on ≥2 DISTINCT patients (R2: the same SHAPE on a second
|
|
7
|
+
* instance; two greens on ONE patient is a lookup table, which the memorization
|
|
8
|
+
* audit kills).
|
|
9
|
+
* - a **red** on a PROVEN entry drops it to candidate, and re-promotion then needs
|
|
10
|
+
* greens on two distinct patients AGAIN — that is what "until a new green on a
|
|
11
|
+
* second distinct patient re-proves" means, so the demoting red clears the
|
|
12
|
+
* distinct-patient set rather than leaving the old count standing to re-promote
|
|
13
|
+
* the entry on the first repeat green.
|
|
14
|
+
* - a red on a CANDIDATE changes nothing: there is nothing below the entry bar, and
|
|
15
|
+
* the entry stays in the record with the red beside it.
|
|
16
|
+
* - a **CASUALTY is not a red.** Only the literal outcome `'red'` demotes; every
|
|
17
|
+
* other non-green outcome (`provider-red`, `wall-halt`, `close-crashed`, …) is a
|
|
18
|
+
* casualty, and casualties are never evidence in either direction.
|
|
19
|
+
*
|
|
20
|
+
* @param {unknown} history the bridge's per-run rows, oldest first
|
|
21
|
+
* @returns {'candidate'|'proven'|null} null = below the entry bar (no green yet)
|
|
22
|
+
*/
|
|
23
|
+
export function deriveStatus(history: unknown): "candidate" | "proven" | null;
|
|
24
|
+
/**
|
|
25
|
+
* Validate a bridge entry (`schema: "bridge-v1"`). Never throws on JSON text or
|
|
26
|
+
* plain parsed data — the same ingest contract both config validators hold; every
|
|
27
|
+
* failure is a named red. Returns the parsed entry on ok, null on any red.
|
|
28
|
+
* @param {object|string|unknown} input parsed entry, or raw JSON text
|
|
29
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
30
|
+
*/
|
|
31
|
+
export function validateBridge(input: object | string | unknown): {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
reds: Red[];
|
|
34
|
+
bridge: any;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* FIRST GREEN MINTS: build a new registry entry from one green. The entry bar is
|
|
38
|
+
* a green (D6) — there is deliberately no way to create an entry without one, so
|
|
39
|
+
* a failed plan cannot enter the registry even by accident.
|
|
40
|
+
* @param {unknown} meta `{ name, goal, specHash, closeStageNames, toolsUsed }` —
|
|
41
|
+
* `specHash` may be an explicit null (one probe-era bridge on disk has none), and
|
|
42
|
+
* the KEY is required so an unknown hash has to be said rather than omitted.
|
|
43
|
+
* @param {unknown} record `{ runid, patient, at, plan, costUsd, spendComplete, wallMs, rounds, specHash? }`
|
|
44
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
45
|
+
*/
|
|
46
|
+
export function mintBridge(meta: unknown, record: unknown): {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
reds: Red[];
|
|
49
|
+
bridge: any;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A green on an EXISTING bridge: the plan AS EXECUTED becomes its next version,
|
|
53
|
+
* and the run gets a history row. Pure — returns a NEW entry; the input is never
|
|
54
|
+
* mutated, so a caller that reds still holds the bridge it read from disk.
|
|
55
|
+
* @param {unknown} bridge a validated bridge entry
|
|
56
|
+
* @param {unknown} record `{ runid, patient, at, plan, costUsd, spendComplete, wallMs, rounds, specHash? }`
|
|
57
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
58
|
+
*/
|
|
59
|
+
export function appendGreen(bridge: unknown, record: unknown): {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
reds: Red[];
|
|
62
|
+
bridge: any;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A red (or a casualty): a history row ONLY. R1 — the bridge file is never edited
|
|
66
|
+
* in place by a red; the recipe that greened stays exactly as it greened, and the
|
|
67
|
+
* status derivation reads the demotion off the history.
|
|
68
|
+
* @param {unknown} bridge a validated bridge entry
|
|
69
|
+
* @param {unknown} record `{ runid, patient, at, outcome, failingStage, costUsd, spendComplete, wallMs, rounds }`
|
|
70
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
71
|
+
*/
|
|
72
|
+
export function appendRed(bridge: unknown, record: unknown): {
|
|
73
|
+
ok: boolean;
|
|
74
|
+
reds: Red[];
|
|
75
|
+
bridge: any;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* One row of D3's listing: what the user (and the selecting LLM) sees per bridge —
|
|
79
|
+
* name, the job sentence it greened, status, greens/reds, and the cost/time BAND of
|
|
80
|
+
* its greens. Deliberately no score (D6).
|
|
81
|
+
*
|
|
82
|
+
* The cost band SKIPS unknowns and says how many it skipped: an aggregate that
|
|
83
|
+
* treats a null as 0 reports a floor that reads as exact, which is F6 in an honest
|
|
84
|
+
* coat. With no priced green at all, every bound is null — never 0.
|
|
85
|
+
*
|
|
86
|
+
* Total and never throws: the listing is a display surface, and one malformed
|
|
87
|
+
* entry must not take the whole listing down (loadRegistry already reports it).
|
|
88
|
+
* @param {unknown} bridge
|
|
89
|
+
* @returns {{name: string|null, goal: string|null, status: 'candidate'|'proven'|null, greens: number, reds: number, lastOutcome: string|null, greenCost: {minUsd: number|null, maxUsd: number|null, minWallMs: number|null, maxWallMs: number|null, unpricedCount: number, untimedCount: number}}}
|
|
90
|
+
*/
|
|
91
|
+
export function listingRow(bridge: unknown): {
|
|
92
|
+
name: string | null;
|
|
93
|
+
goal: string | null;
|
|
94
|
+
status: "candidate" | "proven" | null;
|
|
95
|
+
greens: number;
|
|
96
|
+
reds: number;
|
|
97
|
+
lastOutcome: string | null;
|
|
98
|
+
greenCost: {
|
|
99
|
+
minUsd: number | null;
|
|
100
|
+
maxUsd: number | null;
|
|
101
|
+
minWallMs: number | null;
|
|
102
|
+
maxWallMs: number | null;
|
|
103
|
+
unpricedCount: number;
|
|
104
|
+
untimedCount: number;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* D2's LOAD-TIME gate, as SPLIT by the 2026-08-01 addendum. Exactly three checks —
|
|
109
|
+
* *"is this the right KIND of recipe?"*, asked at the box:
|
|
110
|
+
*
|
|
111
|
+
* 1. **verdict type matches.** v1 admits `green` only, and a bridge only ever
|
|
112
|
+
* exists because a green minted it, so the bridge side is `green` by
|
|
113
|
+
* construction and the check reads the JOB's declared type. **The day a second
|
|
114
|
+
* verdict class is admitted, the bridge entry must carry its own verdictType
|
|
115
|
+
* and this becomes a two-sided comparison** — recorded here so it cannot be
|
|
116
|
+
* quietly assumed to already be one.
|
|
117
|
+
* 2. **close-stage KINDS match.** v1 simplification: the same stage NAMES in the
|
|
118
|
+
* same ORDER is the definition of the same kinds. The probes' R2-legal pair
|
|
119
|
+
* matched on four identical stage names over different patients, which is the
|
|
120
|
+
* material this rule was ruled on; a real kind-comparison (what each stage
|
|
121
|
+
* INSPECTS, independent of what it was named) is a later rung's problem and is
|
|
122
|
+
* not pretended here.
|
|
123
|
+
* 3. **every stored verb fits THIS job's signed menu.** An omitted `tools` means
|
|
124
|
+
* the concrete current TOOL_MENU — the same predicate `resolveSpec` and
|
|
125
|
+
* `validatePlan` read (MED-1), so what the gate judges is what was signed.
|
|
126
|
+
*
|
|
127
|
+
* **NOTHING about paths, scopes or targets.** Those are instance-bound and are
|
|
128
|
+
* EXPECTED to red: every recipe from a different day names yesterday's bricks, and
|
|
129
|
+
* that is what a recipe IS, not a flaw. The pre-probe measured the drafter
|
|
130
|
+
* replacing them unaided (3/3 legal after tweak), while the gate as originally
|
|
131
|
+
* frozen would have refused all six of them at the door. The full `validatePlan`
|
|
132
|
+
* still judges the TWEAKED draft at draft time — nothing executes unvalidated, and
|
|
133
|
+
* no second, looser path exists for an inherited plan (D4).
|
|
134
|
+
*
|
|
135
|
+
* A failing gate is the **recipe-stale / wrong-kind** OUTCOME — a distinct result
|
|
136
|
+
* with cold drafting offered, never a throw.
|
|
137
|
+
* @param {unknown} bridge
|
|
138
|
+
* @param {unknown} job the validateJob-green spec this run is signed against
|
|
139
|
+
* @returns {{ ok: boolean, reds: Red[] }}
|
|
140
|
+
*/
|
|
141
|
+
export function loadGate(bridge: unknown, job: unknown): {
|
|
142
|
+
ok: boolean;
|
|
143
|
+
reds: Red[];
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Read one bridge file. Never throws — a missing/unreadable file and a malformed
|
|
147
|
+
* one are both named reds, because the registry is read at selection time and one
|
|
148
|
+
* bad file must not take the run down.
|
|
149
|
+
* @param {string} file
|
|
150
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
151
|
+
*/
|
|
152
|
+
export function loadBridge(file: string): {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
reds: Red[];
|
|
155
|
+
bridge: any;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Read a whole registry directory. A malformed entry is SKIPPED AND REPORTED —
|
|
159
|
+
* one red per bad file, the good entries still returned, sorted by name for a
|
|
160
|
+
* stable listing. `ok` is false when anything was skipped, so a caller can surface
|
|
161
|
+
* the skip instead of silently listing fewer workflows than exist on disk.
|
|
162
|
+
* @param {string} dir the operator-supplied registry path (no default location)
|
|
163
|
+
* @returns {{ ok: boolean, reds: Red[], bridges: any[] }}
|
|
164
|
+
*/
|
|
165
|
+
export function loadRegistry(dir: string): {
|
|
166
|
+
ok: boolean;
|
|
167
|
+
reds: Red[];
|
|
168
|
+
bridges: any[];
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Write one bridge entry, ATOMICALLY (temp + rename): a registry read concurrent
|
|
172
|
+
* with a write must see the old entry or the new one, never a half-written file
|
|
173
|
+
* that then reads as a malformed skip. An INVALID entry is refused before anything
|
|
174
|
+
* touches the disk — the registry never holds a file its own validator rejects.
|
|
175
|
+
* @param {string} dir the operator-supplied registry directory (must exist)
|
|
176
|
+
* @param {unknown} bridge
|
|
177
|
+
* @returns {{ ok: boolean, reds: Red[], file: string|null }}
|
|
178
|
+
*/
|
|
179
|
+
export function saveBridge(dir: string, bridge: unknown): {
|
|
180
|
+
ok: boolean;
|
|
181
|
+
reds: Red[];
|
|
182
|
+
file: string | null;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Create a registry directory. Separate from `saveBridge` ON PURPOSE: saving must
|
|
186
|
+
* never conjure a registry from a typo'd path, so making one is an explicit act.
|
|
187
|
+
* @param {string} dir
|
|
188
|
+
* @returns {string} the directory
|
|
189
|
+
*/
|
|
190
|
+
export function makeRegistry(dir: string): string;
|
|
191
|
+
/** @param {string} dir @returns {boolean} does a registry directory exist */
|
|
192
|
+
export function registryExists(dir: string): boolean;
|
|
193
|
+
export const BRIDGE_SCHEMA: "bridge-v1";
|
|
194
|
+
export type Red = {
|
|
195
|
+
code: string;
|
|
196
|
+
path: string;
|
|
197
|
+
detail?: string;
|
|
198
|
+
};
|