gooseworks 0.5.2 → 0.5.3

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.
@@ -60,6 +60,7 @@ const errors_1 = require("./errors");
60
60
  const files_1 = require("./files");
61
61
  const paths_1 = require("./paths");
62
62
  const pieces_1 = require("./pieces");
63
+ const plain_words_1 = require("./plain-words");
63
64
  const progress_1 = require("./progress");
64
65
  const progress_reporter_1 = require("./progress-reporter");
65
66
  const save_1 = require("./save");
@@ -81,11 +82,46 @@ const ASPECT_SIZE = {
81
82
  '4:5': { width: 1080, height: 1350 },
82
83
  '16:9': { width: 1920, height: 1080 },
83
84
  };
85
+ const CHECK_FAILED = 'The video didn’t pass the final check.';
86
+ /** For a failure that repeats on every run of the same plan. */
87
+ const PLAN_CHANGE = 'This video can’t be made from this plan. Change the plan, then make it again.';
88
+ /** The server's `failure.code` shape. */
89
+ const FAILURE_CODE = /^[a-z][a-z0-9_]{0,39}$/;
90
+ /** Codes the server keeps the video in Making for (gooseworks-app video-line/run-failure.ts RESUMABLE_CODES). */
91
+ const RESUMABLE_CODES = new Set(['timeout', 'tool_failed', 'needs_missing']);
92
+ /** A part that would not load, in plain words: which part and why go to the log. */
93
+ function loadStop(error) {
94
+ const code = error instanceof loader_1.PartLoadError ? error.code : undefined;
95
+ switch (code) {
96
+ case 'kit_range':
97
+ return new errors_1.KitStop('This video needs a newer video kit. Nothing was spent.', 'update_kit', code);
98
+ case 'unreachable':
99
+ return new errors_1.KitStop('This video’s parts could not be downloaded right now. Run the same command again in a minute. Nothing was spent.', 'stop', code);
100
+ case 'bad_cache':
101
+ return new errors_1.KitStop('The video kit’s folder on this computer is busy or can’t be used. Run the same command again in a minute. Nothing was spent.', 'stop', code);
102
+ case 'withdrawn':
103
+ return new errors_1.KitStop(`A part this video uses was withdrawn. ${PLAN_CHANGE} Nothing was spent.`, 'change_request', code);
104
+ default:
105
+ return new errors_1.KitStop(`A part this video uses can’t run. ${PLAN_CHANGE} Nothing was spent.`, 'change_request', code);
106
+ }
107
+ }
84
108
  function lineWords(error) {
85
109
  return [error.message, error.fix].filter(Boolean).join(' ');
86
110
  }
111
+ /**
112
+ * The code the line hears for a step's last failure. A provider failure a later run can get past
113
+ * (its piece has an attempt left, or no piece is named) is reported as `tool_failed`, which keeps
114
+ * the video in Making; the line ends the video on `provider_failed`.
115
+ */
116
+ function reportedCode(error) {
117
+ if (error.code !== 'provider_failed')
118
+ return error.code;
119
+ return error instanceof pieces_1.PieceFailure && error.spent ? 'provider_failed' : 'tool_failed';
120
+ }
87
121
  /** Plain words for a part's failure: the line's own words when it said them, never a part's detail. */
