bareloop 0.7.0 → 0.9.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 +704 -0
- package/README.md +13 -4
- package/bareloop.context.md +351 -32
- package/package.json +1 -1
- package/src/authorflow.js +1280 -0
- package/src/authoring.js +1296 -0
- package/src/authorjob.js +793 -0
- package/src/authorscout.js +706 -0
- package/src/bridges.js +2 -2
- package/src/declaredclose.js +519 -0
- package/src/exits.js +8 -2
- package/src/index.js +72 -0
- package/src/job.js +91 -10
- package/src/kinds.js +1354 -0
- package/src/ledger.js +70 -9
- package/src/plan.js +57 -101
- package/src/planrun.js +554 -75
- package/src/ralph.js +27 -5
- package/src/reuse.js +135 -11
- package/src/root.js +8 -1
- package/src/run.js +29 -3
- package/src/text.js +120 -0
- package/src/validate.js +121 -0
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +720 -0
- package/types/authoring.d.ts +447 -0
- package/types/authorjob.d.ts +241 -0
- package/types/authorscout.d.ts +307 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +14 -2
- package/types/kinds.d.ts +343 -0
- package/types/plan.d.ts +41 -38
- package/types/planrun.d.ts +125 -18
- package/types/ralph.d.ts +17 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +23 -2
- package/types/text.d.ts +84 -0
- package/types/validate.d.ts +50 -0
- package/types/workbranch.d.ts +102 -0
package/src/ralph.js
CHANGED
|
@@ -285,6 +285,21 @@ export async function runClose(close, redact = (s) => s, { timeoutMs = 120_000,
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The staged gap's header — WHICH wall the close stopped at, in front of that
|
|
290
|
+
* wall's own output. A worker handed three stages' worth of output cannot tell
|
|
291
|
+
* which one it must satisfy, and `src/trend.js` parses this exact line to bucket
|
|
292
|
+
* a grade per stage (its `STAGE_HEADER`).
|
|
293
|
+
*
|
|
294
|
+
* Exported for the one reason `GAP_KEEP_TRIM_MARKER` is: a SECOND executor now
|
|
295
|
+
* renders staged gaps (`src/declaredclose.js`, the declared close), and two
|
|
296
|
+
* spellings of this line would be two instruments — the reader would silently
|
|
297
|
+
* stop bucketing whichever one drifted. Behaviour here is unchanged; this is the
|
|
298
|
+
* same template, named.
|
|
299
|
+
* @param {string} name @param {string} gap
|
|
300
|
+
*/
|
|
301
|
+
export const stageGap = (name, gap) => `close stage "${name}" failed:\n${gap}`;
|
|
302
|
+
|
|
288
303
|
/**
|
|
289
304
|
* Run a STAGED close (PRD v1.28) — an ordered list of named command stages, run
|
|
290
305
|
* in order until one renders a verdict that is not `satisfied`. The return is
|
|
@@ -310,7 +325,7 @@ export async function runClose(close, redact = (s) => s, { timeoutMs = 120_000,
|
|
|
310
325
|
* @param {{timeoutMs?: number, cwd?: string}} [opts]
|
|
311
326
|
* @returns {Promise<any>} runClose's verdict shape, plus `stage` and `stages`
|
|
312
327
|
*/
|
|
313
|
-
export async function runStages(stages, redact = (s) => s, { timeoutMs, cwd } = {}) {
|
|
328
|
+
export async function runStages(stages, redact = (s) => s, /** @type {{timeoutMs?: number, cwd?: string}} */ { timeoutMs, cwd } = {}) {
|
|
314
329
|
const ran = [];
|
|
315
330
|
let last;
|
|
316
331
|
for (const st of stages) {
|
|
@@ -326,9 +341,9 @@ export async function runStages(stages, redact = (s) => s, { timeoutMs, cwd } =
|
|
|
326
341
|
...r,
|
|
327
342
|
stage: st.name,
|
|
328
343
|
stages: ran,
|
|
329
|
-
// The gap says WHICH wall, then the wall's own output
|
|
330
|
-
//
|
|
331
|
-
...(r.gap ? { gap:
|
|
344
|
+
// The gap says WHICH wall, then the wall's own output (`stageGap`, the
|
|
345
|
+
// one spelling — the declared executor renders the same header).
|
|
346
|
+
...(r.gap ? { gap: stageGap(st.name, r.gap) } : {}),
|
|
332
347
|
};
|
|
333
348
|
}
|
|
334
349
|
}
|
|
@@ -359,10 +374,17 @@ export const GAP_KEEP_TRIM_MARKER = 'more elided by the';
|
|
|
359
374
|
* `keepPattern` is set (F28), lines of the WHOLE stream matching it are also
|
|
360
375
|
* carried in a clearly-delimited block between head and tail — capped — so the
|
|
361
376
|
* failing-test names buried in the middle still reach the worker.
|
|
377
|
+
* Exported for the ONE reason `GAP_KEEP_TRIM_MARKER` above is: a second consumer
|
|
378
|
+
* that needs a gap bounded must reuse THIS envelope, not spell its own. A private
|
|
379
|
+
* copy would be a second truncation scheme, and the two would drift on exactly the
|
|
380
|
+
* property that matters — whether the red lines survive, and whether the trim
|
|
381
|
+
* announces itself (F28: silent truncation is the disease this function cures).
|
|
382
|
+
* The close path is unchanged: `runClose` still calls it with the stage's own
|
|
383
|
+
* `gapKeep`, byte-identically.
|
|
362
384
|
* @param {string} s the (redacted) close output
|
|
363
385
|
* @param {string} [keepPattern] regex source; matching lines survive the elision
|
|
364
386
|
*/
|
|
365
|
-
function boundGap(s, keepPattern) {
|
|
387
|
+
export function boundGap(s, keepPattern) {
|
|
366
388
|
if (s.length <= GAP_HEAD + GAP_TAIL + 100) return s;
|
|
367
389
|
const head = s.slice(0, GAP_HEAD);
|
|
368
390
|
const tail = s.slice(-GAP_TAIL);
|
package/src/reuse.js
CHANGED
|
@@ -48,7 +48,7 @@ import { createHash } from 'node:crypto';
|
|
|
48
48
|
import { createRequire } from 'node:module';
|
|
49
49
|
import { loadRegistry, saveBridge, appendGreen, appendRed, mintBridge, registryExists } from './bridges.js';
|
|
50
50
|
import { renderListing, selectionPrompt } from './selection.js';
|
|
51
|
-
import {
|
|
51
|
+
import { closeStagesOf } from './plan.js';
|
|
52
52
|
import { MIN_WALL_MS, jobSpecHash } from './job.js';
|
|
53
53
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
54
54
|
import { readGrade } from './trend.js';
|
|
@@ -498,6 +498,30 @@ function readStepCheckpoint(seen) {
|
|
|
498
498
|
};
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
/**
|
|
502
|
+
* The WORK BRANCH the dead leg created (PRD v1.57 §3), read off its own `work-branch`
|
|
503
|
+
* record — the one place the runner writes it down.
|
|
504
|
+
*
|
|
505
|
+
* It is read rather than re-derived, and that is the whole point. `workBranchName` is
|
|
506
|
+
* deterministic from the signed spec, so a restart computing it fresh would get the very
|
|
507
|
+
* name its predecessor already took and the collision walk would hand it
|
|
508
|
+
* `<name>-2` — a brand-new branch standing beside the work the resume exists to
|
|
509
|
+
* continue. The declared-fold precedent (`priorSpentUsd`, `priorReplans`): what the
|
|
510
|
+
* chain already did is DECLARED on the spine and inherited, never recomputed.
|
|
511
|
+
*
|
|
512
|
+
* The LAST record wins. A leg emits exactly one, but a window can reach back through an
|
|
513
|
+
* abandoned attempt of the same try, and the newest is the branch the work is actually
|
|
514
|
+
* sitting on.
|
|
515
|
+
* @param {any[]} seen the window's own events, in file order
|
|
516
|
+
* @returns {string|null} null on a spine older than the record, or a kill before 0c
|
|
517
|
+
*/
|
|
518
|
+
function readWorkBranch(seen) {
|
|
519
|
+
for (let i = seen.length - 1; i >= 0; i -= 1) {
|
|
520
|
+
if (seen[i].type === 'work-branch' && isNonEmptyString(seen[i].branch)) return String(seen[i].branch);
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
501
525
|
/**
|
|
502
526
|
* The dead leg's CLOSE GRADES, in order — the trend baselines a resumed leg
|
|
503
527
|
* inherits (PRD v1.46 §3, and hamr's approved #2: *a resumed run's trend judges the
|
|
@@ -594,7 +618,11 @@ function readGradeSeed(seen) {
|
|
|
594
618
|
* **The declared fold, not a re-derivation.** A restarted try's own `try-start`
|
|
595
619
|
* carries the fold it inherited (`priorSpentUsd`/`priorWallMs`), so a resume OF a
|
|
596
620
|
* resume adds only its own new rounds. Re-deriving from the whole file would bill an
|
|
597
|
-
* abandoned attempt twice.
|
|
621
|
+
* abandoned attempt twice. The REPLAN ledger (`priorReplans`/`priorReplanGrantUsed`)
|
|
622
|
+
* folds on this same mechanism and deliberately not on `readGradeSeed`'s: a grade seed
|
|
623
|
+
* feeds a readout, so its documented one-leg-per-resume shortening can only under-claim
|
|
624
|
+
* a direction, while an under-claimed CEILING is a refilled allowance and every kill
|
|
625
|
+
* would buy one (PRD v1.12 — the replan bound is the RUN's).
|
|
598
626
|
*
|
|
599
627
|
* **KNOWN LIMIT, stated rather than discovered later.** A call that was killed BEFORE it
|
|
600
628
|
* returned left no event at all — no `worker-round`, no `selection-result` — so money it
|
|
@@ -688,6 +716,19 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
688
716
|
// declaration existed, or a try nobody resumed — both are honestly zero.
|
|
689
717
|
declaredRounds: !('priorRounds' in ev) ? 0
|
|
690
718
|
: (typeof ev.priorRounds === 'number' && Number.isFinite(ev.priorRounds) && ev.priorRounds >= 0 ? ev.priorRounds : null),
|
|
719
|
+
// the REPLAN ledger this attempt was already carrying, on the same declared-fold
|
|
720
|
+
// mechanism and with the same `> 0` belt as the money. It has to fold this way
|
|
721
|
+
// rather than the grade seed's way: `readGradeSeed`'s chain shortens by one leg
|
|
722
|
+
// per resume, which under-claims — harmless for a readout, and for a CEILING an
|
|
723
|
+
// under-claim IS a refilled allowance, one per kill (PRD v1.12).
|
|
724
|
+
declaredReplans: typeof ev.priorReplans === 'number' && Number.isFinite(ev.priorReplans) && ev.priorReplans > 0 ? Math.floor(ev.priorReplans) : 0,
|
|
725
|
+
// the F85-C latch rides BESIDE the count, never derived from it: a chain can have
|
|
726
|
+
// spent the ordinary ceiling with the arbiter's one extra still unearned, and
|
|
727
|
+
// deriving one from the other would collapse those two states into one
|
|
728
|
+
declaredGrantUsed: ev.priorReplanGrantUsed === true,
|
|
729
|
+
// this window's OWN replans, counted from the records the runner emits — the same
|
|
730
|
+
// `replan` event a human reads, so the ledger and the log cannot disagree
|
|
731
|
+
windowReplans: 0, windowGrantUsed: false,
|
|
691
732
|
roundsUsd: 0, roundsComplete: true, r1Written: false, jobEnd: null,
|
|
692
733
|
/** @type {any[]} the window's own events, so the SAME `readTry` the live loop uses
|
|
693
734
|
* reads a graded-but-unrecorded try — never a second reader spelling it differently */
|
|
@@ -745,6 +786,16 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
745
786
|
open.jobEnd = ev;
|
|
746
787
|
} else if (ev.type === 'bridge-write') {
|
|
747
788
|
open.r1Written = true;
|
|
789
|
+
} else if (ev.type === 'replan') {
|
|
790
|
+
// ONE record per replan the runner drafts, so counting them IS the leg's ledger
|
|
791
|
+
// — never re-derived from `plan-accepted` (a resumed leg emits one of those for a
|
|
792
|
+
// plan it RELOADED, which would read as a redraft nobody paid for).
|
|
793
|
+
open.windowReplans += 1;
|
|
794
|
+
// the arbiter stamps `granted` only on the extra it granted past the ordinary
|
|
795
|
+
// ceiling, and stamps nothing on the ordinary one — so PRESENCE is the whole
|
|
796
|
+
// signal, and reading it by presence keeps this reader honest about a reading it
|
|
797
|
+
// does not itself make (never re-deciding whether the trend deserved it).
|
|
798
|
+
if (ev.granted !== undefined) open.windowGrantUsed = true;
|
|
748
799
|
} else if (ev.type === 'try-end') {
|
|
749
800
|
completed.push({ ...ev, seq: undefined, ts: undefined, inherited: true });
|
|
750
801
|
if (open.bridge) tried.push(open.bridge);
|
|
@@ -815,6 +866,30 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
815
866
|
// restart already re-graded that tree. Counting both would enter the same
|
|
816
867
|
// reading twice and flatten a chain that moved.
|
|
817
868
|
grades: readGradeSeed(open.seen),
|
|
869
|
+
// the WORK BRANCH this leg's uncommitted work is sitting on (v1.57 §3). Read
|
|
870
|
+
// from THIS window first and then from the newest abandoned window of the same
|
|
871
|
+
// try, on the checkpoint's own rule and for the checkpoint's own reason: the
|
|
872
|
+
// branch — like the plan — is a fact about the TREE, and the tree survives an
|
|
873
|
+
// attempt that died before recording anything of its own. Null is the honest
|
|
874
|
+
// answer for a leg killed before 0c (or a spine older than the record); the
|
|
875
|
+
// resume then takes the cold path and creates one, which is correct, because a
|
|
876
|
+
// leg that never branched has no work to strand.
|
|
877
|
+
branch: readWorkBranch(open.seen)
|
|
878
|
+
?? abandoned.filter((w) => w.n === open.n).map((w) => readWorkBranch(w.seen)).filter(Boolean).at(-1)
|
|
879
|
+
?? null,
|
|
880
|
+
// the REPLAN ledger, folded like the money one line below and deliberately
|
|
881
|
+
// NOT like the grades one line above. A grade seed feeds a READOUT and its
|
|
882
|
+
// documented shortening chain can only under-claim a direction; this feeds a
|
|
883
|
+
// BOUND, where under-claiming is a refilled allowance and every kill would buy
|
|
884
|
+
// one (PRD v1.12: unlimited replanning launders thrash as adaptation). So it
|
|
885
|
+
// reads the declared fold plus THIS window's own records — the same arithmetic
|
|
886
|
+
// as `priorSpentUsd`, which is what makes a resume of a resume inherit leg 1.
|
|
887
|
+
//
|
|
888
|
+
// The abandoned windows are not read, for the money's exact reason: the
|
|
889
|
+
// restart DECLARED what it inherited, so counting the window it abandoned
|
|
890
|
+
// would charge the chain twice for one leg's replans.
|
|
891
|
+
replans: open.declaredReplans + open.windowReplans,
|
|
892
|
+
replanGrantUsed: open.declaredGrantUsed || open.windowGrantUsed,
|
|
818
893
|
priorSpentUsd: open.declaredSpentUsd + open.roundsUsd,
|
|
819
894
|
// F83 — was that fold EXACT? Three sources, because `runJob` decides the same
|
|
820
895
|
// question from more than this window can see (src/run.js: `!unpriced &&
|
|
@@ -1017,7 +1092,7 @@ export async function runReuse(opts) {
|
|
|
1017
1092
|
const runid = opts.runid ?? Date.now().toString(36);
|
|
1018
1093
|
const resume = isObj(opts.resume) ? /** @type {any} */ (opts.resume) : null;
|
|
1019
1094
|
/** a try that inherits nothing: the ordinary, non-resumed attempt */
|
|
1020
|
-
const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]) };
|
|
1095
|
+
const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]), replans: 0, replanGrantUsed: false };
|
|
1021
1096
|
|
|
1022
1097
|
/** @type {any[]} */
|
|
1023
1098
|
const tries = [];
|
|
@@ -1113,7 +1188,7 @@ export async function runReuse(opts) {
|
|
|
1113
1188
|
|
|
1114
1189
|
/** the shared per-try execution: run the job, read the spine, write the box (R1).
|
|
1115
1190
|
* @param {any|null} bridge @param {number} n
|
|
1116
|
-
* @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[]}} [prior]
|
|
1191
|
+
* @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[], replans: number, replanGrantUsed: boolean}} [prior]
|
|
1117
1192
|
* RESUME — what a KILLED attempt of this same try already consumed, and what it
|
|
1118
1193
|
* already measured. Money and wall are FOLDED into the attempt (`runJob`'s own ledger
|
|
1119
1194
|
* and clock start partly spent) rather than shrinking the caps, so the signed per-try
|
|
@@ -1121,14 +1196,23 @@ export async function runReuse(opts) {
|
|
|
1121
1196
|
* way but on the ROW alone (nothing enforces a round bound here), and `null` — an
|
|
1122
1197
|
* attempt whose turns could not be counted — is reported, never folded. `grades` are
|
|
1123
1198
|
* the close readings that leg recorded, handed on as the run trend's BASELINES: they
|
|
1124
|
-
* feed the HALT READOUT alone and spend none of this attempt's bounds.
|
|
1199
|
+
* feed the HALT READOUT alone and spend none of this attempt's bounds. `replans` /
|
|
1200
|
+
* `replanGrantUsed` are the opposite kind of inheritance and sit here for that
|
|
1201
|
+
* contrast: they SPEND a bound (the plan flow's replan ceiling), because that ceiling
|
|
1202
|
+
* is the RUN's by doctrine and a fresh one per kill is the creep v1.12 forbids.
|
|
1125
1203
|
* @param {any} [seed] RESUME (v2) — WHERE that attempt was killed (`readResume`'s
|
|
1126
1204
|
* `restart.seed`): the plan it accepted and the steps it finished. The fold says how
|
|
1127
1205
|
* much of the try's allowance is gone; this says how much of its WORK is done, so the
|
|
1128
1206
|
* restart re-pays for neither the scout, the draft, nor a finished step. Null is the
|
|
1129
1207
|
* scout/draft death, where nothing paid is re-payable and the try simply restarts.
|
|
1208
|
+
* @param {string|null} [branch] RESUME (v1.57 §3) — the WORK BRANCH that attempt's work
|
|
1209
|
+
* is sitting on (`readResume`'s `restart.branch`). The name is derived from the SIGNED
|
|
1210
|
+
* spec and is therefore the same on every leg, so a restart that recomputed it would
|
|
1211
|
+
* collide with its own predecessor and be handed a fresh `-2` beside the work it came
|
|
1212
|
+
* back for. Declared and inherited, exactly like the money and the replan ledger. Null
|
|
1213
|
+
* is the cold path, and also the leg killed before it ever branched.
|
|
1130
1214
|
* @returns {Promise<any>} the try row */
|
|
1131
|
-
const runTry = async (bridge, n, prior = NO_PRIOR, seed = null) => {
|
|
1215
|
+
const runTry = async (bridge, n, prior = NO_PRIOR, seed = null, branch = null) => {
|
|
1132
1216
|
const mode = bridge ? 'bridge' : 'cold';
|
|
1133
1217
|
const name = bridge ? bridge.name : null;
|
|
1134
1218
|
const resumed = prior.spentUsd > 0 || prior.wallMs > 0;
|
|
@@ -1142,6 +1226,15 @@ export async function runReuse(opts) {
|
|
|
1142
1226
|
// means "a spine older than this field", and a reader that cannot tell that from a
|
|
1143
1227
|
// predecessor's honest unknown would launder the unknown into a zero (F6).
|
|
1144
1228
|
...(resumed ? { priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, priorRounds: prior.rounds, resumedFrom: resume?.fromRunid ?? runid } : {}),
|
|
1229
|
+
// the REPLAN ledger, declared on the same record and for the same reason as the
|
|
1230
|
+
// money: the next reader adds only its own window's `replan` records to this
|
|
1231
|
+
// number, so a chain of resumes folds once instead of restarting the ceiling at
|
|
1232
|
+
// whichever leg it happens to be reading (PRD v1.12 — the bound is the RUN's).
|
|
1233
|
+
// Declared independently of `resumed`, which is a MONEY/WALL test: a leg can
|
|
1234
|
+
// replan without buying a round the fold would notice, and gating the ledger on
|
|
1235
|
+
// the wallet would drop it exactly there. Only when there is one to state — a
|
|
1236
|
+
// decorative zero is indistinguishable from a leg nobody folded.
|
|
1237
|
+
...(prior.replans > 0 ? { priorReplans: prior.replans, priorReplanGrantUsed: prior.replanGrantUsed } : {}),
|
|
1145
1238
|
// WHERE it picks up, on the row's own record: a try that restarts at step 3 of 4 is
|
|
1146
1239
|
// a materially different attempt from one that restarts at its beginning, and a
|
|
1147
1240
|
// reader must not have to infer which happened from the absence of a scout.
|
|
@@ -1173,7 +1266,16 @@ export async function runReuse(opts) {
|
|
|
1173
1266
|
// restarted leg would report `flat` on a run that was converging. Only when there
|
|
1174
1267
|
// ARE grades: an empty array is the cold path, and the cold path stays identical.
|
|
1175
1268
|
...(prior.grades.length ? { resumeGrades: prior.grades } : {}),
|
|
1269
|
+
// and the replan LEDGER. Same omission class as the two above, with a sharper
|
|
1270
|
+
// cost: a grade seam that never arrives makes a readout say `flat`, while a
|
|
1271
|
+
// ceiling that never arrives is spent by the dead leg and fresh in this one —
|
|
1272
|
+
// a bound the operator signed, widened by a kill (PRD v1.12).
|
|
1273
|
+
...(prior.replans > 0 ? { resumeReplans: { count: prior.replans, grantUsed: prior.replanGrantUsed } } : {}),
|
|
1176
1274
|
...(seed ? { resumeSeed: seed } : {}),
|
|
1275
|
+
// …and WHERE its work is. Same omission class as the three folds above, with its
|
|
1276
|
+
// own cost: a branch that never arrives is not a wrong number but a SECOND branch,
|
|
1277
|
+
// and the leg would carry on beside the tree it was resumed to continue.
|
|
1278
|
+
...(branch ? { resumeBranch: branch } : {}),
|
|
1177
1279
|
bridge,
|
|
1178
1280
|
});
|
|
1179
1281
|
} catch (e) {
|
|
@@ -1233,7 +1335,7 @@ export async function runReuse(opts) {
|
|
|
1233
1335
|
name: name ?? job.job,
|
|
1234
1336
|
goal: job.goal,
|
|
1235
1337
|
specHash: trySpecHash,
|
|
1236
|
-
closeStageNames: (
|
|
1338
|
+
closeStageNames: (closeStagesOf(job) ?? []).map((/** @type {any} */ s) => s.name),
|
|
1237
1339
|
toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
|
|
1238
1340
|
};
|
|
1239
1341
|
row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
|
|
@@ -1366,6 +1468,19 @@ export async function runReuse(opts) {
|
|
|
1366
1468
|
decision: 'The plan flow cannot execute this job\'s close — it runs commands whose exit codes are truth. Nothing was judged, and no workflow can be graded by a close that will not run, so the next try and the cold leg would refuse identically for $0 apiece.',
|
|
1367
1469
|
options: ['restate the close as a staged (or single predicate) close — a spec edit, whose new hash needs re-approval', 'wait for the verdict-classes rung'],
|
|
1368
1470
|
},
|
|
1471
|
+
'branch-red': {
|
|
1472
|
+
// NOT re-filed, for `interpreter-red`'s reason plus one of its own: the runner
|
|
1473
|
+
// already escalated it at the fault's own site, and the fault is the OPERATOR's
|
|
1474
|
+
// state (a patient that is not a git checkout, a crowded branch namespace),
|
|
1475
|
+
// never a library failing — the ledger excludes it for exactly that.
|
|
1476
|
+
refile: false,
|
|
1477
|
+
decision: 'The run could not create its own work branch, and no job runs on the branch it was handed (PRD v1.57 §3). That is a property of the PATIENT, not of any stored workflow: every further try and the cold leg would refuse identically, for $0 apiece.',
|
|
1478
|
+
options: [
|
|
1479
|
+
'make the patient a git checkout with at least one commit',
|
|
1480
|
+
'free the work-branch name (delete or rename the stale branches this run collided with)',
|
|
1481
|
+
'abandon the run',
|
|
1482
|
+
],
|
|
1483
|
+
},
|
|
1369
1484
|
'interpreter-red': {
|
|
1370
1485
|
// NOT re-filed as an escalation. `interpreter-red` is one of the categories the
|
|
1371
1486
|
// ledger CLASSIFIES — it aims a runtime-red at a library — and runJob already
|
|
@@ -1446,6 +1561,12 @@ export async function runReuse(opts) {
|
|
|
1446
1561
|
// this run make progress" and "is this loop out of ideas" are two questions,
|
|
1447
1562
|
// and one instrument answering both is how they come to disagree.
|
|
1448
1563
|
grades: Array.isArray(rs.grades) ? rs.grades : [],
|
|
1564
|
+
// the replan LEDGER the chain has spent — belted like the money and NOT like
|
|
1565
|
+
// the grades, because this one seeds a BOUND: a garbage or negative figure
|
|
1566
|
+
// that read through as a widening is the ceiling being refilled by a corrupt
|
|
1567
|
+
// spine (PRD v1.12). The latch rides beside the count, never derived from it.
|
|
1568
|
+
replans: typeof rs.replans === 'number' && Number.isFinite(rs.replans) && rs.replans > 0 ? Math.floor(rs.replans) : 0,
|
|
1569
|
+
replanGrantUsed: rs.replanGrantUsed === true,
|
|
1449
1570
|
}
|
|
1450
1571
|
: NO_PRIOR;
|
|
1451
1572
|
const remainingCapUsd = envelope.perTryBudgetUsd - prior.spentUsd;
|
|
@@ -1459,7 +1580,10 @@ export async function runReuse(opts) {
|
|
|
1459
1580
|
// `gradesInherited` is a COUNT, and only when there is one to state: the seam
|
|
1460
1581
|
// carries stage names and numbers, never a close byte — the spine is append-only,
|
|
1461
1582
|
// so a gap that crosses here crosses for good.
|
|
1462
|
-
restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {})
|
|
1583
|
+
restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {}),
|
|
1584
|
+
// WHERE it comes back to (v1.57 §3), stated before anything is spent rather than
|
|
1585
|
+
// discovered from the restarted leg's own `work-branch` record afterwards
|
|
1586
|
+
...(rs.branch ? { branch: rs.branch } : {}) } : null,
|
|
1463
1587
|
nextTry: rs ? null : startN,
|
|
1464
1588
|
});
|
|
1465
1589
|
|
|
@@ -1497,7 +1621,7 @@ export async function runReuse(opts) {
|
|
|
1497
1621
|
if (rs.mode === 'cold') {
|
|
1498
1622
|
// every bridge try was already spent before the cold leg started: the loop below
|
|
1499
1623
|
// has nothing left to authorize, and the restart IS the cold attempt
|
|
1500
|
-
coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null };
|
|
1624
|
+
coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null, branch: rs.branch ?? null };
|
|
1501
1625
|
startN = resolved.bridgeTries + 1;
|
|
1502
1626
|
} else {
|
|
1503
1627
|
const entry = loadRegistry(registryDir).bridges.find((/** @type {any} */ b) => b.name === rs.bridge);
|
|
@@ -1513,7 +1637,7 @@ export async function runReuse(opts) {
|
|
|
1513
1637
|
});
|
|
1514
1638
|
}
|
|
1515
1639
|
tried.add(entry.name);
|
|
1516
|
-
const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null));
|
|
1640
|
+
const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null, rs.branch ?? null));
|
|
1517
1641
|
if (stop) return stop;
|
|
1518
1642
|
startN = rs.n + 1;
|
|
1519
1643
|
}
|
|
@@ -1581,7 +1705,7 @@ export async function runReuse(opts) {
|
|
|
1581
1705
|
// the verdict that attempt actually earned.
|
|
1582
1706
|
const inheritedCold = coldRestart ? null : tries.find((t) => t.mode === 'cold' && t.inherited) ?? null;
|
|
1583
1707
|
const cold = coldRestart
|
|
1584
|
-
? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed)
|
|
1708
|
+
? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed, coldRestart.branch)
|
|
1585
1709
|
: inheritedCold ?? await runTry(null, tries.length + 1);
|
|
1586
1710
|
const coldStop = afterTry(cold);
|
|
1587
1711
|
if (coldStop) return coldStop;
|
package/src/root.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// UNRELIABLE red-set for comparison. Imported (never re-spelled) so the detector
|
|
31
31
|
// keys off the exact string the shell emits — a magic string here would drift.
|
|
32
32
|
import { GAP_KEEP_TRIM_MARKER } from './ralph.js';
|
|
33
|
+
import { GAP_TRIM_MARKER } from './kinds.js';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Strip a trailing duration stamp from a kept-failure line. Real `node --test`
|
|
@@ -107,7 +108,13 @@ export function createRoot({ gapKeep, redact = (s) => s, writesInformative = tru
|
|
|
107
108
|
*/
|
|
108
109
|
const keptSet = (gap, pattern) => {
|
|
109
110
|
const re = pattern ? new RegExp(pattern, 'm') : keepRe;
|
|
110
|
-
|
|
111
|
+
// BOTH trim announcements, because there are now two gap renderers: ralph's
|
|
112
|
+
// keep-block cap on a command close, and the kind executor's per-stage line
|
|
113
|
+
// cap on a DECLARED one. A trim the detector cannot see is worse than a
|
|
114
|
+
// missing detector — it makes the strong "reds unchanged" claim off a window
|
|
115
|
+
// that silently dropped failures (finding 5), which is the direction that
|
|
116
|
+
// manufactures a fixation reading out of an instrument that went blind.
|
|
117
|
+
if (!re || gap.includes(GAP_KEEP_TRIM_MARKER) || gap.includes(GAP_TRIM_MARKER)) return null;
|
|
111
118
|
const lines = [...new Set(gap.split('\n').filter((l) => re.test(l)).map(normalizeRedLine))].sort();
|
|
112
119
|
return lines.length ? lines : null;
|
|
113
120
|
};
|
package/src/run.js
CHANGED
|
@@ -116,6 +116,22 @@ async function primitiveSmoke(workdir) {
|
|
|
116
116
|
* chain, not just the leg: without them a leg that re-grades an unchanged tree reports
|
|
117
117
|
* `flat` — "revise the goal" — on a run that was converging when the money ran out.
|
|
118
118
|
* Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
|
|
119
|
+
* @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
|
|
120
|
+
* replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
|
|
121
|
+
* `restart.replanGrantUsed`), forwarded to the plan flow, which seeds its ceiling with
|
|
122
|
+
* it. Its sibling above feeds a READOUT; this one feeds a BOUND, and that is the whole
|
|
123
|
+
* difference: `replanned`/`varianceGrantUsed` are locals in `runPlan`, so without this
|
|
124
|
+
* every kill hands the next leg a fresh allowance of an allowance PRD v1.12 makes the
|
|
125
|
+
* RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
|
|
126
|
+
* run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
|
|
127
|
+
* already sets, so a resume OF a resume folds once. Absent is the cold path.
|
|
128
|
+
* @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed attempt
|
|
129
|
+
* created and recorded on its own spine (`readResume`'s `restart.branch`). The branch
|
|
130
|
+
* name is DERIVED from the signed spec and is therefore identical on every leg, so
|
|
131
|
+
* without this the restart would collide with its own predecessor's branch and the
|
|
132
|
+
* collision walk would mint a `-2` beside the work the resume exists to continue. The
|
|
133
|
+
* fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
|
|
134
|
+
* verbatim to the plan flow; absent is the cold path.
|
|
119
135
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
120
136
|
* territory, threaded to the plan flow. Defaults OFF
|
|
121
137
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
@@ -125,10 +141,10 @@ async function primitiveSmoke(workdir) {
|
|
|
125
141
|
* binding.)
|
|
126
142
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
127
143
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
128
|
-
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
|
|
144
|
+
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
129
145
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
|
|
130
146
|
*/
|
|
131
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [] }) {
|
|
147
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null }) {
|
|
132
148
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
133
149
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
134
150
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -204,6 +220,16 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
204
220
|
// it inherited, and it is the only field that can carry it forward.
|
|
205
221
|
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
206
222
|
...(typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? { priorWallMs } : {}),
|
|
223
|
+
// the REPLAN ledger, on the same declaration and for the same reason — a chain of
|
|
224
|
+
// resumes must fold once. This is the DIRECT-spine half of it: `readResume`'s
|
|
225
|
+
// `direct` mode opens its one implicit window on `job-start` and reads the fold off
|
|
226
|
+
// these very fields, so a U-path run resumed twice inherits leg 1's replans rather
|
|
227
|
+
// than restarting the ceiling at leg 2's window (PRD v1.12). Emitted only when there
|
|
228
|
+
// is one — a decorative `0` is indistinguishable from a run nobody folded, and the
|
|
229
|
+
// grant latch travels beside the count rather than being derived from it.
|
|
230
|
+
...(typeof resumeReplans?.count === 'number' && Number.isFinite(resumeReplans.count) && resumeReplans.count > 0
|
|
231
|
+
? { priorReplans: Math.floor(resumeReplans.count), priorReplanGrantUsed: resumeReplans.grantUsed === true }
|
|
232
|
+
: {}),
|
|
207
233
|
});
|
|
208
234
|
|
|
209
235
|
// 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
|
|
@@ -256,7 +282,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
256
282
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
257
283
|
{
|
|
258
284
|
const outcome = await runPlan(job, {
|
|
259
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades,
|
|
285
|
+
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch,
|
|
260
286
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
261
287
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
262
288
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
package/src/text.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
// are two instruments (F2/F21, the adaptlearn instrument caveat). It replaced
|
|
5
5
|
// N0's stripFences when the fence-robust upgrade landed at N2. priceOf is the
|
|
6
6
|
// ONE spelling of the honest-null cost read for the same reason (F6).
|
|
7
|
+
// `scrubRaw` is the third: the ONE way a model's raw output becomes a persisted
|
|
8
|
+
// record, for the same reason again — a second writer that forgets the scrub is
|
|
9
|
+
// a leak on the very trail the scrub exists to protect.
|
|
10
|
+
|
|
11
|
+
import { redactSecrets } from './validate.js';
|
|
7
12
|
|
|
8
13
|
// A fence counts as the artifact's WRAPPER only when it opens within this many
|
|
9
14
|
// lines of the response — the chatty-preamble shape ("Here's the fix:\n```…").
|
|
@@ -64,3 +69,118 @@ export function priceOf(r) {
|
|
|
64
69
|
unpricedRounds: r?.metrics?.unpricedRounds ?? 0,
|
|
65
70
|
};
|
|
66
71
|
}
|
|
72
|
+
|
|
73
|
+
// ── THE RAW MODEL OUTPUT, AS A PERSISTED RECORD ─────────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// Run mslhn707 died on a survey that broke JSON at position 1339 and left NO
|
|
76
|
+
// copy of what the model actually said: the red named the position, the process
|
|
77
|
+
// exited, and the only artifact that could have decided whether the malformation
|
|
78
|
+
// is recurring (and therefore whether the scout must move to a structure-forced
|
|
79
|
+
// tool call) went with it. A raw that is not written down is a diagnosis nobody
|
|
80
|
+
// can make twice.
|
|
81
|
+
//
|
|
82
|
+
// Everything a persisted raw needs is HERE rather than at each writer, and the
|
|
83
|
+
// reason is the `scrubRed` reason one layer up: the scrub, the bound and the
|
|
84
|
+
// announcement are properties of PERSISTING A RAW, not of any one caller
|
|
85
|
+
// remembering them. A future writer that reaches for a raw reaches for this.
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The ceiling on ONE persisted raw. Sized to the survey's own prompt-ingredient
|
|
89
|
+
* cap (`AUTHOR_SCOUT_BLOB_MAX`, 8000) rather than picked: the largest raw this
|
|
90
|
+
* repository produces is a survey, the archive's real surveys ran 5991-8056
|
|
91
|
+
* bytes, and a record that cannot hold one whole specimen cannot answer the
|
|
92
|
+
* question it exists for. The two constants are deliberately independent
|
|
93
|
+
* numbers — one bounds a PROMPT, this one bounds a RECORD.
|
|
94
|
+
*/
|
|
95
|
+
export const RAW_PERSIST_MAX = 8000;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The announced-trim marker for a persisted raw. A NEW spelling rather than a
|
|
99
|
+
* reuse of `GAP_TRIM_MARKER` ('gap trimmed:'), and that is a decision, not
|
|
100
|
+
* decoration: `src/root.js` READS `GAP_TRIM_MARKER` to mean *this gap was cut,
|
|
101
|
+
* so Layer R must not compare it*, so a second producer of those bytes in a
|
|
102
|
+
* different population is exactly the F90 hazard — a reader must know every
|
|
103
|
+
* producer of its marker. Nothing reads this one yet; it exists so the trim
|
|
104
|
+
* announces itself (F28) and so a future reader has an unambiguous key.
|
|
105
|
+
*/
|
|
106
|
+
export const RAW_TRIM_MARKER = 'raw trimmed:';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* ONE raw model emission, ready to persist.
|
|
110
|
+
*
|
|
111
|
+
* Three things happen here and NONE of them is optional at a call site:
|
|
112
|
+
* - the text goes through the ONE redactor (`redactSecrets`, over the ONE
|
|
113
|
+
* `SECRET_PATTERNS` inventory). A raw is the single most likely record in
|
|
114
|
+
* this system to carry a live credential: it is whatever a model read out
|
|
115
|
+
* of a repository, verbatim, and it lands in a file that outlives the run;
|
|
116
|
+
* - it is BOUNDED, and the bound ANNOUNCES itself with the full size (F28 —
|
|
117
|
+
* a bound eats detail, never evidence). The cut is decoded rather than
|
|
118
|
+
* sliced so a multi-byte character is never split in half;
|
|
119
|
+
* - it carries its own address: which call produced it (`label`, the same
|
|
120
|
+
* spelling the cost book meters under) and which attempt (`attempt`), plus
|
|
121
|
+
* the typed failure that describes it when there is one.
|
|
122
|
+
*
|
|
123
|
+
* `cause`/`reason` are `null` when the emission was fine — an ABSENT failure is
|
|
124
|
+
* absent, never an empty string that reads like a diagnosis nobody wrote.
|
|
125
|
+
* @param {{label: string, attempt: number, text: unknown, cause?: string|null,
|
|
126
|
+
* reason?: string|null, cap?: number}} o
|
|
127
|
+
* @returns {{label: string, attempt: number, bytes: number, trimmed: boolean,
|
|
128
|
+
* text: string, cause: string|null, reason: string|null}}
|
|
129
|
+
*/
|
|
130
|
+
export function scrubRaw({ label, attempt, text, cause = null, reason = null, cap = RAW_PERSIST_MAX }) {
|
|
131
|
+
const scrubbed = redactSecrets(String(text ?? ''));
|
|
132
|
+
const buf = Buffer.from(scrubbed, 'utf8');
|
|
133
|
+
const trimmed = buf.length > cap;
|
|
134
|
+
// walk the cut BACK off any continuation byte (0b10xxxxxx): a byte-wise slice
|
|
135
|
+
// through a multi-byte character renders as a replacement char, and a record
|
|
136
|
+
// that garbles the evidence is a worse instrument than one that keeps a byte
|
|
137
|
+
// less of it
|
|
138
|
+
let end = cap;
|
|
139
|
+
while (trimmed && end > 0 && (buf[end] & 0xC0) === 0x80) end -= 1;
|
|
140
|
+
const kept = trimmed ? buf.subarray(0, end).toString('utf8') : scrubbed;
|
|
141
|
+
// the two diagnosis fields are rendered by `stampRaw` below, never here: the
|
|
142
|
+
// writer that stamps them LATER must go through the same one place, or the
|
|
143
|
+
// scrub becomes a property of when you knew the verdict
|
|
144
|
+
return stampRaw({
|
|
145
|
+
label,
|
|
146
|
+
attempt,
|
|
147
|
+
// the FULL size, always — the number a trim is measured against
|
|
148
|
+
bytes: buf.length,
|
|
149
|
+
trimmed,
|
|
150
|
+
// WITHHELD is measured off `end`, not off `cap`: the multi-byte walk-back above
|
|
151
|
+
// can cut up to three bytes short of the cap, and announcing `length - cap` would
|
|
152
|
+
// under-report the withheld bytes by exactly that much. An announced bound that
|
|
153
|
+
// states a number it did not withhold is the announcement failing at its one job.
|
|
154
|
+
text: trimmed
|
|
155
|
+
? `${kept}\n[${RAW_TRIM_MARKER} ${buf.length - end} of ${buf.length} bytes withheld — the cap is ${cap} per raw]`
|
|
156
|
+
: kept,
|
|
157
|
+
cause: null,
|
|
158
|
+
reason: null,
|
|
159
|
+
}, { cause, reason });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stamp a persisted raw with the verdict that DESCRIBES it — through the same
|
|
164
|
+
* boundary the raw's own text rode.
|
|
165
|
+
*
|
|
166
|
+
* It exists because the verdict is not known when the raw is created: a writer
|
|
167
|
+
* records the emission the moment the call returns (so a metered call can never
|
|
168
|
+
* be missing its output), and only classifies it afterwards. The obvious
|
|
169
|
+
* shape — assigning `cause`/`reason` onto the stored object at that later
|
|
170
|
+
* point — writes an UNSCRUBBED string into a record that is already past the
|
|
171
|
+
* one redactor, and `reason` is the worst field to do it to: it quotes the
|
|
172
|
+
* transport's own error prose, which nobody in this repo wrote and nobody
|
|
173
|
+
* bounds. So the stamp is a function, not an assignment, and `scrubRaw` renders
|
|
174
|
+
* its own two fields through it: ONE place decides how a diagnosis becomes
|
|
175
|
+
* persistable, exactly as ONE place decides it for the text.
|
|
176
|
+
*
|
|
177
|
+
* `cause` is enumerated (`SURVEY_CAUSES`) and passes through untouched; a mask
|
|
178
|
+
* over a closed set would only hide a bug in the set.
|
|
179
|
+
* @template {{cause: string|null, reason: string|null}} T
|
|
180
|
+
* @param {T} raw @param {{cause?: string|null, reason?: string|null}} o @returns {T}
|
|
181
|
+
*/
|
|
182
|
+
export function stampRaw(raw, { cause = null, reason = null }) {
|
|
183
|
+
raw.cause = cause ?? null;
|
|
184
|
+
raw.reason = reason === null || reason === undefined ? null : redactSecrets(reason);
|
|
185
|
+
return raw;
|
|
186
|
+
}
|