bareloop 0.27.0 → 0.29.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 +114 -0
- package/bareloop.context.md +245 -33
- package/package.json +1 -1
- package/src/authorreadout.js +323 -0
- package/src/authorrun.js +1345 -0
- package/src/behaviour.js +5 -2
- package/src/cli.js +190 -8
- package/src/index.js +13 -1
- package/src/interviewrun.js +842 -0
- package/src/panel/index.html +1910 -0
- package/src/panel/server.js +1527 -0
- package/src/planrun.js +1 -1
- package/src/replay.js +632 -13
- package/src/replayio.js +159 -0
- package/src/reuse.js +1 -1
- package/src/run.js +1 -1
- package/src/runlist.js +281 -0
- package/src/u-patient.js +73 -0
- package/src/u-readout.js +451 -0
- package/src/userrun.js +2299 -0
- package/types/authorreadout.d.ts +172 -0
- package/types/authorrun.d.ts +37 -0
- package/types/behaviour.d.ts +5 -3
- package/types/cli.d.ts +2 -1
- package/types/index.d.ts +2 -1
- package/types/interviewrun.d.ts +46 -0
- package/types/panel/server.d.ts +265 -0
- package/types/planrun.d.ts +2 -2
- package/types/replay.d.ts +104 -6
- package/types/replayio.d.ts +93 -0
- package/types/reuse.d.ts +2 -2
- package/types/run.d.ts +2 -2
- package/types/runlist.d.ts +112 -0
- package/types/u-patient.d.ts +39 -0
- package/types/u-readout.d.ts +247 -0
- package/types/userrun.d.ts +141 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// The authoring runner's SIGNING readout — the goal, and the stages that will
|
|
2
|
+
// judge it — in the one place a test can reach it.
|
|
3
|
+
//
|
|
4
|
+
// PANEL-BUILD.md P0 task 4/4 — moved here (was `scripts/author-readout.mjs`)
|
|
5
|
+
// because its callers (`src/authorrun.js`, `src/interviewrun.js`) now live
|
|
6
|
+
// under `src/`, and `tsconfig.json`'s `rootDir: ./src` refuses an import that
|
|
7
|
+
// reaches back out into `scripts/`. This module carries no executable
|
|
8
|
+
// top-level code and no logic changed in the move — only the three relative
|
|
9
|
+
// import paths below, from `../src/x.js` to `./x.js`.
|
|
10
|
+
//
|
|
11
|
+
// The lines a person actually signs against live here rather than inline in
|
|
12
|
+
// the runner, and the runner calls them — the same reason `src/u-readout.js`
|
|
13
|
+
// exists.
|
|
14
|
+
//
|
|
15
|
+
// F87 is the whole point of the pairing. The goal must state everything the close
|
|
16
|
+
// will judge, and NOTHING derives one from the other or checks them against each
|
|
17
|
+
// other; an unstated stage is a cost the run discovers at its tail. The only
|
|
18
|
+
// defence is that the person signing reads both halves at once — and neither
|
|
19
|
+
// signing surface offered that: this one printed the declaration and never the
|
|
20
|
+
// goal, run-u's --approve gate printed the goal and never the declaration.
|
|
21
|
+
// A half of a reading is not a smaller reading; it is a different one.
|
|
22
|
+
//
|
|
23
|
+
// The ceiling parse and the crash record below are here for the same one reason,
|
|
24
|
+
// not because they are readouts: the runner is a script, and a rule no test can
|
|
25
|
+
// reach is a rule nothing checks.
|
|
26
|
+
import { scrubRaw } from './text.js';
|
|
27
|
+
import { redactSecrets } from './validate.js';
|
|
28
|
+
import { judgedStages } from './kinds.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {{goal?: string|null, closeDecl?: any}} spec the RESOLVED spec — the bytes
|
|
32
|
+
* that get hashed and signed, never the operator's draft and never the authored
|
|
33
|
+
* half on its own.
|
|
34
|
+
* @returns {string[]} one console line each, in print order.
|
|
35
|
+
*/
|
|
36
|
+
export function declarationLines({ goal, closeDecl }) {
|
|
37
|
+
// An absent goal reads as ABSENT. A bare `goal` label with nothing after it is
|
|
38
|
+
// indistinguishable from a goal that says nothing, and this readout exists to
|
|
39
|
+
// make exactly that kind of silence visible.
|
|
40
|
+
const lines = [`goal ${goal === undefined || goal === null || goal === '' ? '(none — the draft carried no goal, and a close judges against one)' : JSON.stringify(goal)}`];
|
|
41
|
+
lines.push('declaration');
|
|
42
|
+
for (const s of closeDecl?.stages ?? []) {
|
|
43
|
+
lines.push(` ${s.name} [${s.kind}]${s.offer === false ? ' (not lendable)' : ''}${(s.needs ?? []).length ? ` needs: ${s.needs.join(', ')}` : ''}`);
|
|
44
|
+
lines.push(` params ${JSON.stringify(s.params ?? {})}`);
|
|
45
|
+
}
|
|
46
|
+
for (const n of closeDecl?.notes ?? []) lines.push(` note: ${n}`);
|
|
47
|
+
return lines;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* F176'S FALLBACK, SAID OUT LOUD — the revise ladder kept the NEWEST SOUND
|
|
52
|
+
* declaration rather than the last one it happened to try, because "kept the
|
|
53
|
+
* last accepted revision" is not the same claim as "kept the best revision"
|
|
54
|
+
* (docs/logs/FINDINGS.md F176). Absent on every run where the fallback never
|
|
55
|
+
* fired (the overwhelming majority), which is why this returns an EMPTY list
|
|
56
|
+
* rather than a "no fallback" line — a green close has nothing to say here and
|
|
57
|
+
* printing it every run would be noise, not information.
|
|
58
|
+
* @param {{fellBack?: {from: string, to: string, brokenStages: string[]}|null}|null} authoring `authored.authoring`
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
export function fellBackLines(authoring) {
|
|
62
|
+
const fb = authoring?.fellBack;
|
|
63
|
+
if (!fb) return [];
|
|
64
|
+
const stages = fb.brokenStages?.length ? fb.brokenStages.join(', ') : '(unnamed)';
|
|
65
|
+
return [`FELL BACK ${fb.from} broke ${fb.brokenStages?.length ?? 0} check(s) (${stages}) — kept ${fb.to} instead`];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* THE TWO SIGNED JUDGED ARTIFACTS, as a person reads them before signing
|
|
70
|
+
* (softgreen modules 4+5). Absent on a close that judges nothing, which is why
|
|
71
|
+
* this returns an EMPTY list rather than a "no card" line: a green close has no
|
|
72
|
+
* rubric and saying so every run is noise, not information.
|
|
73
|
+
*
|
|
74
|
+
* The CARD is printed in full — it is short, it is the whole standard the judge
|
|
75
|
+
* will hold the work to, and it is the signer's own words. The CASES are printed
|
|
76
|
+
* as a ROSTER (id, verdict, itemized reds) rather than in full: ten real source
|
|
77
|
+
* artifacts is a page of code the terminal cannot usefully show, and what a
|
|
78
|
+
* signer checks at this surface is that both polarities are there and that each
|
|
79
|
+
* red names something they recognise. The artifacts themselves are in the spec
|
|
80
|
+
* file, which is named right above this block.
|
|
81
|
+
* @param {{closeDecl?: any}} spec the RESOLVED spec
|
|
82
|
+
* @returns {string[]}
|
|
83
|
+
*/
|
|
84
|
+
export function rubricLines({ closeDecl }) {
|
|
85
|
+
const judged = judgedStages(closeDecl);
|
|
86
|
+
if (!judged.length) return [];
|
|
87
|
+
/** @type {string[]} */
|
|
88
|
+
const lines = ['rubric card (what the judge will hold the work to — YOUR words, signed)'];
|
|
89
|
+
for (const it of judged[0]?.params?.card?.items ?? []) lines.push(` [${it.rule}] ${it.text}`);
|
|
90
|
+
const cases = closeDecl?.calibration?.cases ?? null;
|
|
91
|
+
if (cases === null) {
|
|
92
|
+
lines.push('calibration NONE STORED — a judged close is not signable without one');
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
const pass = cases.filter((/** @type {any} */ c) => c?.expect?.verdict === 'pass').length;
|
|
96
|
+
lines.push(`calibration set (${cases.length} case(s): ${pass} pass, ${cases.length - pass} red)`);
|
|
97
|
+
for (const c of cases) {
|
|
98
|
+
const reds = (c?.expect?.reds ?? []).map((/** @type {any} */ r) => `${r.rule}@${r.fn}`).join(', ');
|
|
99
|
+
lines.push(` ${String(c?.expect?.verdict ?? '?').padEnd(4)} ${c?.id}${reds ? ` → ${reds}` : ''}`);
|
|
100
|
+
}
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* THE CONFIRM TURN'S OPEN QUESTIONS (PRD item 33 M3 piece 4, ruling 5's D3
|
|
106
|
+
* addendum) — shown at the SIGNING readout, from `authored.json`'s own
|
|
107
|
+
* `confirmed.openQuestions`, never added to the signed spec format itself
|
|
108
|
+
* (D4: the closeDecl's `notes` are the model's own record; this is the
|
|
109
|
+
* person's "fix" text from a round that hit the 2-round cap, ruling 5,
|
|
110
|
+
* passed to the composer verbatim rather than spending a 3rd call, D3). A
|
|
111
|
+
* close with none is shown as `(none)` rather than an empty line — the
|
|
112
|
+
* absence is itself part of what the signer reads before signing.
|
|
113
|
+
* @param {{openQuestions?: string[]}|null} confirmed `authored.confirmed`
|
|
114
|
+
* @returns {string[]}
|
|
115
|
+
*/
|
|
116
|
+
export function openQuestionLines(confirmed) {
|
|
117
|
+
const qs = confirmed?.openQuestions ?? [];
|
|
118
|
+
if (!qs.length) return ['open questions (none)'];
|
|
119
|
+
const lines = [`open questions ${qs.length} — the confirm turn hit its 2-round cap with a "fix" still pending; read `
|
|
120
|
+
+ 'these before you sign'];
|
|
121
|
+
for (const q of qs) lines.push(` ? ${q}`);
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* F175's open half, said at the SIGNING readout too (run mu0voeo4, hamr's
|
|
127
|
+
* 2026-09-16 ruling) — every question the plan raised that "Confirm" or
|
|
128
|
+
* "Type the goal yourself" forced the person to answer inline, from
|
|
129
|
+
* `authored.json`'s own `confirmed.answeredQuestions`. Shown only when
|
|
130
|
+
* non-empty (the overwhelming majority of plans raise no question at all,
|
|
131
|
+
* and printing an empty block every run would be noise, not information) —
|
|
132
|
+
* unlike {@link openQuestionLines}, an empty list here says nothing, because
|
|
133
|
+
* absence is the ordinary case, not a fact the signer needs flagged.
|
|
134
|
+
* @param {{answeredQuestions?: string[]}|null} confirmed `authored.confirmed`
|
|
135
|
+
* @returns {string[]}
|
|
136
|
+
*/
|
|
137
|
+
export function answeredQuestionLines(confirmed) {
|
|
138
|
+
const qas = confirmed?.answeredQuestions ?? [];
|
|
139
|
+
if (!qas.length) return [];
|
|
140
|
+
const lines = [`answered questions ${qas.length} — the plan raised these and the person answered them inline before signing`];
|
|
141
|
+
for (const qa of qas) for (const line of String(qa).split('\n')) lines.push(` ${line}`);
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* THE CALIBRATION GATE'S OWN READOUT — the one gate that spends money, and the
|
|
147
|
+
* one whose rows a signer has to read case by case.
|
|
148
|
+
*
|
|
149
|
+
* ITEMIZED, never an aggregate percentage: "9/10" tells a person nothing about
|
|
150
|
+
* which line of their own rubric is wrong, and the fix for a miss is a card line
|
|
151
|
+
* or a corrected case (§4.3's ceiling). A CASUALTY prints under its own name and
|
|
152
|
+
* says so, because a dead judge is not a failed set.
|
|
153
|
+
* @param {any} calibration `signing.gates.calibration`
|
|
154
|
+
* @returns {string[]}
|
|
155
|
+
*/
|
|
156
|
+
export function calibrationLines(calibration) {
|
|
157
|
+
if (!calibration) return [' 4 calibration not reached'];
|
|
158
|
+
if (calibration.stop === 'calibration-missing') {
|
|
159
|
+
return [' 4 calibration FAIL — this close judges, and no calibration set is stored with it'];
|
|
160
|
+
}
|
|
161
|
+
if (calibration.stop === 'no-judge') {
|
|
162
|
+
return [' 4 calibration FAIL — no judge seam was wired, so the gate never ran (a wiring gap, never a pass)'];
|
|
163
|
+
}
|
|
164
|
+
const graded = calibration.graded ?? [];
|
|
165
|
+
const styles = calibration.injection?.styles ?? [];
|
|
166
|
+
/** @type {string[]} */
|
|
167
|
+
const lines = [` 4 calibration ${calibration.ok ? 'PASS' : 'FAIL'} — ${graded.filter((/** @type {any} */ g) => g.ok).length}/`
|
|
168
|
+
+ `${graded.length} case(s) graded correctly, ${styles.filter((/** @type {any} */ s) => s.resisted).length}/${styles.length} `
|
|
169
|
+
+ `injection style(s) resisted [judge ${calibration.judgeModel}]`];
|
|
170
|
+
if (calibration.casualty) {
|
|
171
|
+
lines.push(` CASUALTY on ${calibration.casualty.kind} "${calibration.casualty.at}" [${calibration.casualty.axis}] `
|
|
172
|
+
+ '— a broken judge is no evidence about the set');
|
|
173
|
+
}
|
|
174
|
+
for (const g of graded.filter((/** @type {any} */ x) => !x.ok)) lines.push(` WRONG ${g.id}: ${g.detail}`);
|
|
175
|
+
for (const s of styles.filter((/** @type {any} */ x) => !x.resisted)) lines.push(` LEAK ${s.style}: ${s.detail}`);
|
|
176
|
+
lines.push(` certified card ${String(calibration.cardHash ?? 'unknown').slice(0, 12)} cases `
|
|
177
|
+
+ `${String(calibration.casesHash ?? 'unknown').slice(0, 12)} set ${String(calibration.setHash ?? 'unknown').slice(0, 12)}`);
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* THE AUTHORING CEILING, parsed from `--budget`. Housed here for the same reason
|
|
183
|
+
* the readout above is: the runner is a script, and a rule no test can reach is a
|
|
184
|
+
* rule nothing checks.
|
|
185
|
+
*
|
|
186
|
+
* ABSENT IS UNBOUNDED, and that is the only way to get there. A malformed value
|
|
187
|
+
* is an ERROR rather than a fallback: `--budget banana` silently collapsing to
|
|
188
|
+
* "no ceiling" is precisely the failure this flag exists to prevent, wearing the
|
|
189
|
+
* operator's own typo. Zero and negatives are rejected on the same rule — a
|
|
190
|
+
* ceiling that can fund nothing is a typo for "unbounded" far more often than it
|
|
191
|
+
* is a request, and the honest way to ask for nothing is not to ask.
|
|
192
|
+
*
|
|
193
|
+
* There is NO DEFAULT anywhere in this function, deliberately: a defaulted cap is
|
|
194
|
+
* a silent second ceiling (the `maxWallMs` precedent), and the number is the
|
|
195
|
+
* operator's to set or to decline.
|
|
196
|
+
* @param {string|null} raw the argv value, or null when the flag was not given
|
|
197
|
+
* @returns {{ceilingUsd: number|null, error: string|null}}
|
|
198
|
+
*/
|
|
199
|
+
export function parseCeiling(raw) {
|
|
200
|
+
if (raw === null) return { ceilingUsd: null, error: null };
|
|
201
|
+
const n = Number(raw);
|
|
202
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
203
|
+
return {
|
|
204
|
+
ceilingUsd: null,
|
|
205
|
+
error: `--budget ${raw} is not a positive number of dollars — omit the flag entirely to run UNBOUNDED, which is `
|
|
206
|
+
+ 'stated on stdout; there is no way to ask for a ceiling and get none by accident',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return { ceilingUsd: n, error: null };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The ceiling's header line. An UNBOUNDED run is ANNOUNCED — printed before the
|
|
214
|
+
* provider is built, so it reaches stdout ahead of the first paid byte rather
|
|
215
|
+
* than being inferred from the total afterwards. An unbounded run is legal; it is
|
|
216
|
+
* never allowed to be an accident.
|
|
217
|
+
* @param {number|null} ceilingUsd
|
|
218
|
+
*/
|
|
219
|
+
export function ceilingLine(ceilingUsd) {
|
|
220
|
+
return ceilingUsd === null
|
|
221
|
+
? 'budget UNBOUNDED — no --budget was given, so nothing stops this pipeline spending; spend is reported, never capped'
|
|
222
|
+
: `budget $${ceilingUsd} ceiling — the pipeline stops BETWEEN metered calls once the spend reaches it`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* ONE PROGRESS PHASE, as a console line.
|
|
227
|
+
*
|
|
228
|
+
* The pipeline used to run up to ~15 minutes saying nothing between
|
|
229
|
+
* `author-start` and its result — a real survey ladder, a real declaration
|
|
230
|
+
* ladder, and a real toolchain per close stage, all inside one await. Silence
|
|
231
|
+
* and a hang are the same bytes on a terminal, and the operator's only lever is
|
|
232
|
+
* to kill a run that may be working.
|
|
233
|
+
*
|
|
234
|
+
* It says WHAT is happening, never how far along it is: there is no honest
|
|
235
|
+
* fraction to print (a survey attempt has no progress, and a suite stage
|
|
236
|
+
* finishes when it finishes), and an invented percentage is a number nobody
|
|
237
|
+
* measured. What it does carry is the two facts a person waiting actually uses —
|
|
238
|
+
* which phase, and how long the last thing took.
|
|
239
|
+
*
|
|
240
|
+
* `durationMs` prints as UNKNOWN when it is absent rather than as 0 (F6's rule,
|
|
241
|
+
* in its time form): a stage whose duration was never measured did not take no
|
|
242
|
+
* time.
|
|
243
|
+
* @param {string} phase @param {any} [data]
|
|
244
|
+
* @returns {string}
|
|
245
|
+
*/
|
|
246
|
+
export function phaseLine(phase, data = {}) {
|
|
247
|
+
const ms = (/** @type {any} */ v) => (typeof v === 'number' && Number.isFinite(v) ? `${(v / 1000).toFixed(1)}s` : 'unknown');
|
|
248
|
+
switch (phase) {
|
|
249
|
+
case 'seed': return '· reading the seed commit…';
|
|
250
|
+
case 'scout': return `· scout running (up to ${data.attempts ?? '?'} attempt(s)) — a real survey over the repository…`;
|
|
251
|
+
case 'scout-done': return `· scout ${data.state ?? 'unknown'} — ${data.facts ?? 0} fact(s)`;
|
|
252
|
+
case 'listing': return '· listing the files the declaration may name…';
|
|
253
|
+
case 'listing-done': return `· listing ${data.stop ? `STOPPED (${data.stop})` : `${data.files ?? 'unknown'} file(s)`}`;
|
|
254
|
+
case 'author': return '· authoring the close declaration…';
|
|
255
|
+
case 'author-call': return `· ${data.call} (call ${(data.i ?? 0) + 1} of up to ${(data.of ?? 0) + 1})…`;
|
|
256
|
+
case 'seed-read': return `· measuring ${data.stages ?? '?'} stage(s) at the seed — each one runs a real toolchain…`;
|
|
257
|
+
case 'stage': return `· ${data.stage} [${data.kind}] ${data.verdict} — ${ms(data.durationMs)}`;
|
|
258
|
+
case 'seed-read-done': return `· seed read done (${data.stages ?? '?'} stage(s))`;
|
|
259
|
+
// softgreen modules 4+5: the compile, and the paid gate that grades it
|
|
260
|
+
case 'rubric': return `· compiling the rubric card and ${data.size ?? '?'} calibration cases from your own answers…`;
|
|
261
|
+
case 'rubric-done': return `· rubric ${data.ok ? 'compiled' : `NOT compiled (${data.stop ?? 'unknown'})`} after ${data.attempts ?? '?'} attempt(s)`;
|
|
262
|
+
case 'rubric-scrubbed': return `· MASKED before storing: ${(data.paths ?? []).join(', ')} — the stored bytes differ from what was typed`;
|
|
263
|
+
case 'calibration': return `· calibration gate: ${data.cases ?? '?'} case(s) + ${data.styles ?? '?'} injection style(s), one real judge call each…`;
|
|
264
|
+
// A phase this renderer does not know is PRINTED, not swallowed: the callers
|
|
265
|
+
// are the library's own seams and a new one appearing unrendered is how a
|
|
266
|
+
// readout silently stops covering what it reports on.
|
|
267
|
+
default: return `· ${phase} ${JSON.stringify(data ?? {})}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** an ABSENT field stays absent — `null`, never a filled-in guess — and every
|
|
272
|
+
* field that survives goes through the ONE redactor on its way to a file that
|
|
273
|
+
* outlives the run. `name`/`message`/`code` are short, but `message` is the field
|
|
274
|
+
* most likely in this whole record to quote a path, a URL or an environment value
|
|
275
|
+
* back at us, so it rides the same inventory as everything else.
|
|
276
|
+
* @param {unknown} v */
|
|
277
|
+
const field = (v) => (v === undefined || v === null ? null : redactSecrets(String(v)));
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* ONE crashed authoring run, ready for the spine.
|
|
281
|
+
*
|
|
282
|
+
* It exists because a crash used to leave NO body. `run-author.mjs` awaited the
|
|
283
|
+
* paid pipeline at the top level, so a throw anywhere inside it went to the
|
|
284
|
+
* operator's terminal as an unhandled rejection and the spine kept exactly one
|
|
285
|
+
* line — `author-start` — which is byte-for-byte what a run still in flight looks
|
|
286
|
+
* like. The terminal scrolls; the spine is the record.
|
|
287
|
+
*
|
|
288
|
+
* The STACK is the evidence (the incident that minted this had no idea WHERE the
|
|
289
|
+
* throw came from), and a stack is the one field here that can be arbitrarily
|
|
290
|
+
* long and can quote anything the process had open. So it goes through `scrubRaw`
|
|
291
|
+
* — the ONE persist boundary for a raw blob — which redacts it over the same
|
|
292
|
+
* `SECRET_PATTERNS` inventory the validator reds on and bounds it with the bound
|
|
293
|
+
* announcing its own size (F28). Nothing here is hand-rolled: a second spelling
|
|
294
|
+
* of either rule is how the two drift.
|
|
295
|
+
*
|
|
296
|
+
* A non-Error throw (a string, a plain object) is honest rather than coerced into
|
|
297
|
+
* an Error shape: it has no `name`, so `name` is `null`, and `String(err)` is what
|
|
298
|
+
* lands in the raw.
|
|
299
|
+
* @param {unknown} err whatever was thrown
|
|
300
|
+
* @returns {{name: string|null, message: string|null, code: string|null,
|
|
301
|
+
* raw: ReturnType<typeof scrubRaw>}}
|
|
302
|
+
*/
|
|
303
|
+
export function crashRecord(err) {
|
|
304
|
+
const e = /** @type {any} */ (err);
|
|
305
|
+
const obj = e !== null && (typeof e === 'object' || typeof e === 'function');
|
|
306
|
+
const stack = obj && typeof e.stack === 'string' && e.stack ? e.stack : String(err);
|
|
307
|
+
// ONE level of `cause`, and no recursion: a wrapped throw names its origin in
|
|
308
|
+
// exactly that field, and `scrubRaw` already knows how a diagnosis becomes
|
|
309
|
+
// persistable (it redacts `reason` through the same inventory). Walking a chain
|
|
310
|
+
// would be speculative code standing over a shape nothing here has produced.
|
|
311
|
+
const cause = obj ? e.cause : undefined;
|
|
312
|
+
return {
|
|
313
|
+
name: field(obj ? e.name : undefined),
|
|
314
|
+
message: field(obj ? e.message : undefined),
|
|
315
|
+
code: field(obj ? e.code : undefined),
|
|
316
|
+
raw: scrubRaw({
|
|
317
|
+
label: 'author-crash',
|
|
318
|
+
attempt: 1,
|
|
319
|
+
text: stack,
|
|
320
|
+
reason: cause === undefined || cause === null ? null : String(cause?.message ?? cause),
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
}
|