bareloop 0.6.0 → 0.8.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 +690 -0
- package/README.md +30 -11
- package/bareloop.context.md +658 -38
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/exits.js +8 -2
- package/src/index.js +29 -0
- package/src/job.js +43 -6
- package/src/ladder.js +196 -0
- package/src/ledger.js +84 -9
- package/src/plan.js +106 -107
- package/src/planrun.js +1040 -168
- package/src/ralph.js +156 -29
- package/src/reuse.js +1665 -0
- package/src/run.js +92 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/src/validate.js +121 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +3 -0
- package/types/job.d.ts +13 -2
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +41 -41
- package/types/planrun.d.ts +208 -9
- package/types/ralph.d.ts +98 -4
- package/types/reuse.d.ts +392 -0
- package/types/run.d.ts +63 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
- package/types/validate.d.ts +50 -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
|
+
}
|
package/src/validate.js
CHANGED
|
@@ -155,3 +155,124 @@ export function sweepSecretLiterals(root, red) {
|
|
|
155
155
|
}
|
|
156
156
|
})(root, '');
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
// ── the catastrophic-backtracking reject (F49) ────────────────────────────────
|
|
160
|
+
// Housed here, with the other primitives BOTH validators share, because THREE
|
|
161
|
+
// regex fields reach an untimed evaluator and they are split across the two
|
|
162
|
+
// documents: the agent's `artifact-written.pattern` (plan.js) and the
|
|
163
|
+
// operator's `judged.pattern` + `gapKeep` (job.js). plan.js already imports
|
|
164
|
+
// job.js, so job.js cannot import plan.js — and a second copy here would be the
|
|
165
|
+
// drift class SECRET_PATTERNS exists to prevent: a shape one side rejects and
|
|
166
|
+
// the other admits. ONE inventory, imported by both.
|
|
167
|
+
|
|
168
|
+
/** Length of an UNBOUNDED quantifier token at `src[i]` (`*`, `+`, or `{n,}`,
|
|
169
|
+
* plus an optional trailing lazy `?`), or 0. Bounded forms (`?`, `{n}`,
|
|
170
|
+
* `{n,m}`) return 0: they cannot drive exponential backtracking (a bounded
|
|
171
|
+
* outer repeat is polynomial at worst, and the body is self-authored — F49).
|
|
172
|
+
* @param {string} src @param {number} i */
|
|
173
|
+
function unboundedQuantLen(src, i) {
|
|
174
|
+
let len = src[i] === '*' || src[i] === '+' ? 1 : 0;
|
|
175
|
+
if (!len && src[i] === '{') { const m = /^\{\d+,\}/.exec(src.slice(i)); if (m) len = m[0].length; }
|
|
176
|
+
if (len && src[i + len] === '?') len++; // the lazy modifier is part of the same token
|
|
177
|
+
return len;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
182
|
+
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
183
|
+
* (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
|
|
184
|
+
* group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
|
|
185
|
+
* and is caught by propagating the inner repeat up through the wrapper. Such a
|
|
186
|
+
* pattern can hang `RegExp.test` for seconds on a short crafted body, and THREE
|
|
187
|
+
* fields feed it to an evaluator with NO timeout — one per document:
|
|
188
|
+
* - the AGENT's `artifact-written.pattern`, run by `evalExits` (F49, the
|
|
189
|
+
* original site). Self-DoS only: the agent authors both the pattern and
|
|
190
|
+
* (via the worker) the artifact, so a hang burns only its own run's
|
|
191
|
+
* wall-clock, and there is no arbiter compromise — it cannot escape the
|
|
192
|
+
* fence, forge a green, or leak a secret.
|
|
193
|
+
* - the OPERATOR's `judged.pattern` (`runClose`) and `gapKeep` (`boundGap`),
|
|
194
|
+
* compiled and run against close output in BARELOOP'S OWN PROCESS. These
|
|
195
|
+
* are worse than an operator self-DoS, and the reason is F67's lesson: the
|
|
196
|
+
* regex blocks the MAIN EVENT LOOP, so the in-process stall fuse — a timer
|
|
197
|
+
* in that same loop — cannot fire. The run dies to the OUTSIDE watchdog and
|
|
198
|
+
* is read as a model stall, so a bad regex in a SIGNED spec presents as a
|
|
199
|
+
* provider problem. MEASURED: `boundGap(stream, '(a+)+$')` on a 40-char
|
|
200
|
+
* body does not finish in 20s.
|
|
201
|
+
* So the reject targets the dominant exponential class and fails it as a
|
|
202
|
+
* mechanical gap at the validation gate, before any tokens burn. A full ReDoS
|
|
203
|
+
* analyzer needs a real regex engine (an external native dep we do not take for
|
|
204
|
+
* a LOW issue); exotic overlapping-alternation blowup is out of scope by
|
|
205
|
+
* decision (PARKED to the close-authoring rung — PRD v1.55).
|
|
206
|
+
*
|
|
207
|
+
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
208
|
+
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
209
|
+
* detected — there is no inner QUANTIFIER to find, so the shape scan cannot
|
|
210
|
+
* see it; the blowup comes from overlapping alternation BRANCHES under a
|
|
211
|
+
* repeat. A genuine limit of the approach, not a bug in it. Widening the
|
|
212
|
+
* detector changes which SIGNED specs are admissible, which is
|
|
213
|
+
* arbiter-adjacent — parked, not chased (PRD v1.55).
|
|
214
|
+
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
215
|
+
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
216
|
+
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
217
|
+
* SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
|
|
218
|
+
* never admits an unsafe pattern), and the cost is bounded: the plan drafter
|
|
219
|
+
* gets a mechanical gap and rewrites (drop the outer `+`, or match once).
|
|
220
|
+
* Detecting "safe because anchored" needs the same real engine we declined,
|
|
221
|
+
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
222
|
+
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
223
|
+
*
|
|
224
|
+
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
225
|
+
* so this scan assumes valid, balanced JS regex syntax.
|
|
226
|
+
* @param {string} src a compiled regex source string
|
|
227
|
+
* @returns {boolean} true iff a nested unbounded quantifier is present
|
|
228
|
+
*/
|
|
229
|
+
export function hasNestedQuantifier(src) {
|
|
230
|
+
/** @type {{ quant: boolean }[]} */
|
|
231
|
+
const stack = [];
|
|
232
|
+
for (let i = 0; i < src.length; i++) {
|
|
233
|
+
const c = src[i];
|
|
234
|
+
if (c === '\\') { i++; continue; } // escaped atom — the next char is a literal
|
|
235
|
+
if (c === '[') { // character class: quantifier chars inside are literals
|
|
236
|
+
i++;
|
|
237
|
+
if (src[i] === '^') i++;
|
|
238
|
+
// NO POSIX leading-`]`-is-a-literal rule: JS is not POSIX here. `[]` is the
|
|
239
|
+
// EMPTY class (matches nothing) and `[^]` the any-char idiom — both close at
|
|
240
|
+
// that first `]`. Skipping it ran the scan past the class's real end and
|
|
241
|
+
// swallowed every quantifier after it, so `x[^](a+)+$` read as safe while
|
|
242
|
+
// RegExp.test on it does not finish in 15s on a 31-char body. A false
|
|
243
|
+
// NEGATIVE is the dangerous direction (F49), so the scan follows JS.
|
|
244
|
+
//
|
|
245
|
+
// Honest limit on the monotonicity claim: this is a PARSE correction, not
|
|
246
|
+
// the "add rejections only" tightening F49's rule contemplates, and it is
|
|
247
|
+
// not strictly rejection-monotonic. Measured over 14,862 compilable
|
|
248
|
+
// generated patterns (2,369 previously rejected): 2 rejections disappear,
|
|
249
|
+
// both pathological `[]`-bearing shapes whose old `true` came from the very
|
|
250
|
+
// misparse being fixed, and both verified to finish RegExp.test in ~0ms.
|
|
251
|
+
// Every entry of the checked-in REDOS_BAD/GOOD/OVERREJECTED corpus is
|
|
252
|
+
// unchanged. So no real hazard was un-rejected — but the claim is
|
|
253
|
+
// "measured, no hazard lost", never "monotonic by construction".
|
|
254
|
+
while (i < src.length && src[i] !== ']') { if (src[i] === '\\') i++; i++; }
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (c === '(') { stack.push({ quant: false }); continue; }
|
|
258
|
+
if (c === ')') {
|
|
259
|
+
const g = stack.pop();
|
|
260
|
+
const qlen = unboundedQuantLen(src, i + 1);
|
|
261
|
+
if (g && g.quant && qlen) return true; // group repeats unboundedly AND its body did too
|
|
262
|
+
// The group is an unbounded-repeated atom of its parent when it is directly
|
|
263
|
+
// re-quantified (qlen) OR its own body already repeats unboundedly (g.quant):
|
|
264
|
+
// a redundant wrapper — ((a+))+ , (?:(a+))* , (((\d*)))+ — is the SAME
|
|
265
|
+
// exponential class as (a+)+, so an inner repeat must propagate THROUGH the
|
|
266
|
+
// enclosing group or the outer quantifier's close never sees it. Propagation
|
|
267
|
+
// is MONOTONIC (it only ever SETS quant=true → only ever adds rejections): it
|
|
268
|
+
// can widen over-rejection — the named FAIL-SAFE direction — but can never
|
|
269
|
+
// introduce a false negative, which is the dangerous one (F49).
|
|
270
|
+
if ((qlen || (g && g.quant)) && stack.length) stack[stack.length - 1].quant = true;
|
|
271
|
+
i += qlen;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const qlen = unboundedQuantLen(src, i); // a quantifier applying to the preceding atom, inside the current group
|
|
275
|
+
if (qlen) { if (stack.length) stack[stack.length - 1].quant = true; i += qlen - 1; }
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|