88
122
  function failureWords(error) {
123
+ if (error instanceof pieces_1.PieceFailure && error.spent)
124
+ return pieces_1.FAILED_TWICE;
89
125
  if (error instanceof pieces_1.PieceFailure)
90
126
  return [error.line.error, error.line.fix].filter(Boolean).join(' ');
91
127
  switch (error.code) {
@@ -97,6 +133,9 @@ function failureWords(error) {
97
133
  return 'This computer is missing something this video needs. Run the computer check, then make it again.';
98
134
  case 'timeout':
99
135
  return 'Part of this video took too long. Run the same command again; what was made so far is kept.';
136
+ case 'bad_input':
137
+ case 'output_invalid':
138
+ return PLAN_CHANGE;
100
139
  default:
101
140
  return 'Part of this video could not be made. Run the same command again; what was made so far is kept.';
102
141
  }
@@ -169,6 +208,9 @@ class Maker {
169
208
  parts = new Map();
170
209
  hosted = new Map();
171
210
  pieceLocks = new Map();
211
+ /** Where the run is, for the failure report: a step id or a stage before or after the steps. */
212
+ at = 'hand-over';
213
+ failureSent = false;
172
214
  constructor(videoId, deps) {
173
215
  this.videoId = videoId;
174
216
  this.deps = deps;
@@ -224,6 +266,11 @@ class Maker {
224
266
  this.store = new save_1.RunStore(this.layout);
225
267
  this.cache = new pieces_1.PieceCache(this.layout.pieces);
226
268
  this.reporter = new progress_reporter_1.ProgressReporter(deps.line, this.videoId, this.book, deps.log, (message, reason, code) => this.halt(new errors_1.KitStop(message, reason, code)), deps.heartbeatMs, this.stop.signal);
269
+ const replayed = await this.replayPending();
270
+ if (replayed)
271
+ return replayed;
272
+ if (this.fatal)
273
+ throw this.fatal;
227
274
  // Reports start now, so checking the style and parts never looks quiet.
228
275
  this.reporter.start();
229
276
  void this.reporter.send(true);
@@ -235,9 +282,71 @@ class Maker {
235
282
  finally {
236
283
  this.reporter?.stopTimer();
237
284
  await this.reporter?.flush().catch(() => undefined);
285
+ await this.keepPendingReport().catch((error) => deps.log.write('warn', 'the failure report could not be saved', { error: error instanceof Error ? error.message : String(error) }));
238
286
  await release();
239
287
  }
240
288
  }
289
+ reportKey() {
290
+ try {
291
+ return {
292
+ quote_id: this.handed.quote_id,
293
+ plan_sha256: (0, canonical_1.canonicalHash)((0, secrets_1.withoutSignedLinks)(this.handed.plan)),
294
+ lock_sha256: this.handed.parts_lock ? (0, canonical_1.canonicalHash)(this.handed.parts_lock) : '',
295
+ kit: version_1.KIT_VERSION,
296
+ toolchain: this.toolchain,
297
+ };
298
+ }
299
+ catch {
300
+ return null;
301
+ }
302
+ }
303
+ /**
304
+ * An earlier run's failure report the line never took goes before anything else, when it still
305
+ * stands: the same plan, parts, kit and tools, and a code no new run gets past. Otherwise this
306
+ * run reports for itself.
307
+ */
308
+ async replayPending() {
309
+ const saved = await (0, save_1.readJson)(this.layout.pendingReport);
310
+ if (!saved)
311
+ return null;
312
+ const key = this.reportKey();
313
+ const stands = saved.report === 1 &&
314
+ !!saved.failure &&
315
+ typeof saved.failure.code === 'string' &&
316
+ typeof saved.failure.detail === 'string' &&
317
+ !RESUMABLE_CODES.has(saved.failure.code) &&
318
+ !!key &&
319
+ Object.keys(key).every((field) => saved[field] === key[field]);
320
+ if (!stands) {
321
+ this.deps.log.write('info', 'an earlier run’s failure report no longer stands', { code: saved.failure?.code ?? null });
322
+ await (0, promises_1.rm)(this.layout.pendingReport, { force: true });
323
+ return null;
324
+ }
325
+ const sent = await this.reporter.replay(saved.failure);
326
+ this.deps.log.write(sent === 'taken' ? 'info' : 'warn', 'an earlier run’s failure report', { sent, step: saved.failure.step, code: saved.failure.code });
327
+ if (sent === 'unsent')
328
+ return null;
329
+ await (0, promises_1.rm)(this.layout.pendingReport, { force: true });
330
+ if (sent === 'refused')
331
+ return null;
332
+ this.failureSent = true;
333
+ return { status: 'failed', message: saved.failure.detail };
334
+ }
335
+ /** Keeps a failure report the line never took for the next run; one taken, or a finished video, clears it. */
336
+ async keepPendingReport() {
337
+ if (!this.reporter || !this.layout)
338
+ return;
339
+ const unsent = this.reporter.undelivered;
340
+ const key = unsent ? this.reportKey() : null;
341
+ if (unsent && key) {
342
+ const pending = { report: 1, ...key, failure: unsent, saved_at: this.now().toISOString() };
343
+ await (0, save_1.writeJson)(this.layout.pendingReport, pending);
344
+ this.deps.log.write('warn', 'the failure report was not taken; the next run sends it', { step: unsent.step, code: unsent.code });
345
+ }
346
+ else if (this.failureSent || this.run?.status === 'done') {
347
+ await (0, promises_1.rm)(this.layout.pendingReport, { force: true });
348
+ }
349
+ }
241
350
  devFolders() {
242
351
  const parts = this.deps.env[env_1.KIT_DEV_ENV.partsDir] || null;
243
352
  const styles = this.deps.env[env_1.KIT_DEV_ENV.stylesDir] || null;
@@ -254,13 +363,28 @@ class Maker {
254
363
  this.run.status = stop && stop.reason === 'stop' ? 'stopped' : 'failed';
255
364
  await this.saveRun().catch(() => undefined);
256
365
  }
366
+ if (stop && (stop.reason === 'failed' || stop.reason === 'change_request' || stop.reason === 'refused')) {
367
+ this.reportFailure(this.at, stop.code ?? 'change_request', stop.message);
368
+ await this.reporter?.flush().catch(() => undefined);
369
+ }
257
370
  if (stop) {
258
371
  if (stop.reason === 'update_kit')
259
372
  return { status: 'update_kit', message: stop.message };
260
373
  return { status: stop.reason === 'stop' ? 'stopped' : 'failed', message: stop.message };
261
374
  }
262
- this.deps.log.write('error', 'the run ended on an error', { error: error instanceof Error ? error.message : String(error) });
263
- return { status: 'failed', message: 'This video could not be made right now. Run the same command again; what was made so far is kept.' };
375
+ this.deps.log.write('error', 'the run ended on an error', { step: this.at, error: error instanceof Error ? error.message : String(error) });
376
+ const message = 'This video could not be made right now. Run the same command again; what was made so far is kept.';
377
+ // Not a step's failure (a download or a disk write, say): a new run may get past it.
378
+ this.reportFailure(this.at, 'tool_failed', message);
379
+ await this.reporter?.flush().catch(() => undefined);
380
+ return { status: 'failed', message };
381
+ }
382
+ /** Tells the line once why the run gave up; the line's own refusals already ended it on its words. */
383
+ reportFailure(step, code, detail) {
384
+ if (!this.reporter || this.failureSent || this.reporter.refused)
385
+ return;
386
+ this.failureSent = true;
387
+ void this.reporter.fail({ step: step.slice(0, 60), code: FAILURE_CODE.test(code) ? code : 'change_request', detail: detail.trim().slice(0, 2000) });
264
388
  }
265
389
  async saveRun() {
266
390
  this.run.updated_at = this.now().toISOString();
@@ -269,6 +393,7 @@ class Maker {
269
393
  async makeHandedOver(dev) {
270
394
  const { deps, handed } = this;
271
395
  const plan = handed.plan;
396
+ this.at = 'plan';
272
397
  if (!plan || plan.project_id !== this.videoId || plan.style_id !== handed.style_package?.style_id || plan.style_version !== handed.style_package?.version) {
273
398
  throw new errors_1.KitStop('The plan handed over is not this video’s. Nothing was spent.', 'change_request');
274
399
  }
@@ -308,11 +433,14 @@ class Maker {
308
433
  throw new errors_1.KitStop('The style handed over is not the one approved for this video. Nothing was spent.', 'change_request');
309
434
  }
310
435
  const pin = { id: plan.style_id, version: plan.style_version, hash: plan.style_hash };
436
+ this.at = 'style';
437
+ const note = (why, fields) => deps.log.write('error', why, fields);
311
438
  this.style = dev.styles
312
- ? await (0, style_1.readLocalStyle)(dev.styles, pin)
313
- : await (0, style_1.fetchStyle)({ ref: handed.style_package, projectId: this.videoId, dir: this.layout.style, download: (u, m) => deps.line.download(u, m, this.stop.signal) });
439
+ ? await (0, style_1.readLocalStyle)(dev.styles, pin, note)
440
+ : await (0, style_1.fetchStyle)({ ref: handed.style_package, projectId: this.videoId, dir: this.layout.style, download: (u, m) => deps.line.download(u, m, this.stop.signal), note });
314
441
  await this.loadParts(!!dev.parts);
315
442
  // The plan's files, checked against the hashes frozen at the yes.
443
+ this.at = 'plan-files';
316
444
  const planFiles = {
317
445
  dir: this.layout.inputs,
318
446
  download: (u, m) => deps.line.download(u, m, this.stop.signal),
@@ -361,51 +489,70 @@ class Maker {
361
489
  ...this.layersOn().map((slot) => ({ key: `layer-${slot}`, ref: layerRefs?.[slot], slot })),
362
490
  ];
363
491
  for (const { key, ref, slot } of wanted) {
492
+ this.at = key;
364
493
  if (!ref || typeof ref.id !== 'string' || typeof ref.version !== 'string')
365
494
  throw new errors_1.KitStop('This video names a part it has no version for. Nothing was spent.', 'change_request');
366
495
  if (lock) {
367
496
  const entry = lock.parts[ref.id];
368
- if (!entry || entry.version !== ref.version)
369
- throw new errors_1.KitStop(`Part ${ref.id} ${ref.version} is not in this video’s parts list, so it can’t run. Nothing was spent.`, 'change_request');
497
+ if (!entry || entry.version !== ref.version) {
498
+ this.deps.log.write('error', 'part not in the parts list', { step: key, part: `${ref.id}@${ref.version}`, locked: entry?.version ?? null });
499
+ throw new errors_1.KitStop(`A part this video uses is not in this video’s parts list, so it can’t run. ${PLAN_CHANGE} Nothing was spent.`, 'change_request', 'not_locked');
500
+ }
370
501
  const tooOld = (0, version_1.kitRangeRefusal)({ id: ref.id, version: ref.version, kit: entry.kit });
371
- if (tooOld)
372
- throw new errors_1.KitStop(`${tooOld} Nothing was spent.`, 'update_kit');
502
+ if (tooOld) {
503
+ this.deps.log.write('error', 'part needs another kit', { step: key, refusal: tooOld });
504
+ throw new errors_1.KitStop('This video needs a newer video kit. Nothing was spent.', 'update_kit', 'kit_range');
505
+ }
373
506
  }
374
507
  }
375
508
  for (const { key, ref, slot } of wanted) {
509
+ this.at = key;
376
510
  let loaded;
377
511
  try {
378
512
  loaded = await this.deps.host.loader.load({ ref, lock, dev, home: this.deps.home, env: this.deps.env, signal: this.stop.signal });
379
513
  }
380
514
  catch (error) {
381
- const words = `${error instanceof Error ? error.message : 'A part could not be loaded.'} Nothing was spent.`;
382
- if (error instanceof loader_1.PartLoadError && error.code === 'kit_range')
383
- throw new errors_1.KitStop(words, 'update_kit');
384
- if (error instanceof loader_1.PartLoadError && error.code === 'unreachable')
385
- throw new errors_1.KitStop(`${words} Run the same command again in a minute.`, 'stop');
386
- throw new errors_1.KitStop(words, 'change_request');
515
+ this.deps.log.write('error', 'part could not be loaded', {
516
+ step: key,
517
+ part: `${ref.id}@${ref.version}`,
518
+ code: error instanceof loader_1.PartLoadError ? error.code : null,
519
+ error: error instanceof Error ? error.message : String(error),
520
+ });
521
+ throw loadStop(error);
387
522
  }
388
523
  const m = loaded.manifest;
389
- if (m.id !== ref.id || m.version !== ref.version)
390
- throw new errors_1.KitStop(`Part ${ref.id} ${ref.version} loaded as another version. Nothing was spent.`, 'change_request');
524
+ const refused = (why, fields = {}) => this.deps.log.write('error', why, { step: key, part: `${ref.id}@${ref.version}`, ...fields });
525
+ if (m.id !== ref.id || m.version !== ref.version) {
526
+ refused('part loaded as another version', { loaded: `${m.id}@${m.version}` });
527
+ throw new errors_1.KitStop(`A part this video uses loaded as another version, so it can’t run. ${PLAN_CHANGE} Nothing was spent.`, 'change_request', 'manifest_mismatch');
528
+ }
391
529
  const refusal = (0, version_1.interfaceRefusal)(m);
392
- if (refusal)
393
- throw new errors_1.KitStop(`${refusal} Nothing was spent.`, 'update_kit');
394
- if (slot && (m.layer !== slot || m.kind !== SLOT_KIND[slot]))
395
- throw new errors_1.KitStop(`Part ${ref.id} can’t fill the ${slot} layer. Nothing was spent.`, 'change_request');
530
+ if (refusal) {
531
+ refused('part needs another interface', { refusal });
532
+ throw new errors_1.KitStop('This video needs a newer video kit. Nothing was spent.', 'update_kit', 'interface');
533
+ }
534
+ if (slot && (m.layer !== slot || m.kind !== SLOT_KIND[slot])) {
535
+ refused('part can’t fill its layer', { slot, layer: m.layer ?? null, kind: m.kind });
536
+ throw new errors_1.KitStop(`A part this video uses can’t do the job its plan gives it. ${PLAN_CHANGE} Nothing was spent.`, 'change_request', 'wrong_layer');
537
+ }
396
538
  const lockModels = lock ? lock.parts[ref.id].models : m.needs.models;
397
539
  const paid = m.needs.network || m.needs.models.length > 0 || m.kind.startsWith('generate_');
398
- if (paid && (!Array.isArray(lockModels) || lockModels.length === 0))
399
- throw new errors_1.KitStop(`Part ${ref.id} orders paid pieces but this video’s parts list allows it none. Nothing was spent.`, 'change_request');
540
+ if (paid && (!Array.isArray(lockModels) || lockModels.length === 0)) {
541
+ refused('paid part has no models in the parts list');
542
+ throw new errors_1.KitStop(`A part this video uses would order paid pieces its approved price doesn’t allow. ${PLAN_CHANGE} Nothing was spent.`, 'change_request', 'no_models');
543
+ }
400
544
  const missing = this.missingNeeds(m.needs);
401
- if (missing)
402
- throw new errors_1.KitStop(`This computer can’t run part ${ref.id}: ${missing}. Run the computer check, then make the video again. Nothing was spent.`, 'refused');
545
+ if (missing) {
546
+ refused('part needs what this computer lacks', { missing });
547
+ throw new errors_1.KitStop(`This computer can’t make this video: ${missing}. Run the computer check, then make the video again. Nothing was spent.`, 'refused', 'needs_missing');
548
+ }
403
549
  try {
404
550
  (0, schema_1.assertCheckable)(m.inputs);
405
551
  (0, schema_1.assertCheckable)(m.outputs);
406
552
  }
407
553
  catch (error) {
408
- throw new errors_1.KitStop(`Part ${ref.id} can’t be checked by this kit (${error.message}). Nothing was spent.`, 'update_kit');
554
+ refused('part schema can’t be checked', { error: error.message });
555
+ throw new errors_1.KitStop('This video needs a newer video kit. Nothing was spent.', 'update_kit', 'schema');
409
556
  }
410
557
  this.parts.set(key, loaded);
411
558
  }
@@ -443,7 +590,7 @@ class Maker {
443
590
  };
444
591
  for (const step of this.style.style.timeline) {
445
592
  const loaded = this.parts.get(step.id);
446
- const { inputs, round } = fixed(step.id, (0, bind_1.bindInputs)(step.inputs, scope, step.id));
593
+ const { inputs, round } = fixed(step.id, this.bind(step.id, step.inputs, scope));
447
594
  const out = await this.runStep({ id: step.id, ref: step.part, loaded, inputs, models: this.modelsOf(step.part, loaded), fix: round }, hashes);
448
595
  outputs.set(step.id, out);
449
596
  if (out.timeline && typeof out.timeline === 'object')
@@ -469,8 +616,10 @@ class Maker {
469
616
  continue;
470
617
  }
471
618
  const video = out.video;
472
- if (!(0, canonical_1.isFileRef)(video) || video.media !== 'video')
473
- throw new errors_1.KitStop(`The ${slot} layer made no video.`, 'change_request');
619
+ if (!(0, canonical_1.isFileRef)(video) || video.media !== 'video') {
620
+ this.deps.log.write('error', 'layer made no video', { step: id });
621
+ throw new errors_1.KitStop(`Finishing this video gave back no video. ${PLAN_CHANGE}`, 'change_request', 'output_invalid');
622
+ }
474
623
  cut = video;
475
624
  if (out.timeline && typeof out.timeline === 'object')
476
625
  timeline = out.timeline;
@@ -485,6 +634,19 @@ class Maker {
485
634
  throw new errors_1.KitStop('The final check gave no verdict.', 'change_request');
486
635
  return { cut, captions, verdict, steps: hashes };
487
636
  }
637
+ /** A style step's inputs: a reference the kit can't resolve goes to the log, plain words to the person. */
638
+ bind(stepId, inputs, scope) {
639
+ this.at = stepId;
640
+ try {
641
+ return (0, bind_1.bindInputs)(inputs, scope, stepId);
642
+ }
643
+ catch (error) {
644
+ if (!(error instanceof errors_1.KitStop))
645
+ throw error;
646
+ this.deps.log.write('error', 'step inputs could not be bound', { step: stepId, error: error.message });
647
+ throw new errors_1.KitStop(`One step of this video can’t get what it needs, so the video stopped there. ${PLAN_CHANGE}`, 'change_request', 'bad_input');
648
+ }
649
+ }
488
650
  modelsOf(ref, loaded) {
489
651
  return this.lock ? (this.lock.parts[ref.id]?.models ?? []) : loaded.manifest.needs.models;
490
652
  }
@@ -502,10 +664,11 @@ class Maker {
502
664
  /** One step: reused when its saved record has the same hash and intact files, else run (and retried once). */
503
665
  async runStep(spec, hashes) {
504
666
  const { manifest } = spec.loaded;
667
+ this.at = spec.id;
505
668
  const inputErrors = (0, schema_1.schemaErrors)(manifest.inputs, spec.inputs, `${spec.id} inputs`);
506
669
  if (inputErrors.length) {
507
670
  this.deps.log.write('error', 'step inputs refused', { step: spec.id, errors: inputErrors });
508
- throw new errors_1.KitStop(`The style gave its ${spec.id} step something its part can’t take, so the video stopped before that step.`, 'change_request');
671
+ throw new errors_1.KitStop(`One step of this video was given something it can’t take, so the video stopped there. ${PLAN_CHANGE}`, 'change_request', 'bad_input');
509
672
  }
510
673
  const hash = (0, canonical_1.stepHash)({ interface: manifest.interface, part: spec.ref, toolchain: this.toolchain, inputs: spec.inputs, fix: spec.fix });
511
674
  hashes[spec.id] = hash;
@@ -588,18 +751,20 @@ class Maker {
588
751
  await this.saveRun();
589
752
  if (stopped)
590
753
  throw stopped;
591
- this.deps.log.write('warn', 'step failed', { step: spec.id, attempt, code: failure.code });
754
+ this.deps.log.write('warn', 'step failed', { step: spec.id, attempt, code: failure.code, detail: record.error?.detail ?? null });
592
755
  if (failure.retryable && attempt === 1 && manifest.retry?.transient !== 0)
593
756
  continue;
757
+ const words = failureWords(failure);
758
+ const code = reportedCode(failure);
594
759
  this.book.finish(spec.id, 'failed');
595
- void this.reporter.send(true);
596
- throw new errors_1.KitStop(failureWords(failure), failure.code === 'over_quote' ? 'stop' : 'failed', failure.code);
760
+ this.reportFailure(spec.id, code, words);
761
+ throw new errors_1.KitStop(words, failure.code === 'over_quote' ? 'stop' : 'failed', code);
597
762
  }
598
763
  finally {
599
764
  link.done();
600
765
  }
601
766
  }
602
- throw new errors_1.KitStop(failureWords(new errors_1.PartError('tool_failed')), 'failed');
767
+ throw new errors_1.KitStop(failureWords(new errors_1.PartError('tool_failed')), 'failed', 'tool_failed');
603
768
  }
604
769
  /** A part's output files must be in this video's folder (or the part's own) and unchanged. */
605
770
  async checkOutputFiles(outputs, partDir, tmpDir) {
@@ -705,11 +870,27 @@ class Maker {
705
870
  await this.writeFinal(null, verdict);
706
871
  this.book.finish('layer-check', 'failed');
707
872
  this.book.note = 'The final check found a problem';
708
- await this.reporter.send(true);
873
+ // Check parts give each failed check words in `message`, beside the interface's fields; only plain ones reach the card.
874
+ const said = verdict.checks.find((c) => c.status === 'fail');
875
+ const message = typeof said?.message === 'string' ? said.message : '';
876
+ const detail = (0, plain_words_1.plainWords)(message, CHECK_FAILED, this.idNames());
877
+ this.deps.log.write('warn', 'the final check failed', { check: typeof said?.code === 'string' ? said.code : null, message, detail });
878
+ this.reportFailure('layer-check', 'check_failed', detail);
879
+ await this.reporter.flush();
709
880
  this.run.status = 'failed';
710
881
  await this.saveRun();
711
882
  return { status: 'failed', message: 'The video didn’t pass the final check, so it wasn’t sent. Nothing more will be charged for it.' };
712
883
  }
884
+ /** Part, step and layer ids, which the person never sees. */
885
+ idNames() {
886
+ const names = new Set();
887
+ for (const [key, loaded] of this.parts) {
888
+ names.add(key);
889
+ names.add(loaded.manifest.id);
890
+ names.add(`${loaded.manifest.id}@${loaded.manifest.version}`);
891
+ }
892
+ return [...names];
893
+ }
713
894
  async writeFinal(result, verdict) {
714
895
  await (0, promises_1.mkdir)(this.layout.final, { recursive: true, mode: 0o700 });
715
896
  await (0, save_1.writeJson)(path.join(this.layout.final, 'check.json'), verdict);
@@ -737,6 +918,7 @@ class Maker {
737
918
  /** Upload, our server's check, and one fix after a first failed check. */
738
919
  async upload(result) {
739
920
  let current = result;
921
+ this.at = 'upload';
740
922
  for (;;) {
741
923
  const final = (await this.writeFinal(current, current.verdict));
742
924
  const data = await (0, promises_1.readFile)(final);
@@ -745,13 +927,13 @@ class Maker {
745
927
  const sha256 = (0, canonical_1.sha256Hex)(data);
746
928
  // Only the exact bytes the final check passed are sent.
747
929
  if (sha256 !== current.cut.sha256 || data.length !== current.cut.bytes) {
748
- throw new errors_1.KitStop('The finished video changed after its final check, so it wasn’t sent. Run the same command again to make it again.', 'failed');
930
+ throw new errors_1.KitStop('The finished video changed after its final check, so it wasn’t sent. Run the same command again to make it again.', 'failed', 'tool_failed');
749
931
  }
750
932
  let captions_vtt;
751
933
  if (current.captions) {
752
934
  const captions = await (0, promises_1.readFile)(current.captions.path);
753
935
  if ((0, canonical_1.sha256Hex)(captions) !== current.captions.sha256 || captions.length !== current.captions.bytes) {
754
- throw new errors_1.KitStop('The captions changed after the final check, so the video wasn’t sent. Run the same command again to make it again.', 'failed');
936
+ throw new errors_1.KitStop('The captions changed after the final check, so the video wasn’t sent. Run the same command again to make it again.', 'failed', 'tool_failed');
755
937
  }
756
938
  if (captions.length > types_1.MAX_CAPTIONS_BYTES)
757
939
  throw new errors_1.KitStop('The captions file is larger than the line takes.', 'failed');