edf2csv 0.5.86 → 0.5.88

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 CHANGED
@@ -3,6 +3,63 @@
3
3
  Notable changes to edf2csv. Versions follow [semantic versioning](https://semver.org); while the
4
4
  major version is 0, a minor bump may contain breaking changes.
5
5
 
6
+ ## 0.5.88
7
+
8
+ ### Fixed: a worker killed from outside made the batch exit 2, "the command line is the problem"
9
+
10
+ `worstOf` combines the children's exit codes into the run's:
11
+
12
+ ```ts
13
+ return codes.includes(EXIT_ERROR) ? EXIT_ERROR : EXIT_USAGE;
14
+ ```
15
+
16
+ which assumes every child exits 1 or 2. A child killed by a signal exits 130 or 143 — its own
17
+ interrupt handler does that — so a `--jobs` worker stopped by SIGTERM fell through to 2. The
18
+ exit-code table calls 2 "The command line is the problem", and warnings-and-errors "The command
19
+ was invoked incorrectly, or asked for something the recording can't provide". The command was
20
+ fine; something killed a worker, which is how the out-of-memory killer and a scheduler's time
21
+ limit both arrive. A script branching on 2 to mean "I typed it wrong" would retry forever.
22
+
23
+ Asked the other way round now: 2 is the narrow claim, so every recording has to have earned it,
24
+ and anything else in the list makes the run a failure. `[143]` is 1, `[2]` is still 2, `[2, 143]`
25
+ is 1.
26
+
27
+ Pinned by testing the mapping directly rather than by racing a signal at a real worker — the
28
+ mapping is the defect, and a batch long enough to catch mid-flight is a race in a test suite.
29
+
30
+ ## 0.5.87
31
+
32
+ ### Fixed: `--info --stdout` described files the command never writes
33
+
34
+ ```
35
+ $ edf2csv mixed-rates.edf --info --stdout
36
+ Would write 1,155 rows, roughly 22.2 KB.
37
+ warning: Channels use 3 different sampling rates (256 Hz, 128 Hz, 1 Hz).
38
+ They are written to one file per rate so no channel is resampled.
39
+ ```
40
+
41
+ for a command that refuses to run, writes nothing, and names no file:
42
+
43
+ ```
44
+ $ edf2csv mixed-rates.edf --stdout
45
+ error: --stdout needs exactly one table, but this recording produces 3 ...
46
+ ```
47
+
48
+ `--info` exists to say what a conversion will do, and refusing is one of the things it does.
49
+ It was not passed `--stdout` at all, so the plan it described was a different command's.
50
+
51
+ The conversion's three `--stdout` guards are lifted into `stdoutRefusal` and `--info` asks the
52
+ same question, so there is one wording rather than two that can drift — the test asserts the
53
+ preview contains the refusal's own sentence. Reported as a warning rather than a refusal, for
54
+ the reason 0.5.51 gives about the destination guards: `--info` writes nothing, so a rule about
55
+ the output has no business stopping it from describing the recording, and being told the command
56
+ will not work is exactly what was asked. New diagnostic code `STDOUT_UNSUPPORTED`, documented on
57
+ all three pages.
58
+
59
+ `--info --stdout --json` is no longer refused either. That guard exists because a CSV and a
60
+ summary cannot share stdout; under `--info` there is no CSV, and the description *is* the JSON.
61
+ It was the one way a script could see this warning at all.
62
+
6
63
  ## 0.5.86
7
64
 
8
65
  ### Fixed: a file holding half a million samples was told no data was written
package/dist/cli.d.ts CHANGED
@@ -9,4 +9,13 @@
9
9
  */
10
10
  import { defaultOutputDir } from './convert/run.js';
11
11
  export declare function main(argv: readonly string[]): Promise<number>;
12
- export { defaultOutputDir };
12
+ /**
13
+ * The exit code for a run in which several files failed for different reasons.
14
+ *
15
+ * 2 means "you invoked it wrong" and 1 means "something went wrong with a file". When both
16
+ * happened, 1 is the honest answer: the invocation cannot be the whole story once a file has
17
+ * genuinely failed. With a single input there is only ever one code, so its exit status is
18
+ * exactly what it always was.
19
+ */
20
+ declare function worstOf(codes: readonly number[]): number;
21
+ export { defaultOutputDir, worstOf };
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import process from 'node:process';
18
18
  import { EdfError } from './edf/errors.js';
19
19
  import { EdfFile } from './edf/reader.js';
20
20
  import { buildPlan, withoutFileRateWarning } from './convert/plan.js';
21
- import { ConversionError, USAGE_ERROR_CODES, convert, defaultOutputDir } from './convert/run.js';
21
+ import { ConversionError, USAGE_ERROR_CODES, convert, defaultOutputDir, stdoutRefusal, } from './convert/run.js';
22
22
  import { ChannelSelectionError } from './convert/channels.js';
23
23
  // Shared with the library so a bad option is the same error whichever way it arrived.
24
24
  import { OptionError } from './convert/options.js';
@@ -256,7 +256,17 @@ export async function main(argv) {
256
256
  // Both of these claim stdout. Allowing them together wrote the CSV and then the summary
257
257
  // object onto one stream, producing a document that is neither valid CSV nor valid
258
258
  // JSON — and silently, since each half looked right on its own.
259
- if (toStdout && asJson) {
259
+ /*
260
+ Not under --info, which writes no CSV for --json to collide with.
261
+
262
+ The two claim stdout because one puts a CSV there and the other a summary. `--info` puts
263
+ neither: it prints a description, and under --json that description *is* the JSON. So
264
+ `--info --stdout --json` — a script asking "would --stdout work on this recording, in a
265
+ form I can parse" — was refused for a conflict that cannot arise, which is the same shape
266
+ as the destination guards 0.5.51 stopped applying to it. It is also the only way to see
267
+ the STDOUT_UNSUPPORTED warning from a script.
268
+ */
269
+ if (toStdout && asJson && values['info'] !== true) {
260
270
  /*
261
271
  `error: ` and the seven-space continuation, like every other refusal.
262
272
 
@@ -409,7 +419,7 @@ export async function main(argv) {
409
419
  if (batch && !asJson && index > 0)
410
420
  process.stdout.write('\n');
411
421
  try {
412
- warnings += await showInfo(input, shared, asJson, jsonIndent, batch);
422
+ warnings += await showInfo(input, shared, asJson, jsonIndent, batch, values['stdout'] === true);
413
423
  }
414
424
  catch (error) {
415
425
  failures.push(reportError(error, batch ? input : undefined));
@@ -618,7 +628,7 @@ export async function main(argv) {
618
628
  }
619
629
  }
620
630
  /** Print one recording's `--info`, and return how many diagnostics it raised. */
621
- async function showInfo(input, shared, asJson, jsonIndent, batch = false) {
631
+ async function showInfo(input, shared, asJson, jsonIndent, batch = false, toStdout = false) {
622
632
  const file = await EdfFile.open(input);
623
633
  try {
624
634
  /*
@@ -664,6 +674,30 @@ async function showInfo(input, shared, asJson, jsonIndent, batch = false) {
664
674
  recordStarts: timing.starts,
665
675
  }, shared);
666
676
  plan.diagnostics.push(...timing.diagnostics);
677
+ /*
678
+ What --stdout would do with this recording, which --info did not ask.
679
+
680
+ `--info --stdout` on a three-rate file predicted "Would write 1,155 rows, roughly
681
+ 22.2 KB" and said the channels "are written to one file per rate" — for a command that
682
+ refuses to run, writes nothing and names no file. --info exists to say what a
683
+ conversion will do, and refusing is one of the things it does.
684
+
685
+ A warning rather than a refusal, for the reason 0.5.51 gives about the destination
686
+ guards: --info writes nothing, so a rule about the output has no business stopping it
687
+ from describing the recording — and being told the command will not work is exactly
688
+ what was asked. The conversion's own guard supplies the words, so there is one wording.
689
+ */
690
+ if (toStdout) {
691
+ const refusal = stdoutRefusal(file, plan);
692
+ if (refusal) {
693
+ plan.diagnostics.push({
694
+ code: 'STDOUT_UNSUPPORTED',
695
+ severity: 'warning',
696
+ message: `--stdout would refuse this recording: ${refusal.message.replace(/^--stdout /u, '')}`,
697
+ ...(refusal.hint === undefined ? {} : { hint: refusal.hint }),
698
+ });
699
+ }
700
+ }
667
701
  process.stdout.write(asJson ? `${infoJson(file, plan, jsonIndent)}\n` : `${formatInfo(file, plan)}\n`);
668
702
  // Under --json the warnings travel inside the document, exactly as they do for a
669
703
  // conversion, so stderr stays empty and the whole result is one parseable thing.
@@ -1254,7 +1288,22 @@ function assertDistinct(inputs, destinations) {
1254
1288
  * exactly what it always was.
1255
1289
  */
1256
1290
  function worstOf(codes) {
1257
- return codes.includes(EXIT_ERROR) ? EXIT_ERROR : EXIT_USAGE;
1291
+ /*
1292
+ Anything that is not "the command line is the problem" is a failure of the run.
1293
+
1294
+ This tested for `EXIT_ERROR` and fell through to `EXIT_USAGE` for everything else, on the
1295
+ assumption that a child exits 1 or 2. A child killed by a signal exits 130 or 143 — its
1296
+ own interrupt handler does that — so a `--jobs` worker stopped by SIGTERM made the batch
1297
+ exit 2, which cli-reference's table defines as "The command line is the problem" and
1298
+ warnings-and-errors as "The command was invoked incorrectly, or asked for something the
1299
+ recording can't provide". The command was fine; something killed a worker. The
1300
+ out-of-memory killer and a scheduler's time limit both arrive exactly this way, and the
1301
+ serial path reports the same event as the failure it is.
1302
+
1303
+ Asking the question the other way round is also the honest one: 2 is the narrow claim,
1304
+ so it should be the one that has to be earned by every code in the list.
1305
+ */
1306
+ return codes.some((code) => code !== EXIT_USAGE) ? EXIT_ERROR : EXIT_USAGE;
1258
1307
  }
1259
1308
  /**
1260
1309
  * Convert one recording in a separate process, and collect everything it printed.
@@ -1567,7 +1616,10 @@ function message(error, indent = '') {
1567
1616
  // one, alongside the usage text, so they pass nothing and stay flush left.
1568
1617
  return printableLines(error instanceof Error ? error.message : String(error), indent);
1569
1618
  }
1570
- export { defaultOutputDir };
1619
+ // `worstOf` is exported for the test that pins the exit-code mapping. A batch's verdict is
1620
+ // one of the few things a script branches on, and the mapping is easier to check directly
1621
+ // than by racing a signal at a worker.
1622
+ export { defaultOutputDir, worstOf };
1571
1623
  /**
1572
1624
  * Whether this file was executed rather than imported.
1573
1625
  *